diff -Nru eoxserver-0.4.0beta2/debian/changelog eoxserver-0.3.2/debian/changelog --- eoxserver-0.4.0beta2/debian/changelog 2015-01-06 12:38:21.000000000 +0000 +++ eoxserver-0.3.2/debian/changelog 2015-01-20 18:58:28.000000000 +0000 @@ -1,23 +1,29 @@ -eoxserver (0.4.0beta2-0~trusty0) trusty; urgency=low +eoxserver (0.3.2-0~trusty5) trusty; urgency=low - * 0.4 beta2-release. + * Rebuild. - -- Angelos Tzotsos Tue, 06 Jan 2015 14:00:00 +0200 + -- Angelos Tzotsos Tue, 20 Jan 2015 21:04:00 +0200 -eoxserver (0.4.0beta1-0~trusty1) trusty; urgency=low +eoxserver (0.3.2-0~trusty4) trusty; urgency=low - * 0.4 beta-release. + * Added binary scripts. - -- Fabian Schindler Mon, 16 Dec 2014 16:02:00 +0200 + -- Angelos Tzotsos Tue, 29 Jul 2014 19:20:00 +0200 + +eoxserver (0.3.2-0~trusty3) trusty; urgency=low + + * Added installation file. + + -- Angelos Tzotsos Mon, 28 Jul 2014 19:38:00 +0200 eoxserver (0.3.2-0~trusty2) trusty; urgency=low * Disabled reftools. - -- Angelos Tzotsos Mon, 29 Jul 2014 16:04:00 +0200 + -- Angelos Tzotsos Mon, 28 Jul 2014 16:04:00 +0200 eoxserver (0.3.2-0~trusty1) trusty; urgency=low * Initial debian build. - -- Angelos Tzotsos Sun, 28 Jul 2014 15:54:00 +0200 + -- Angelos Tzotsos Sun, 27 Jul 2014 15:54:00 +0200 diff -Nru eoxserver-0.4.0beta2/debian/control eoxserver-0.3.2/debian/control --- eoxserver-0.4.0beta2/debian/control 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/debian/control 2014-07-27 14:09:46.000000000 +0000 @@ -9,8 +9,14 @@ autoconf, automake, autotools-dev, + gcc, + libproj-dev, + libgeos-dev, + libgeos++-dev, + libgdal-dev (>= 1.10.1-0~), python-all (>= 2.6.6-3~), - python-all-dev (>= 2.6.6-3~) + python-all-dev (>= 2.6.6-3~), + libmapserver1-dev Standards-Version: 3.9.3 X-Python-Version: >= 2.6 Homepage: http://eoxserver.org diff -Nru eoxserver-0.4.0beta2/debian/rules eoxserver-0.3.2/debian/rules --- eoxserver-0.4.0beta2/debian/rules 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/debian/rules 2014-07-28 14:48:22.000000000 +0000 @@ -5,7 +5,7 @@ dh $@ --with python2 override_dh_auto_build: - python setup.py build + python setup.py build --disable-extended-reftools override_dh_auto_install: - python setup.py install --no-compile --root=$(CURDIR)/debian/tmp --install-layout=deb; \ No newline at end of file + python setup.py install --disable-extended-reftools --no-compile --root=$(CURDIR)/debian/tmp --install-layout=deb; \ No newline at end of file diff -Nru eoxserver-0.4.0beta2/eoxserver/backends/access.py eoxserver-0.3.2/eoxserver/backends/access.py --- eoxserver-0.4.0beta2/eoxserver/backends/access.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/backends/access.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,180 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -import hashlib -import logging - -from eoxserver.backends.cache import get_cache_context -from eoxserver.backends.component import BackendComponent, env - - -logger = logging.getLogger(__name__) - - -def generate_hash(location, format, hash_impl="sha1"): - h = hashlib.new(hash_impl) - if format is not None: - h.update(format) - h.update(location) - return h.hexdigest() - - -def connect(data_item, cache=None): - """ Connect to a :class:`DataItem `. - If the data item is not connectable but retrievable, this function uses - :func:`retrieve` as a fallback. - - :param data_item: the :class:`DataItem ` - to connect to - :param cache: an instance of :class:`CacheContext - ` or ``None`` - if the caching shall be handled internally - :returns: the connection string to retrieve data from or a local path - if the ``DataItem`` was ``retrieved`` - """ - - backend = BackendComponent(env) - - storage = data_item.storage - - if storage: - component = backend.get_connected_storage_component( - storage.storage_type - ) - - if not storage or not component: - return retrieve(data_item, cache) - - return component.connect(storage.url, data_item.location) - - -def retrieve(data_item, cache=None): - """ Retrieve a :class:`DataItem `, i.e: - make it locally available. This takes into account any download from a - :class:`Storage ` and any unpacking from - a :class:`Package ` the ``DataItem`` - might be contained in. - - :param data_item: the :class:`DataItem ` - to connect retrieve - :param cache: an instance of :class:`CacheContext - ` or ``None`` - if the caching shall be handled internally - """ - - backend = BackendComponent(env) - - if cache is None: - cache = get_cache_context() - - # compute a cache path where the file *would* be cached - with cache: - item_id = generate_hash(data_item.location, data_item.format) - path = cache.relative_path(item_id) - - logger.debug("Retrieving %s (ID: %s)" % (data_item, item_id)) - - if item_id in cache: - logger.debug("Item %s is already in the cache." % item_id) - return path - - if data_item.package is None and data_item.storage: - return _retrieve_from_storage( - backend, data_item, data_item.storage, item_id, path, cache - ) - - elif data_item.package: - return _extract_from_package( - backend, data_item, data_item.package, item_id, path, cache - ) - - else: - return data_item.location - - -def _retrieve_from_storage(backend, data_item, storage, item_id, path, cache): - """ Helper function to retrieve a file from a storage. - """ - - logger.debug("Accessing storage %s." % storage) - - component = backend.get_file_storage_component( - storage.storage_type - ) - - actual_path = component.retrieve( - storage.url, data_item.location, path - ) - - if actual_path and actual_path != path: - cache.add_mapping(actual_path, item_id) - - return actual_path or path - - -def _extract_from_package(backend, data_item, package, item_id, path, cache): - """ Helper function to extract a file from a package. - """ - - logger.debug("Accessing package %s." % package) - - package_location = retrieve(package, cache) - - component = backend.get_package_component( - package.format - ) - - logger.debug( - "Extracting from %s: %s and saving it at %s" - % (package_location, data_item.location, path) - ) - - actual_path = component.extract( - package_location, data_item.location, path - ) - - if actual_path and actual_path != path: - cache.add_mapping(actual_path, item_id) - - return actual_path or path - - -def open(data_item, cache=None): - """ Returns a file object pointing to the given location. This function - works like the builtin function :func:`open() <__builtins__.open>` but on - a :class:`DataItem ` and performs a - :func:`retrieve` first. - - :param data_item: the :class:`DataItem ` - to open - :param cache: an instance of :class:`CacheContext - ` or ``None`` - if the caching shall be handled internally - """ - - return __builtins__.open(retrieve(data_item, cache)) diff -Nru eoxserver-0.4.0beta2/eoxserver/backends/admin.py eoxserver-0.3.2/eoxserver/backends/admin.py --- eoxserver-0.4.0beta2/eoxserver/backends/admin.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/backends/admin.py 2013-12-10 14:57:00.000000000 +0000 @@ -1,9 +1,10 @@ #------------------------------------------------------------------------------- +# $Id$ # # Project: EOxServer -# Authors: Fabian Schindler +# Authors: Stephan Krause # Stephan Meissl -# Stephan Krause +# Fabian Schindler # #------------------------------------------------------------------------------- # Copyright (C) 2011 EOX IT Services GmbH @@ -11,8 +12,8 @@ # 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 +# 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 @@ -27,88 +28,77 @@ # THE SOFTWARE. #------------------------------------------------------------------------------- -from django import forms -from django.contrib import admin - -from eoxserver.backends.component import BackendComponent, env -from eoxserver.backends import models +from eoxserver.backends.models import ( + Location, FTPStorage, RasdamanStorage, LocalPath, + RemotePath, RasdamanLocation, CacheFile, +) +from django.contrib import admin #=============================================================================== -# choice helpers +# Generic Storage Admin (Abstract!) #=============================================================================== - -def get_format_choices(): - backend_component = BackendComponent(env) - return map(lambda r: (r.name, r.get_supported_formats()), backend_component.data_readers) - - -def get_package_format_choices(): - backend_component = BackendComponent(env) - return map(lambda p: (p.name, p.name), backend_component.packages) - - -def get_storage_type_choices(): - backend_component = BackendComponent(env) - return map(lambda p: (p.name, p.name), backend_component.storages) - +class StorageAdmin(admin.ModelAdmin): + def save_model(self, request, obj, form, change): + obj.storage_type = self.model.STORAGE_TYPE + obj.save() #=============================================================================== -# Forms +# Local Path #=============================================================================== +class LocalPathAdmin(admin.ModelAdmin): + model = LocalPath + + list_display = ("location_type", "path",) + list_editable = ("path",) -class StorageForm(forms.ModelForm): - """ Form for `Storages`. Overrides the `format` formfield and adds choices - dynamically. - """ - - def __init__(self, *args, **kwargs): - super(StorageForm, self).__init__(*args, **kwargs) - self.fields['storage_type'] = forms.ChoiceField( - choices=[("---------", None)] + get_storage_type_choices() - ) - - -class LocationForm(forms.ModelForm): - """ Form for `Locations`. Overrides the `format` formfield and adds choices - dynamically. - """ - - def __init__(self, *args, **kwargs): - super(LocationForm, self).__init__(*args, **kwargs) - #self.fields['format'] = forms.ChoiceField( - # choices=[("---------", None)] + get_format_choices() - #) +admin.site.register(LocalPath, LocalPathAdmin) +#=============================================================================== +# FTP Storage Admin +#=============================================================================== -class PackageForm(forms.ModelForm): - """ Form for `Packages`. Overrides the `format` formfield and adds choices - dynamically. - """ +class RemotePathInline(admin.TabularInline): + model = RemotePath + extra = 1 +class FTPStorageAdmin(StorageAdmin): + inlines = (RemotePathInline, ) - def __init__(self, *args, **kwargs): - super(PackageForm, self).__init__(*args, **kwargs) - self.fields['format'] = forms.ChoiceField( - choices=[("---------", None)] + get_package_format_choices() - ) +class RemotePathAdmin(admin.ModelAdmin): + model = RemotePath + + list_display = ("location_type", "path",) + list_editable = ("path",) +admin.site.register(FTPStorage, FTPStorageAdmin) +admin.site.register(RemotePath, RemotePathAdmin) #=============================================================================== -# Admins +# Rasdaman Storage Admin #=============================================================================== +class RasdamanLocationInline(admin.TabularInline): + model = RasdamanLocation + extra = 1 +class RasdamanStorageAdmin(StorageAdmin): + inlines = (RasdamanLocationInline,) -class StorageAdmin(admin.ModelAdmin): - form = StorageForm - model = models.Storage - -admin.site.register(models.Storage, StorageAdmin) +class RasdamanLocationAdmin(admin.ModelAdmin): + model = RasdamanLocation + + list_display = ("location_type", "collection", "oid") + list_editable = ("collection", "oid") + +admin.site.register(RasdamanStorage, RasdamanStorageAdmin) +admin.site.register(RasdamanLocation, RasdamanLocationAdmin) +#=============================================================================== +# Cache File Admin +#=============================================================================== -class PackageAdmin(admin.ModelAdmin): - form = PackageForm - model = models.Package +class CacheFileAdmin(admin.ModelAdmin): + pass -admin.site.register(models.Package, PackageAdmin) +admin.site.register(CacheFile, CacheFileAdmin) diff -Nru eoxserver-0.4.0beta2/eoxserver/backends/base.py eoxserver-0.3.2/eoxserver/backends/base.py --- eoxserver-0.4.0beta2/eoxserver/backends/base.py 1970-01-01 00:00:00.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/backends/base.py 2013-12-10 14:57:00.000000000 +0000 @@ -0,0 +1,93 @@ +#------------------------------------------------------------------------------- +# $Id$ +# +# Project: EOxServer +# Authors: Stephan Krause +# Stephan Meissl +# +#------------------------------------------------------------------------------- +# Copyright (C) 2011 EOX IT Services GmbH +# +# 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 of this Software or works derived from this 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. +#------------------------------------------------------------------------------- + +from eoxserver.core.system import System +from eoxserver.core.records import RecordWrapper +from eoxserver.core.exceptions import InternalError + +class LocationWrapper(RecordWrapper): + """ + This is the base class for location wrappers. It inherits from + :class:`~.RecordWrapper`. It should not be instantiated + directly, but one of its subclasses should be used. + """ + + def __init__(self): + super(LocationWrapper, self).__init__() + + self.storage = self._bind_to_storage() + + def _bind_to_storage(self): + # While a working implementation this method should be overridden + # by subclasses for efficiency's sake + + return System.getRegistry().findAndBind( + intf_id = "backends.interfaces.StorageInterface", + params = { + "backends.interfaces.storage_type": self.getType() + } + ) + + def getStorageCapabilities(self): + """ + Returns the capabilities of the corresponding storage. See + :meth:`.StorageInterface.getStorageCapabilities` for details. + """ + return self.storage.getStorageCapabilities() + + def getSize(self): + """ + Returns the size (in bytes) of the object at the location. Raises + :exc:`~.InternalError` if the corresponding storage is not capable of + retrieving the size. See :meth:`.StorageInterface.getSize` for details. + """ + return self.storage.getSize(self) + + def getLocalCopy(self, target): + """ + Copies the resource to the path ``target`` on the local file system. + Raises :exc:`~.InternalError` if the corresponding storage is not + capable of copying data. See :meth:`.StorageInterface.getLocalCopy` for + details. + """ + return self.storage.getLocalCopy(self, target) + + def detect(self, search_pattern=None): + """ + Searches the location for objects that match ``search_pattern``. If the + parameter is omitted, all found objects are returned. It returns a list + of locations of the same type that point to these objects. Raises + :exc:`~.InternalError` if the corresponding storage is not capable of + auto-detection. See :meth:`.StorageInterface.detect` for details. + """ + return self.storage.detect(self, search_pattern) + + def exists(self): + return self.storage.exists(self) + diff -Nru eoxserver-0.4.0beta2/eoxserver/backends/cache.py eoxserver-0.3.2/eoxserver/backends/cache.py --- eoxserver-0.4.0beta2/eoxserver/backends/cache.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/backends/cache.py 2013-12-10 14:57:00.000000000 +0000 @@ -1,8 +1,8 @@ #------------------------------------------------------------------------------- +# $Id$ # # Project: EOxServer -# Authors: Fabian Schindler -# Stephan Krause +# Authors: Stephan Krause # Stephan Meissl # #------------------------------------------------------------------------------- @@ -11,8 +11,8 @@ # 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 +# 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 @@ -27,181 +27,385 @@ # THE SOFTWARE. #------------------------------------------------------------------------------- -import os -from os import path -import shutil -import tempfile -import errno -import logging -import threading - -from eoxserver.core.config import get_eoxserver_config -from eoxserver.backends.config import CacheConfigReader - - -logger = logging.getLogger(__name__) - - -# global instance of the cache context -cache_context_storage = threading.local() - - -class CacheException(Exception): - pass - - -def setup_cache_session(config=None): - """ Initialize the cache context for this session. If a cache context was - already present, an exception is raised. - """ - if not config: - config = CacheConfigReader(get_eoxserver_config()) - - set_cache_context( - CacheContext(config.retention_time, config.directory, True) - ) - - -def shutdown_cache_session(): - """ Shutdown the cache context for this session and trigger any pending - cleanup actions required. - """ - try: - cache_context = get_cache_context() - cache_context.cleanup() - except CacheException: - # it seems that the cache was already shut down. - pass - set_cache_context(None) - - -def set_cache_context(cache_context): - """ Sets the cache context for this session. Raises an exception if there - was already a cache context associated. +""" +This module provides an implementation of a cache, intended primarily +for caching content from remote backends. + +.. warning:: The current implementation of the :class:`Cache` class is not + functional and shall not be used. A future implementation must be able to + work properly in a multi-process multi-threaded environment (i.e. provide + some kind of data access synchronization). This requires inter-process + communication to be implemented and is thus too much of an effort for the + time being. +""" + + +import os.path +from datetime import datetime +from time import sleep +from threading import Thread, RLock + +from eoxserver.core.system import System +from eoxserver.core.readers import ConfigReaderInterface +from eoxserver.backends.models import ( + CacheFile, LocalPath +) +from eoxserver.backends.exceptions import DataAccessError +from eoxserver.core.exceptions import ConfigError + +class Cache(object): + # IMPORTANT: DO NOT USE THIS IMPLEMENTATION! + # It is essentially crap, useless and potentially harmful + + _size = None + _size_lock = RLock() + + _free_size = None + _free_thread = None + _free_lock = RLock() + + @classmethod + def addFile(cls, cache_file): + """ + This method registers that a file is added to the cache and + updates the cache size accordingly. + """ + file_size = cache_file.getSize() + + if file_size is not None: + cls._size_lock.acquire() + cls._size += file_size + cls._size_lock.release() + + if not cls.check(0): + self.free() + + @classmethod + def removeFile(cls, cache_file): + file_size = cache_file.getSize() + + if file_size is not None: + cls._size_lock.acquire() + cls._size -= file_size + cls._size_lock.release() + + @classmethod + def synchronizeSize(cls): + cls._size_lock.acquire() + cls._size = CacheFile.objects.aggregate(cache_size=Sum('size'))["cache_size"] + cls._size_lock.release() + + @classmethod + def check(cls, file_size): + cls._size_lock.acquire() + preview_size = cls._size + file_size + cls._size_lock.release() + + return preview_size < CacheConfigReader.getMaxSize() + + @classmethod + def free(cls, free_size=None, defensive=False): + """ + This method frees cache space. It takes two optional arguments: + + * ``free_size`` denotes the free cache space that shall be + available (at least) after the operation + * ``defensive``: a boolean which determines if the operation + shall be defensive (i.e. only free cache space until the + cache can accomodate for a file of ``free_size`` bytes) or + offensive (i.e. free cache space until there are no more + cache files left to discard). It defaults to ``False``, i.e. + offensive cache freeing. + + If ``free_size`` is omitted and ``defensive`` is set to + ``False`` (the default) every cache file that may be discarded + will be purged. If ``free_size`` is omitted and ``defensive`` + is set to ``True`` the method will return immediately. + + Note that when freeing in "offensive" mode a separate thread + will be created that purges files from the cache. Execution of + the calling thread is halted until the free cache space is + big enough to accomodate for ``free_size`` bytes. The cache + freeing thread will then continue in the background, while the + calling thread can resume its work. + """ + + if not free_size and defensive: + pass + else: + cls._free_lock.acquire() + + try: + if cls._free_thread and cls._free_thread.isAlive(): + if not cls._free_size == "max": + if free_size is None: + cls._free_size = "max" + else: + cls._free_size += free_size + else: + if free_size is None: + cls._free_size = "max" + else: + cls._free_size = free_size + + cls._free_thread = Thread( + target=cls._free_background, + kwargs={"defensive": defensive} + ) + + cls._free_thread.start() + + cls._free_lock.release() + + except: + cls._free_lock.release() + + raise + + while not cls._free_size_reached(): + sleep(0.001) + + @classmethod + def _free_background(cls, defensive=False): + # this method is run in a separate thread of execution and + # loops over all removable cache files. It will stop when there + # are no more files left to discard + + before = datetime.now() - timedelta( + hours=CacheConfigReader().getRetentionTime() + ) + + cache_file_records = list(CacheFile.objects.filter( + access_timestamp__lt = before + ).order_by('access_timestamp')) + + while len(cache_file_records) > 0 and ((defensive and not cls._free_size_reached()) or not defensive): + cache_file = CacheFile(cache_file_records.pop()) + + cache_file.purge() + + cls._free_lock.acquire() + cls._free_size = None + cls._free_lock.release() + + @classmethod + def _free_size_reached(cls): + cls._free_lock.acquire() + if cls._free_size == "max": + ret_val = False + else: + ret_val = cls.check(cls._free_size) + cls._free_lock.release() + + return ret_val + +class CacheFileWrapper(object): """ - if cache_context is not None: - if getattr(cache_context_storage, "cache_context", None) is not None: - raise CacheException( - "The cache context for this session was already initialized." - ) - - cache_context_storage.cache_context = cache_context - - -def get_cache_context(): - """ Get the thread local cache context for this session. Raises an exception - if the session was not initialized. + This class wraps :class:`~.CacheFile` records and adds the logic to handle + them to the database model. """ - cache_context = getattr(cache_context_storage, "cache_context", None) - if not cache_context: - raise CacheException( - "The cache context for this session was not initialized." + + def __init__(self, model): + self.__model = model + + @classmethod + def create(cls, filename): + """ + This class method creates a :class:`CacheFileWrapper` instance for the + given file name. It makes a database record for the cache file, but + does NOT copy it from its location to the cache. You have to call + :meth:`copy` on the instance for that. + """ + #======================================================================= + # provisional solution + + cache_dir = System.getRegistry().bind("backends.cache.CacheConfigReader").getCacheDir() + + if cache_dir is None: + raise ConfigError("Cache directory is not configured.") + + target_dir = os.path.join( + cache_dir, "cache_%s" % datetime.now().strftime("%Y%m%d") ) - return cache_context - - -class CacheContext(object): - """ Context manager to manage cached files. - """ - def __init__(self, retention_time=None, cache_directory=None, managed=False): - self._cached_objects = set() - - if not cache_directory: - cache_directory = tempfile.mkdtemp(prefix="eoxs_cache") - self._temporary_dir = True - else: - self._temporary_dir = False - - self._cache_directory = cache_directory - self._retention_time = retention_time - self._level = 0 - self._mappings = {} - - self._managed = managed - - - @property - def cache_directory(self): - """ Returns the configured cache directory. + + if not os.path.exists(target_dir): + os.makedirs(target_dir) + + #----------------------------------------------------------------------- + # use this when viable cache implementation is finished + #target_dir = Cache.getTargetDir() + + #======================================================================= + + dest_path = os.path.join(target_dir, filename) + + location_record = LocalPath.objects.create( + location_type = LocalPath.LOCATION_TYPE, + path = dest_path + ) + + model = CacheFile.objects.create( + location = location_record, + size = None, + access_timestamp = datetime.now() + ) + + cache_file = cls(model) + + return cache_file + + def getModel(self): """ - return self._cache_directory - - - def relative_path(self, cache_path): - """ Returns a path relative to the cache directory. + Returns the model record wrapped by the implementation. """ - return path.join(self._cache_directory, cache_path) - + + return self.__model - def add_mapping(self, path, item): - """ Add an external file to this context. Those files will be treated as - if they are "within" the caches directory, but will not be clead up - afterwards. + def getLocation(self): """ - self._mappings[path] = item - - - def add_path(self, cache_path): - """ Add a path to this cache context. Also creates necessary - sub-directories. + Returns the a :class:`~.LocalPathWrapper` object pointing to the + location of the cache file. """ - self._cached_objects.add(cache_path) - relative_path = self.relative_path(cache_path) - - try: - # create all necessary subdirectories - os.makedirs(path.dirname(relative_path)) - except OSError, e: - # it's only ok if the dir already existed - if e.errno != errno.EEXIST: - raise - - return relative_path - - - def cleanup(self): - """ Perform cache cleanup. + return System.getRegistry().getFromFactory( + "backends.factories.LocationFactory", + { + "record": self.__model.location + } + ) + + def getSize(self): """ - if self._retention_time and not self._temporary_dir: - # no cleanup required - return - - elif not self._temporary_dir: - for path in self._cached_objects: + Returns the size of the cache file in bytes. Note that the return value + is ``None`` if the :class:`CacheFileWrapper` instance has been + initialized already, but :meth:`copy` has not been called yet. + """ + return self.__model.size + + def copy(self, location): + """ + Copy the file from its current ``location`` to the cache. This may + raise :exc:`~.InternalError` if the storage implementation for the + location does not support the :meth:`~.StorageInterface.getSize` and/or + :meth:`~.StorageInterface.getLocalCopy` methods or + :exc:`~.DataAccessError` if there was a fault when retrieving the + original file. + """ + + # this may raise InternalError + size = location.getSize() + + # make cache space for the file; this is disabled at the + # moment due to insufficiencies in the cache implementation + #if size is not None: + # if not Cache.check(size): + # Cache.free(size) + + # this may raise InternalError or DataAccessError; just pass + # them on + location.getLocalCopy(self.__model.location.path) + + self.__model.access_timestamp = datetime.now() + + self.__model.size = \ + os.path.getsize(self.__model.location.path) + + self.__model.save() + + # register the file at the cache; this is disabled at the + # moment due to insufficiencies in the cache implementation + #Cache.addFile(self) + + def access(self): + """ + This method shall be called every time a cache file is accessed. It + updates the access timestamp of the model that should be used by cache + implementations to determine which cache files can be removed. + """ + self.__model.access_timestamp = datetime.now() + + self.__model.save() + + def purge(self): + """ + Delete the cache file from the local file system and delete the + associate :class:`~.CacheFile` database record. Raises + :exc:`~.DataAccessError` if the file could not be deleted. + """ + path = self.__model.location.path + + if os.path.exists(path): + try: os.remove(path) - self._cached_objects.clear() - - else: - shutil.rmtree(self._cache_directory) - self._cached_objects.clear() + except Exception, e: + raise DataAccessError( + "Could not remove file '%s' from cache. Error message: '%s'" % ( + path, str(e) + ) + ) + + # remove the file from the cache; this is disabled at the + # moment due to insufficiencies in the cache implementation + #Cache.removeFile(self) + + self.__model.delete() + +#class CacheStartupHandler(object): +# def startup(self): +# Cache.synchronizeSize() +# +# def reset(self): +# Cache.synchronizeSize() +class CacheConfigReader(object): + """ + This is the configuration reader for the cache configuration. It should be + used by cache implementations. + + The cache can be configured by config file entries in the section + ``backends.cache``. There are three of them: - def contains(self, cache_path): - """ Check whether or not the path is contained in this cache. + * ``cache_dir``: if you want to use the cache you have to define this + setting; it tells under which directory tree the cache files shall be + stored. Note that if you change this setting, the cached files at the + old location will not be forgotten. + * ``max_size``: the maximum size of the cache in bytes; be sure to set + this to a value that exceeds maximum traffic within the given + retention time, otherwise you will get :exc:`~.CacheOverflow` errors + at runtime + * ``retention_time``: the minimum time cache files will be kept + expressed in hours. At your own risk you can set it to 0, but strange + things may occur then due to one thread deleting the data another one + needs. A minimum of 1 hour is recommended, the default is 168 + (a week). + """ + REGISTRY_CONF = { + "name": "Cache Config Reader", + "impl_id": "backends.cache.CacheConfigReader" + } + + def validate(self, config): """ - if cache_path in self._cached_objects: - return True - - return path.exists(self.relative_path(cache_path)) - - def __contains__(self, cache_path): - """ Alias for method `contains`. + Returns ``True``. """ - return self.contains(cache_path) - + return True - def __enter__(self): - """ Context manager protocol, for recursive use. Each time the a context - is entered, the internal level is raised by one. + def getCacheDir(self): """ - self._level += 1 - return self - - - def __exit__(self, etype=None, evalue=None, tb=None): - """ Exit of context manager protocol. Performs cache cleanup if - the level drops to zero. + Returns the ``cache_dir`` config file setting. """ - self._level -= 1 - if self._level == 0 and not self._managed: - self.cleanup() + return System.getConfig().getConfigValue("backends.cache", "cache_dir") + + def getMaxSize(self): + """ + Returns the ``max_size`` config file setting. + """ + return System.getConfig().getConfigValue("backends.cache", "max_size") + + def getRetentionTime(self): + """ + Returns the ``retention_time`` config file setting. + """ + return System.getConfig().getConfigValue("backends.cache", "retention_time") + +CacheConfigReaderImplementation = \ +ConfigReaderInterface.implement(CacheConfigReader) diff -Nru eoxserver-0.4.0beta2/eoxserver/backends/component.py eoxserver-0.3.2/eoxserver/backends/component.py --- eoxserver-0.4.0beta2/eoxserver/backends/component.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/backends/component.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,127 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -import itertools - -from eoxserver.core import env, Component, ExtensionPoint -from eoxserver.backends.interfaces import * - - -class BackendComponent(Component): - """ This :class:`Component ` provides - extension points and helpers to easily retrive Package and Storage - components by their type names. - """ - - file_storages = ExtensionPoint(FileStorageInterface) - connected_storages = ExtensionPoint(ConnectedStorageInterface) - packages = ExtensionPoint(PackageInterface) - - @property - def storages(self): - """ Helper to retrieve components for all storage interfaces. - """ - return itertools.chain(self.file_storages, self.connected_storages) - - def get_file_storage_component(self, storage_type): - """ Retrieve a component implementing the - :class:`eoxserver.backends.interfaces.FileStorageInterface` with the - desired ``storage_type``. - - :param storage_type: the desired storage type - :returns: the desired storage component or ``None`` - """ - - storage_type = storage_type.upper() - result_component = None - for storage_component in self.file_storages: - if storage_component.name.upper() == storage_type: - if result_component is not None: - raise Exception("Ambigouus storage component") - result_component = storage_component - - return result_component - - def get_connected_storage_component(self, storage_type): - """ Retrieve a component implementing the - :class:`eoxserver.backends.interfaces.ConnectedStorageInterface` with - the desired ``storage_type``. - - :param storage_type: the desired storage type - :returns: the desired storage component or ``None`` - """ - - storage_type = storage_type.upper() - result_component = None - for storage_component in self.connected_storages: - if storage_component.name.upper() == storage_type: - if result_component is not None: - raise Exception("Ambigouus storage component") - result_component = storage_component - - return result_component - - def get_storage_component(self, storage_type): - """ Retrieve a component implementing the - :class:`eoxserver.backends.interfaces.FileStorageInterface` or - :class:`eoxserver.backends.interfaces.ConnectedStorageInterface` with - the desired ``storage_type``. - - :param storage_type: the desired storage type - :returns: the desired storage component or ``None`` - """ - - file_storage = self.get_file_storage_component(storage_type) - connected_storage = self.get_connected_storage_component(storage_type) - - if file_storage is not None and connected_storage is not None: - raise Exception("Ambigouus storage component") - - return file_storage or connected_storage - - def get_package_component(self, format): - """ Retrieve a component implementing the - :class:`eoxserver.backends.interfaces.PackageInterface` with - the desired ``format``. - - :param format: the desired package format - :returns: the desired package component or ``None`` - """ - - format = format.upper() - result_component = None - for package_component in self.packages: - if package_component.name.upper() == format: - if result_component is not None: - raise Exception("Ambigouus package component") - result_component = package_component - - return result_component - - -BackendComponent(env) diff -Nru eoxserver-0.4.0beta2/eoxserver/backends/config.py eoxserver-0.3.2/eoxserver/backends/config.py --- eoxserver-0.4.0beta2/eoxserver/backends/config.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/backends/config.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,35 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -from eoxserver.core.decoders import config - - -class CacheConfigReader(config.Reader): - config.section("backends") - retention_time = config.Option() # TODO - directory = config.Option() diff -Nru eoxserver-0.4.0beta2/eoxserver/backends/exceptions.py eoxserver-0.3.2/eoxserver/backends/exceptions.py --- eoxserver-0.4.0beta2/eoxserver/backends/exceptions.py 1970-01-01 00:00:00.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/backends/exceptions.py 2013-12-10 14:57:00.000000000 +0000 @@ -0,0 +1,49 @@ +#------------------------------------------------------------------------------- +# $Id$ +# +# Project: EOxServer +# Authors: Stephan Krause +# Stephan Meissl +# +#------------------------------------------------------------------------------- +# Copyright (C) 2011 EOX IT Services GmbH +# +# 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 of this Software or works derived from this 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. +#------------------------------------------------------------------------------- + +""" +This module contains exception definitions for the storage backends. +""" + +from eoxserver.core.exceptions import EOxSException + +class DataAccessError(EOxSException): + """ + This exception shall be raised if any data access error occurs. + """ + + pass + +class CacheOverflow(EOxSException): + """ + This exception shall be raised it the cache cannot accomodate for new + incoming data despite attempts to free space. + """ + + pass diff -Nru eoxserver-0.4.0beta2/eoxserver/backends/factories.py eoxserver-0.3.2/eoxserver/backends/factories.py --- eoxserver-0.4.0beta2/eoxserver/backends/factories.py 1970-01-01 00:00:00.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/backends/factories.py 2013-12-10 14:57:00.000000000 +0000 @@ -0,0 +1,62 @@ +#------------------------------------------------------------------------------- +# $Id$ +# +# Project: EOxServer +# Authors: Stephan Krause +# Stephan Meissl +# +#------------------------------------------------------------------------------- +# Copyright (C) 2011 EOX IT Services GmbH +# +# 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 of this Software or works derived from this 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. +#------------------------------------------------------------------------------- + +""" +This module provides factories for the Data Access Layer. +""" + +from eoxserver.core.system import System +from eoxserver.core.records import ( + RecordWrapperFactoryInterface, RecordWrapperFactory +) +from eoxserver.core.exceptions import InternalError +from eoxserver.backends.models import Location + +class LocationFactory(RecordWrapperFactory): + """ + This is a factory for location wrappers. It inherits from + :class:`~.RecordWrapperFactory`. + """ + REGISTRY_CONF = { + "name": "LocationFactory", + "impl_id": "backends.factories.LocationFactory", + "binding_method": "direct" + } + + def _get_record_by_pk(self, pk): + return Location.objects.get(pk=pk) + + def _get_record_wrapper(self, record): + wrapper = self.impls[record.location_type]() + wrapper.setRecord(record) + + return wrapper + +LocationFactoryImplementation = \ +RecordWrapperFactoryInterface.implement(LocationFactory) diff -Nru eoxserver-0.4.0beta2/eoxserver/backends/ftp.py eoxserver-0.3.2/eoxserver/backends/ftp.py --- eoxserver-0.4.0beta2/eoxserver/backends/ftp.py 1970-01-01 00:00:00.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/backends/ftp.py 2013-12-10 14:57:00.000000000 +0000 @@ -0,0 +1,472 @@ +#------------------------------------------------------------------------------- +# $Id$ +# +# Project: EOxServer +# Authors: Stephan Krause +# Stephan Meissl +# +#------------------------------------------------------------------------------- +# Copyright (C) 2011 EOX IT Services GmbH +# +# 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 of this Software or works derived from this 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. +#------------------------------------------------------------------------------- + +""" +This module provides the implementation of the FTP remote file backend. +""" + +import os.path +from fnmatch import fnmatch +from ftplib import ( + FTP, error_perm as ftplib_error_perm, + error_temp as ftplib_error_temp +) +import logging + +from eoxserver.core.system import System +from eoxserver.core.exceptions import InternalError +from eoxserver.backends.interfaces import ( + StorageInterface, RemotePathInterface +) +from eoxserver.backends.models import FTPStorage as FTPStorageRecord +from eoxserver.backends.models import RemotePath +from eoxserver.backends.base import LocationWrapper +from eoxserver.backends.exceptions import DataAccessError + +logger = logging.getLogger(__name__) + +class FTPStorage(object): + """ + This is an implementation of the :class:`~.StorageInterface` for + accessing files on a remote FTP server. + + Note that internally, it creates a persistent connection that may + be used for multiple requests on the same location or requests for + multiple locations on the same server. DO NOT try to connect to + different servers using the same :class:`FTPStorage` instance + however, this will cause trouble and most definitely not work! + """ + + REGISTRY_CONF = { + "name": "FTP Storage Interface", + "impl_id": "backends.ftp.FTPStorage", + "registry_values": { + "backends.interfaces.storage_type": "ftp" + } + } + + def __init__(self): + self.ftp = None + + def __del__(self): + if self.ftp: + try: + self.ftp.quit() + except: + self.ftp.close() + + def getType(self): + """ + Returns ``"ftp"``. + """ + + return "ftp" + + def getStorageCapabilities(self): + """ + Returns the storage capabilities, i.e. the names of the optional methods + implemented by the storage. Currently + ``("getSize", "getLocalCopy", "detect")``. + """ + + return ("getSize", "getLocalCopy", "detect") + + def getSize(self, location): + """ + Returns the size of the object at ``location``. Note that not all + FTP implementations are able to respond to this call. In that case + ``None`` will be returned. + """ + + self._connect(location) + + return self.ftp.size(location.getPath()) + + def getLocalCopy(self, location, target): + """ + Copies the file at the remote ``location`` to the + ``target`` path on the local file system. The parameter + ``location`` is expected to be an instance of + :class:`RemotePathWrapper`, ``target`` is expected to be a + string denoting the destination path. + + The method raises :exc:`~.InternalError` in case the location is + not of appropriate type and :exc:`~.DataAccessError` in case + an error occurs while copying the resources. + """ + + # check location type + if location.getType() != "ftp": + raise InternalError( + "Cannot open '%s' location with FTP storage." %\ + location.getType() + ) + + # determine the local path + if os.path.exists(target) and os.path.isdir(target): + dest_path = os.path.join( + target, os.path.basename(location.getPath()) + ) + else: + dest_path = target + + # create FTP connection + self._connect(location) + + # retrieve the given file + cmd = "RETR %s" % location.getPath() + + try: + local_file = open(dest_path, 'wb') + except Exception, e: + raise DataAccessError( + "Could not open destination file '%s'. Error message: '%s'" % ( + dest_path, str(e) + ) + ) + + logger.info("Get remote file '%s'" % location.getPath()) + logger.info("Write to local path '%s'" % dest_path) + + try: + self.ftp.retrbinary(cmd, local_file.write) + except Exception, e: + raise DataAccessError( + "Retrieving data for file '%s' on '%s' via FTP failed. Error message: '%s'" % ( + location.getPath(), location.getHost(), str(e) + ) + ) + + # we have successfully written the file; clean up + local_file.close() + + # return the file location + return System.getRegistry().bind( + "backends.factories.LocationFactory" + ).create( + type="local", path=dest_path + ) + + def detect(self, location, search_pattern=None): + """ + Recursively detects files in a directory tree and returns their + locations. This will raise :exc:`~.DataAccessError` if the object at + ``location`` is not a directory. + """ + self._connect(location) + + paths = self._recursive_nlst(location.getPath(), search_pattern) + + factory = System.getRegistry().bind( + "backends.factories.LocationFactory" + ) + + return [ + factory.create( + type="ftp", + host = location.getHost(), + port = location.getPort(), + user = location.getUser(), + passwd = location.getPassword(), + path = path + ) + + for path in paths + ] + + def exists(self, location): + """ + Checks the existance of a certain location within the storage. + Returns `True` if the location exists and `False` if not or the + location is not accessible. + """ + try: + if self.ftp.nlst(location.getPath()): + return True + else: + return False + except ftplib_error_temp: + return False + + + def _recursive_nlst(self, search_path, search_pattern=None): + # this does a recursive search of an FTP directory + # + # raises :exc:`DataAccessError` if the NLST FTP command fails + + try: + found_paths = self.ftp.nlst(search_path) + except Exception, e: + raise DataAccessError( + "Could not list remote directory '%s'. Error message: '%s'" % ( + search_path, str(e) + ) + ) + + file_paths = [] + + for path in found_paths: + # try to change to the directory called name; this will fail + # if the object called name is not a directory. + # Unfortunately this is the most straightforward way of + # checking whether the object called name is a directory. + # + # if it is not a directory, check if it matches the search + # pattern (if any) and append to the list of found paths. + # + # if another error occurred, raise :exc:`~.DataAccessError` + try: + cur_dir = self.ftp.pwd() + self.ftp.cwd(path) + self.ftp.cwd(cur_dir) + + file_paths.extend( + self._recursive_nlst(path, search_pattern) + ) + except ftplib_error_perm: + if (search_pattern and fnmatch(os.path.basename(path), search_pattern)) \ + or not search_pattern: + file_paths.append(path) + except Exception, e: + raise DataAccessError( + "Could not search directory tree '%s'. Error message: '%s'" % ( + search_path, str(e) + ) + ) + + return file_paths + + def _connect(self, location): + # This method establishes an FTP connection with the host the + # location is situated on. + # + # see also the comment in the class's docstring + + if self.ftp: + return self.ftp + else: + self.ftp = FTP() + + try: + if location.getPort() is None: + self.ftp.connect(location.getHost()) + else: + self.ftp.connect(location.getHost(), location.getPort()) + + if location.getUser() is None: + self.ftp.login() + else: + if location.getPassword() is None: + self.ftp.login(location.getUser()) + else: + self.ftp.login(location.getUser(), location.getPassword()) + + self.ftp.sendcmd('TYPE I') # set to binary mode; needed for getSize + self.ftp.set_pasv(False) # turn passive mode off; there seem to be problems with it + + except Exception, e: + raise DataAccessError( + "Could not connect to FTP host '%s'. Error message: '%s'" % ( + location.getHost(), str(e) + ) + ) + +FTPStorageImplementation = StorageInterface.implement(FTPStorage) + +class RemotePathWrapper(LocationWrapper): + """ + This is a wrapper class for remote paths. It inherits from + :class:`~.LocationWrapper`. + + .. method:: setAttrs(**kwargs) + + This method is called to initialize the wrapper. The following attribute + keyword arguments are accepted: + + * ``host`` (required): the FTP host name + * ``port`` (optional): the FTP port number + * ``user`` (optional): the user name to be used for login + * ``passwd`` (optional): the password to be used for login + * ``path`` (required): the path to the location on the remote server + + """ + + REGISTRY_CONF = { + "name": "Remote Path Wrapper", + "impl_id": "backends.ftp.RemotePathWrapper", + "factory_ids": ( + "backends.factories.LocationFactory", + ) + } + + def __init__(self): + super(RemotePathWrapper, self).__init__() + + self.host = None + self.port = None + self.user = None + self.passwd = None + self.path = None + + def getType(self): + """ + Returns ``"ftp"``. + """ + return "ftp" + + def getStorageType(self): + """ + Returns ``"ftp"``. + """ + return "ftp" + + def getHost(self): + """ + Returns the FTP host name. + """ + if self.record: + return self.record.storage.host + else: + return self.host + + def getPort(self): + """ + Returns the FTP port number or ``None`` if it has not been defined. + """ + if self.record: + return self.record.storage.port + else: + return self.port + + def getUser(self): + """ + Returns the user name to be used for login to the remote server or + ``None`` if it has not been defined. + """ + if self.record: + return self.record.storage.user + else: + return self.user + + def getPassword(self): + """ + Returns the password to be used for login to the remote server or + ``None`` if it has not been defined. + """ + if self.record: + return self.record.storage.passwd + else: + return self.passwd + + def getPath(self): + """ + Returns the path on the remote server. + """ + if self.record: + return self.record.path + else: + return self.path + + + def _bind_to_storage(self): + return System.getRegistry().bind("backends.ftp.FTPStorage") + + + def _validate_record(self, record): + if record.location_type != "ftp": + raise InternalError( + "Cannot assign a '%s' type location record to remote path location." %\ + record.location_type + ) + + def _set_record(self, record): + if isinstance(record, RemotePath): + self.record = record + else: + self.record = record.remotepath + + def _validate_attrs(self, **kwargs): + if "path" not in kwargs or "host" not in kwargs: + raise InternalError( + "The 'path' and 'host' keyword arguments must be submitted to create a remote path instance." + ) + + def _set_attrs(self, **kwargs): + self.path = kwargs["path"] + self.host = kwargs["host"] + + self.port = kwargs.get("port") + self.user = kwargs.get("user") + self.passwd = kwargs.get("passwd") + + def _fetch_unique_record(self): + # no uniqueness constraints apply + + return None + + def _get_query(self, fields=None): + query = {} + + if fields is None or "path" in fields: + query["path"] = self.getPath() + + if fields is None or "host" in fields: + query["storage__host"] = self.getHost() + + if fields is None or "port" in fields: + query["storage__port"] = self.getPort() + + if fields is None or "user" in fields: + query["storage__user"] = self.getUser() + + if fields is None or "passwd" in fields: + query["storage__passwd"] = self.getPassword() + + return query + + def _get_query_set(self, query): + return RemotePath.objects.filter(**query) + + def _create_record(self): + storage_record = FTPStorageRecord.objects.get_or_create( + storage_type = FTPStorageRecord.STORAGE_TYPE, + host = self.host, + port = self.port, + user = self.user, + passwd = self.passwd + )[0] + + self.record = RemotePath.objects.create( + location_type = RemotePath.LOCATION_TYPE, + storage = storage_record, + path = self.path + ) + +RemotePathWrapperImplementation = \ +RemotePathInterface.implement(RemotePathWrapper) diff -Nru eoxserver-0.4.0beta2/eoxserver/backends/interfaces.py eoxserver-0.3.2/eoxserver/backends/interfaces.py --- eoxserver-0.4.0beta2/eoxserver/backends/interfaces.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/backends/interfaces.py 2013-12-10 14:57:00.000000000 +0000 @@ -1,9 +1,9 @@ #------------------------------------------------------------------------------- +# $Id$ # # Project: EOxServer # Authors: Stephan Krause # Stephan Meissl -# Fabian Schindler # #------------------------------------------------------------------------------- # Copyright (C) 2011 EOX IT Services GmbH @@ -11,8 +11,8 @@ # 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 +# 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 @@ -27,88 +27,319 @@ # THE SOFTWARE. #------------------------------------------------------------------------------- +""" +This module defines interfaces for the Data Access Layer. +""" + +from eoxserver.core.interfaces import * +from eoxserver.core.registry import RegisteredInterface +from eoxserver.core.records import RecordWrapperInterface -class AbstractStorageInterface(object): - @property - def name(self): - "Name of the storage implementation." - - def validate(self, url): - """ Validates the given storage locator and raises a - :exc:`django.core.exceptions.ValidationError` if errors occurred. - """ - - -class FileStorageInterface(AbstractStorageInterface): - """ Interface for storages that provide access to files and allow the - retrieval of those. - """ - - def retrieve(self, url, location, path): - """ Retrieve a remote file from the storage specified by the given `url` - and location and store it to the given `path`. Storages that don't - need to actually retrieve and store files, just need to return a - path to a local file instead of storing it under `path`. - - :param url: the URL denoting the storage itself - :param location: the location of the file to retrieve on the storage - :param path: a local path where the file should be saved under; - this is used as a *hint* - :returns: the actual path where the file was stored; in some cases - this can be different than the passed ``path`` - """ - - def list_files(self, url, location): - """ Return a list of retrievable files available on the storage located - at the specified URL and given location. - - :param url: the URL denoting the storage itself - :param location: a template to find items on the storage - :returns: an iterable of the storage contents under the specified - ``location`` - """ - - -class ConnectedStorageInterface(AbstractStorageInterface): - """ Interface for storages that do not store "files" but provide access to - data in a different fashion. - """ - - def connect(self, url, location): - """ Return a connection string for a remote dataset residing on a - storage specified by the given `url` and `location`. - - :param url: the URL denoting the storage itself - :param location: the location of the file to retrieve on the storage - :returns: a connection string to open the stream to actually - retrieve data - """ - - -class PackageInterface(object): - - @property - def name(self): - "Name of the package implementation." - - def extract(self, package_filename, location, path): - """ Extract a file specified by the ``location`` from the package to the - given ``path`` specification. - - :param package_filename: the local filename of the package - :param location: a location *within* the package to be extracted - :param path: a local path where the file should be saved under; - this is used as a *hint* - :returns: the actual path where the file was stored; in some cases - this can be different than the passed ``path`` - """ - - def list_contents(self, package_filename, location): - """ Return a list of item locations under the specified location in the - given package. - - :param package_filename: the local filename of the package - :param location: a template to find items within the package - :returns: an iterable of the package contents under the specified - ``location`` - """ +class StorageInterface(RegisteredInterface): + """ + This is the interface for any kind of storage (local file system, + remote repositories, databases, ...). It defines three methods: + + .. method:: getType + + This method shall return a string designating the type of the + storage wrapped by the implementation. Current choices are: + + * ``local`` + * ``ftp`` + * ``rasdaman`` + + Additional modules may add more choices in the future. + + .. method:: getStorageCapabilities + + This method shall return which of the optional methods a storage + implements. + + The following methods are optional in the sense that they are not needed + to be implemented in a meaningful way, either because the storage type + does not support it, or because it is not needed. Even in this case, they + need to be present and should raise :exc:`~.InternalError`. + + .. method:: getSize(location) + + This method shall return the size in bytes of the object at ``location`` + or ``None`` if it cannot be retrieved (e.g. for some FTP server + implementations). + + .. method:: getLocalCopy(location, target) + + This method shall make a local copy of the object at + ``location`` at the path ``target``. + + The method shall return the location of the local copy, + i.e. an implementation of a descendant of + :class:`LocationInterface`. + + In case the type of ``location`` cannot be handled by the + specific storage :exc:`~.InternalError` shall be raised. In case + the copying of resources fails :exc:`~.DataAccessError` shall + be raised. + + .. method:: detect(location, search_pattern=None) + + This method shall return a list of object locations found at the + given ``location`` (which may designate some kind of collection, + like a directory) that match the given ``search_pattern``. If + ``search_pattern`` is omitted any object location shall be + returned. + """ + + REGISTRY_CONF = { + "name": "Storage Interface", + "intf_id": "backends.interfaces.Storage", + "binding_method": "kvp", + "registry_keys": ( + "backends.interfaces.storage_type", + ) + } + + getType = Method( + returns = StringArg("@return") + ) + + getStorageCapabilities = Method( + returns = ListArg("@return") + ) + + getLocalCopy = Method( + ObjectArg("location"), + StringArg("target"), + returns = ObjectArg("@return") + ) + + getSize = Method( + ObjectArg("location"), + returns = IntArg("@return", default=None) + ) + + + detect = Method( + ObjectArg("location"), + StringArg("search_pattern", default=None), + returns = ListArg("@return") + ) + +class LocationInterface(RecordWrapperInterface): + """ + This is the base interface for locations where to find data, + metadata or resources in general. It is not intended to be + instantiated directly, but rather through its descendant interfaces. + It inherits from :class:`~.RecordWrapperInterface`. + + .. method:: getStorageCapabilities + + This method shall return the capabilities of the underlying storage + implementation. See :meth:`StorageInterface.getStorageCapabilities`. + + .. method:: getSize + + This method shall return the size of the object at the location or + ``None`` if it cannot be retrieved. Note that :exc:`~.InternalError` + will be raised if this operation is not supported by the underlying + storage implementation. See :meth:`StorageInterface.getSize`. + + .. method:: getLocalCopy(target) + + This method shall retrieve a local copy of the object at the location + and save it to ``target``. This parameter may be a path to a file or + directory. The method shall return the location of the local copy, i.e. + an implementation of :class:`LocalPathInterface`. + + .. method:: detect(search_pattern=None): + + This method shall return a list of locations of objects matching the + given ``search_pattern`` to be found under the location, which is + expected to be a tree-like object, most commonly a directory. If + ``search_pattern`` is omitted all the locations shall be returned. + """ + + REGISTRY_CONF = { + "name": "Location Interface", + "intf_id": "backends.interfaces.Location", + "binding_method": "factory" + } + + getStorageCapabilities = Method( + returns = ListArg("@return") + ) + + getSize = Method( + returns = IntArg("@return", default=None) + ) + + getLocalCopy = Method( + StringArg("target"), + returns = ObjectArg("@return") + ) + + detect = Method( + StringArg("search_pattern", default=None), + returns = ListArg("@return") + ) + + exists = Method( + returns = BoolArg("@return") + ) + +class LocalPathInterface(LocationInterface): + """ + This is the interface for locations on the local file system. It + inherits from :class:`LocationInterface`. + + .. method:: getPath + + This method shall return the path to the resource on the local + file system. + + .. method:: open + + This method shall attempt to open the file at this location and return + a :class:`file` object. It accepts one optional parameter ``mode`` + which is passed on to the builtin :func:`open` command (defaults to + ``'r'``). The method shall raise :exc:`~.DataAccessError` if the file + cannot be opened, or if the object at the location is not a file. + """ + + REGISTRY_CONF = { + "name": "Local Path Interface", + "intf_id": "backends.interfaces.LocalPath", + "binding_method": "factory" + } + + getPath = Method( + returns = StringArg("@return") + ) + + open = Method( + StringArg("mode", default='r'), + returns = ObjectArg("@return", arg_class=file) + ) + +class RemotePathInterface(LocationInterface): + """ + This is the interface for data and metadata files stored on a + remote server. It inherits from :class:`LocationInterface`. + + .. method:: getStorageType + + This method shall return the type of the remote storage, e.g. + ``"ftp"``. + + .. method:: getHost + + This method shall return the host name of the remote storage. + + .. method:: getPort + + This method shall return the port number of the remote storage, + or ``None`` if it is not defined. + + .. method:: getUser + + This method shall return the user name to be used for access to + the remote storage, or ``None`` if it is not defined. + + .. method:: getPasswd + + This method shall return the password to be used for access to + the remote storage, or ``None`` if it is not defined. + + .. method:: getPath + + This method shall return the path to the resource on the remote + storage. + """ + + REGISTRY_CONF = { + "name": "Remote Path Interface", + "intf_id": "backends.interfaces.RemotePath", + "binding_method": "factory" + } + + getStorageType = Method( + returns = StringArg("@return") + ) + + getHost = Method( + returns = StringArg("@return") + ) + + getPort = Method( + returns = IntArg("@return", default=None) + ) + + getUser = Method( + returns = StringArg("@return", default=None) + ) + + getPassword = Method( + returns = StringArg("@return", default=None) + ) + + getPath = Method( + returns = StringArg("@return") + ) + +class DatabaseLocationInterface(LocationInterface): + """ + This is the interface for raster data stored in a database. It + inherits from :class:`LocationInterface` and adds some methods. + + .. method:: getHost + + This method shall return the hostname of the database manager. + + .. method:: getPort + + This method shall return the number of the port where the + database manager listens for connections, or ``None`` if the + port is undefined. + + .. method:: getDBName + + This method shall return the database name, or ``None`` if it + is undefined. + + .. method:: getUser + + This method shall return the user name to be used for opening + database connections, or ``None`` if it is undefined. + + .. method:: getPassword + + This method shall return the password to be used for opening + database connections, or ``None`` if it is undefined. + + """ + + REGISTRY_CONF = { + "name": "Database Package Interface", + "intf_id": "backends.interfaces.DatabaseLocation", + "binding_method": "factory" + } + + getHost = Method( + returns = StringArg("@return") + ) + + getPort = Method( + returns = IntArg("@return") + ) + + getDBName = Method( + returns = StringArg("@return") + ) + + getUser = Method( + returns = StringArg("@return") + ) + + getPassword = Method( + returns = StringArg("@return") + ) diff -Nru eoxserver-0.4.0beta2/eoxserver/backends/local.py eoxserver-0.3.2/eoxserver/backends/local.py --- eoxserver-0.4.0beta2/eoxserver/backends/local.py 1970-01-01 00:00:00.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/backends/local.py 2013-12-10 14:57:00.000000000 +0000 @@ -0,0 +1,270 @@ +#------------------------------------------------------------------------------- +# $Id$ +# +# Project: EOxServer +# Authors: Stephan Krause +# Stephan Meissl +# +#------------------------------------------------------------------------------- +# Copyright (C) 2011 EOX IT Services GmbH +# +# 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 of this Software or works derived from this 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. +#------------------------------------------------------------------------------- + +""" +This module implements the local storage backend for EOxServer. +""" + +import os.path +import shutil + +from eoxserver.core.system import System +from eoxserver.core.exceptions import InternalError +from eoxserver.core.util.filetools import findFiles +from eoxserver.backends.interfaces import ( + StorageInterface, LocalPathInterface +) +from eoxserver.backends.models import LocalPath +from eoxserver.backends.base import LocationWrapper +from eoxserver.backends.exceptions import DataAccessError + +class LocalStorage(object): + """ + This is a wrapper for the storage on the local file system. + """ + + REGISTRY_CONF = { + "name": "Local Storage", + "impl_id": "backends.local.LocalStorage", + "registry_values": { + "backends.interfaces.storage_type": "local" + } + } + + def getType(self): + """ + Returns ``"local"``. + """ + return "local" + + def getStorageCapabilities(self): + """ + Returns the names of the optional methods implemented by the storage. + Currently ``("getSize", "getLocalCopy", "detect")``. + """ + return ("getSize", "getLocalCopy", "detect") + + def getSize(self, location): + """ + Returns the size (in bytes) of the object at the location or ``None`` + if it cannot be retrieved. + """ + try: + return os.path.getsize(location.getPath()) + except: + return None + + def getLocalCopy(self, location, target): + """ + Makes a local copy of the file at ``location`` at the path ``target`` + and returns the location of the copy (i.e. a :class:`LocalPathWrapper` + instance). Raises :exc:`~.InternalError` if the location does not refer + to an object on the local file system or :exc:`~.DataAccessError` if + copying fails. + """ + if location.getType() != "local": + raise InternalError( + "Location type '%s' not supported by local storage." %\ + location.getType() + ) + + try: + shutil.copy(location.getPath(), target) + except: + raise DataAccessError( + "Could not copy object at '%s' to '%s'" % ( + location.getPath(), + target + ) + ) + + if os.path.isdir(target): + return System.getRegistry().bind("backends.factories.LocationFactory").create( + type = "local", + path = os.path.join( + target, os.path.basename(location.getPath()) + ) + ) + else: + return System.getRegistry().bind("backends.factories.LocationFactory").create( + type = "local", + path = target + ) + + def detect(self, location, search_pattern=None): + """ + Recursively detects files whose name matches ``search_pattern`` in the + directory tree under ``location`` and returns their locations as a list + of :class:`LocalPathWrapper` instances. If ``search_pattern`` is omitted + all files found are returned. + """ + if location.getType() != "local": + raise InternalError( + "Location type '%s' not supported by local storage." %\ + location.getType() + ) + + if search_pattern is None: + _pattern = "*" + else: + _pattern = search_pattern + + paths = findFiles(location.getPath(), _pattern) + + factory = System.getRegistry().bind( + "backends.factories.LocationFactory" + ) + + return [ + factory.create(type="local", path=path) for path in paths + ] + + def exists(self, location): + return os.path.exists(location.getPath()) + +LocalStorageImplementation = StorageInterface.implement(LocalStorage) + +class LocalPathWrapper(LocationWrapper): + """ + This a wrapper for locations on the local file system. It inherits from + :class:`~.LocationWrapper`. + + .. method:: setAttrs(**kwargs) + + The ``path`` keyword argument is mandatory; it is expected to contain + the path to the location on the local file system. + """ + + REGISTRY_CONF = { + "name": "Local Path Wrapper", + "impl_id": "backends.local.LocalPathWrapper", + "factory_ids": ( + "backends.factories.LocationFactory", + ) + } + + def __init__(self): + super(LocalPathWrapper, self).__init__() + + self.path = None + + def getType(self): + """ + Returns ``"local"``. + """ + return "local" + + def getPath(self): + """ + Returns the path to the location on the local file system. + """ + if self.record: + return self.record.path + else: + return self.path + + def open(self, mode='r'): + """ + Opens the file at the location on the local file system and return + the :class:`file` object. The ``mode`` flag is passed to the builtin + :func:`open` function and defaults to ``'r'`` read only. Raises + :exc:`~.DataAccessError` if the object at the location is not a file + or cannot be opened for some other reason. + """ + path = self.getPath() + + if os.path.exists(path): + if os.path.isfile(path): + try: + return open(path, 'r') + except Exception, e: + raise DataAccessError( + "Could not open file '%s'. Error message: '%s'" % ( + path, str(e) + ) + ) + else: + raise DataAccessError( + "Object at path '%s' is not a file and cannot be opened." %\ + path + ) + else: + raise DataAccessError( + "Could not open file '%s'. Path does not exist." % path + ) + + def _bind_to_storage(self): + return System.getRegistry().bind("backends.local.LocalStorage") + + def _validate_record(self, record): + if record.location_type != "local": + raise InternalError( + "Cannot assign a '%s' record to a local path wrapper." %\ + record.location_type + ) + + def _set_record(self, record): + if isinstance(record, LocalPath): + self.record = record + else: + self.record = record.localpath + + def _validate_attrs(self, **kwargs): + if "path" not in kwargs: + raise InternalError( + "The 'path' keyword argument is required to initialize a local path wrapper." + ) + + def _set_attrs(self, **kwargs): + self.path = kwargs["path"] + + def _fetch_unique_record(self): + # no uniqueness constraints apply + + return None + + def _get_query(self, fields=None): + query = {} + + if fields is None or "path" in fields: + query["path"] = self.getPath() + + return query + + def _get_query_set(self, query): + return LocalPath.objects.filter(**query) + + def _create_record(self): + self.record = LocalPath.objects.create( + location_type = LocalPath.LOCATION_TYPE, + path = self.path + ) + +LocalPathImplementation = \ +LocalPathInterface.implement(LocalPathWrapper) diff -Nru eoxserver-0.4.0beta2/eoxserver/backends/middleware.py eoxserver-0.3.2/eoxserver/backends/middleware.py --- eoxserver-0.4.0beta2/eoxserver/backends/middleware.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/backends/middleware.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,56 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -import logging - -from eoxserver.backends.cache import setup_cache_session, shutdown_cache_session - - -logger = logging.getLogger(__name__) - - -class BackendsCacheMiddleware(object): - """ A `Django Request Middleware - `_ to manage - cache setup and teardown when a request is beeing processed. - """ - - def process_request(self, request): - setup_cache_session() - - def process_response(self, request, response): - shutdown_cache_session() - return response - - def process_template_response(self, request, response): - shutdown_cache_session() - return response - - def process_exception(self, request, exception): - shutdown_cache_session() - return None diff -Nru eoxserver-0.4.0beta2/eoxserver/backends/migrations/0001_initial.py eoxserver-0.3.2/eoxserver/backends/migrations/0001_initial.py --- eoxserver-0.4.0beta2/eoxserver/backends/migrations/0001_initial.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/backends/migrations/0001_initial.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,93 +0,0 @@ -# -*- coding: utf-8 -*- -from south.utils import datetime_utils as datetime -from south.db import db -from south.v2 import SchemaMigration -from django.db import models - - -class Migration(SchemaMigration): - - def forwards(self, orm): - # Adding model 'Storage' - db.create_table(u'backends_storage', ( - (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('url', self.gf('django.db.models.fields.CharField')(max_length=1024)), - ('storage_type', self.gf('django.db.models.fields.CharField')(max_length=32)), - )) - db.send_create_signal(u'backends', ['Storage']) - - # Adding model 'Package' - db.create_table(u'backends_package', ( - (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('location', self.gf('django.db.models.fields.CharField')(max_length=1024)), - ('format', self.gf('django.db.models.fields.CharField')(max_length=64, null=True, blank=True)), - ('storage', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['backends.Storage'], null=True, blank=True)), - ('package', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='packages', null=True, to=orm['backends.Package'])), - )) - db.send_create_signal(u'backends', ['Package']) - - # Adding model 'Dataset' - db.create_table(u'backends_dataset', ( - (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - )) - db.send_create_signal(u'backends', ['Dataset']) - - # Adding model 'DataItem' - db.create_table(u'backends_dataitem', ( - (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('location', self.gf('django.db.models.fields.CharField')(max_length=1024)), - ('format', self.gf('django.db.models.fields.CharField')(max_length=64, null=True, blank=True)), - ('storage', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['backends.Storage'], null=True, blank=True)), - ('dataset', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='data_items', null=True, to=orm['backends.Dataset'])), - ('package', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='data_items', null=True, to=orm['backends.Package'])), - ('semantic', self.gf('django.db.models.fields.CharField')(max_length=64)), - )) - db.send_create_signal(u'backends', ['DataItem']) - - - def backwards(self, orm): - # Deleting model 'Storage' - db.delete_table(u'backends_storage') - - # Deleting model 'Package' - db.delete_table(u'backends_package') - - # Deleting model 'Dataset' - db.delete_table(u'backends_dataset') - - # Deleting model 'DataItem' - db.delete_table(u'backends_dataitem') - - - models = { - u'backends.dataitem': { - 'Meta': {'object_name': 'DataItem'}, - 'dataset': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'data_items'", 'null': 'True', 'to': u"orm['backends.Dataset']"}), - 'format': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'location': ('django.db.models.fields.CharField', [], {'max_length': '1024'}), - 'package': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'data_items'", 'null': 'True', 'to': u"orm['backends.Package']"}), - 'semantic': ('django.db.models.fields.CharField', [], {'max_length': '64'}), - 'storage': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['backends.Storage']", 'null': 'True', 'blank': 'True'}) - }, - u'backends.dataset': { - 'Meta': {'object_name': 'Dataset'}, - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) - }, - u'backends.package': { - 'Meta': {'object_name': 'Package'}, - 'format': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'location': ('django.db.models.fields.CharField', [], {'max_length': '1024'}), - 'package': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'packages'", 'null': 'True', 'to': u"orm['backends.Package']"}), - 'storage': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['backends.Storage']", 'null': 'True', 'blank': 'True'}) - }, - u'backends.storage': { - 'Meta': {'object_name': 'Storage'}, - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'storage_type': ('django.db.models.fields.CharField', [], {'max_length': '32'}), - 'url': ('django.db.models.fields.CharField', [], {'max_length': '1024'}) - } - } - - complete_apps = ['backends'] \ No newline at end of file diff -Nru eoxserver-0.4.0beta2/eoxserver/backends/models.py eoxserver-0.3.2/eoxserver/backends/models.py --- eoxserver-0.4.0beta2/eoxserver/backends/models.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/backends/models.py 2013-12-10 14:57:00.000000000 +0000 @@ -1,9 +1,9 @@ #------------------------------------------------------------------------------- +# $Id$ # # Project: EOxServer -# Authors: Fabian Schindler +# Authors: Stephan Krause # Stephan Meissl -# Stephan Krause # #------------------------------------------------------------------------------- # Copyright (C) 2011 EOX IT Services GmbH @@ -11,8 +11,8 @@ # 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 +# 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 @@ -27,62 +27,138 @@ # THE SOFTWARE. #------------------------------------------------------------------------------- -from django.core.exceptions import ValidationError from django.db import models - +from eoxserver.backends.validators import validate_path class Storage(models.Model): - """ Model to symbolize storages that provide file or other types of access - to data items and packages. """ - url = models.CharField(max_length=1024) - storage_type = models.CharField(max_length=32) - - def __unicode__(self): - return "%s: %s" % (self.storage_type, self.url) - - -class BaseLocation(models.Model): - """ Abstract base type for everything that describes a locateable object. + This class describes the storage facility a collection of data is + stored on. Fields: + + * ``storage_type``: a string denoting the storage type + * ``name``: a string denoting the name of the storage """ - location = models.CharField(max_length=1024) - format = models.CharField(max_length=64, null=True, blank=True) - - storage = models.ForeignKey(Storage, null=True, blank=True) - package = None # placeholder + storage_type = models.CharField(max_length=32, editable=False) + name = models.CharField(max_length=256) + + def save(self, *args, **kwargs): + self.storage_type = self.STORAGE_TYPE + super(Storage, self).save(*args, **kwargs) - def clean(self): - if self.storage is not None and self.package is not None: - raise ValidationError( - "Only one of 'package' and 'storage' can be set." - ) - - class Meta: - abstract = True +class FTPStorage(Storage): + """ + This class describes an FTP repository. It inherits from + :class:`Storage`. Additional fields: + + * ``host``: the host name + * ``port`` (optional): the port number + * ``user`` (optional): the user name to use + * ``passwd`` (optional): the password to use + """ + + STORAGE_TYPE = "ftp" + + host = models.CharField(max_length=1024) + port = models.IntegerField(null=True, blank=True) + user = models.CharField(max_length=1024, null=True, blank=True) + passwd = models.CharField(max_length=128, null=True, blank=True) + +class RasdamanStorage(Storage): + """ + This class describes a rasdaman database access. It inherits from + :class:`Storage`. Additional fields: + + * ``host``: the host name + * ``port`` (optional): the port number + * ``user`` (optional): the user name to use + * ``passwd`` (optional): the password to use + """ + + STORAGE_TYPE = "rasdaman" + + host = models.CharField(max_length=1024) + port = models.IntegerField(null=True, blank=True) + user = models.CharField(max_length=1024, null=True, blank=True) + passwd = models.CharField(max_length=128, null=True, blank=True) + db_name = models.CharField(max_length=128, null=True, blank=True) +class Location(models.Model): + """ + :class:`Location` is the base class for describing the physical or + logical location of a (general) resource relative to some storage. + Fields: + + * ``location_type``: a string denoting the type of location + """ + location_type = models.CharField(max_length=32, editable=False) + def __unicode__(self): - if self.format: - return "%s (%s)" % (self.location, self.format) - return self.location - - -class Package(BaseLocation): - """ Model for Packages. Packages are files that contain multiple files or - provide access to multiple data items. + if self.location_type == "local": + return self.localpath.path + elif self.location_type == "rasdaman": + return "rasdaman:%s:%s" % (self.rasdamanlocation.collection, self.rasdamanlocation.oid) + elif self.location_type == "ftp": + return "ftp://%s/%s" % (self.remotepath.storage.host, self.remotepath.path) + else: + return "Unknown location type" + + def save(self, *args, **kwargs): + self.location_type = self.LOCATION_TYPE + super(Location, self).save(*args, **kwargs) + +class LocalPath(Location): """ - package = models.ForeignKey("self", related_name="packages", null=True, blank=True) - - -class Dataset(models.Model): - """ Model for a set of associated data and metadata items. + :class:`LocalPath` describes a path on the local file system. It + inherits from :class:`Location`. Fields: + + * ``path``: a path on the local file system """ - - -class DataItem(BaseLocation): - """ Model for locateable data items contributing to a dataset. Data items - can be linked to either a storage or a package or none of both. + LOCATION_TYPE = "local" + + path = models.CharField(max_length=1024, validators=[validate_path]) + +class RemotePath(Location): + """ + :class:`RemotePath` describes a path on an FTP repository. It + inherits from :class:`Location`. Fields: + + * ``storage``: a foreign key of an :class:`FTPStorage` entry. + * ``path``: path on the repository + """ + + LOCATION_TYPE = "ftp" + + storage = models.ForeignKey(FTPStorage, related_name="paths") + path = models.CharField(max_length=1024) + +class RasdamanLocation(Location): + """ + :class:`RasdamanLocation` describes the parameters for accessing a + rasdaman array. It inherits from :class:`Location`. Fields: + + * ``storage``: a foreign key of a :class:`RasdamanStorage` entry + * ``collection``: name of the rasdaman collection that contains the + array + * ``oid``: rasdaman OID of the array (note that this is a float) + """ + + LOCATION_TYPE = "rasdaman" + + storage = models.ForeignKey(RasdamanStorage, related_name="rasdaman_locations") + collection = models.CharField(max_length=1024) # comparable to table + oid = models.FloatField(null=True, blank=True) # float due to rasdaman architecture; comparable to array + +class CacheFile(models.Model): + """ + :class:`CacheFile` stores the whereabouts of a file held in the + cache. Fields: + + * ``location``: a link to a :class:`LocalPath` denoting the path + to the cached file + * ``size``: the size of the file in bytes, null if it is not known + * ``access_timestamp``: the time of the last access """ - dataset = models.ForeignKey(Dataset, related_name="data_items", null=True, blank=True) - package = models.ForeignKey(Package, related_name="data_items", null=True, blank=True) - semantic = models.CharField(max_length=64) + location = models.ForeignKey(LocalPath, related_name="cache_files") + size = models.IntegerField(null=True) + access_timestamp = models.DateTimeField() diff -Nru eoxserver-0.4.0beta2/eoxserver/backends/packages/tar.py eoxserver-0.3.2/eoxserver/backends/packages/tar.py --- eoxserver-0.4.0beta2/eoxserver/backends/packages/tar.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/backends/packages/tar.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,47 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -from tarfile import TarFile -from eoxserver.core import Component, implements -from eoxserver.backends.interfaces import PackageInterface - - -class TARPackage(Component): - implements(PackageInterface) - - - name = "TAR" - - def extract(self, package_filename, location, path): - tarfile = TarFile(package_filename, "r") - tarfile.extract(location, path) - - - def list_files(self, package_filename): - tarfile = TarFile(package_filename, "r") - # TODO: get list diff -Nru eoxserver-0.4.0beta2/eoxserver/backends/packages/zip.py eoxserver-0.3.2/eoxserver/backends/packages/zip.py --- eoxserver-0.4.0beta2/eoxserver/backends/packages/zip.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/backends/packages/zip.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,53 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -import shutil -from zipfile import ZipFile - -from eoxserver.core import Component, implements -from eoxserver.backends.interfaces import PackageInterface - - -class ZIPPackage(Component): - """Implementation of the package interface for ZIP package files. - """ - - implements(PackageInterface) - - name = "ZIP" - - def extract(self, package_filename, location, path): - zipfile = ZipFile(package_filename, "r") - infile = zipfile.open(location) - with open(path, "wb") as outfile: - shutil.copyfileobj(infile, outfile) - - - def list_files(self, package_filename): - zipfile = ZipFile(package_filename, "r") - # TODO: get list diff -Nru eoxserver-0.4.0beta2/eoxserver/backends/rasdaman.py eoxserver-0.3.2/eoxserver/backends/rasdaman.py --- eoxserver-0.4.0beta2/eoxserver/backends/rasdaman.py 1970-01-01 00:00:00.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/backends/rasdaman.py 2013-12-10 14:57:00.000000000 +0000 @@ -0,0 +1,297 @@ +#------------------------------------------------------------------------------- +# $Id$ +# +# Project: EOxServer +# Authors: Stephan Krause +# Stephan Meissl +# +#------------------------------------------------------------------------------- +# Copyright (C) 2011 EOX IT Services GmbH +# +# 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 of this Software or works derived from this 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. +#------------------------------------------------------------------------------- + +""" +This module implements the rasdaman database backend for EOxServer. +""" + +from eoxserver.core.system import System +from eoxserver.core.exceptions import InternalError +from eoxserver.backends.models import RasdamanStorage as RasdamanStorageRecord +from eoxserver.backends.models import RasdamanLocation +from eoxserver.backends.interfaces import ( + StorageInterface, DatabaseLocationInterface +) +from eoxserver.backends.base import LocationWrapper +from eoxserver.backends.exceptions import DataAccessError + +class RasdamanStorage(object): + """ + This class implements the rasdaman storage. + """ + REGISTRY_CONF = { + "name": "rasdaman Storage", + "impl_id": "backends.rasdaman.RasdamanStorage", + "registry_values": { + "backends.interfaces.storage_type": "rasdaman" + } + } + + def getType(self): + """ + Returns ``"rasdaman"`` + """ + return "rasdaman" + + def getStorageCapabilities(self): + """ + Returns the storage capabilities, i.e. the names of the optional methods + implemented by the storage. Currently none are supported. + """ + return tuple() + + def getSize(self, location): + """ + Not supported; raises :exc:`~.InternalError`. + """ + raise InternalError( + "The rasdaman storage does not support array size retrieval." + ) + + def getLocalCopy(self, location, target): + """ + Not supported; raises :exc:`~.InternalError`. + """ + raise InternalError( + "The rasdaman storage does not support copies to the local file system." + ) + + def detect(self, location, search_pattern=None): + """ + Not supported; raises :exc:`~.InternalError`. + """ + raise InternalError( + "The rasdaman storage does not support detection of datasets." + ) + + def exists(self, location): + raise InternalError( + "The rasdaman storage does not support detection of datasets." + ) + +RasdamanStorageImplementation = \ +StorageInterface.implement(RasdamanStorage) + +class RasdamanArrayWrapper(LocationWrapper): + """ + This is a wrapper for rasdaman database locations. It inherits from + :class:`~.LocationWrapper`. + + .. method:: setAttrs(**kwargs) + + The following attribute keyword arguments are accepted: + + * ``host`` (required): the host name of the server rasdaman runs on + * ``port`` (optional): the port number where to reach rasdaman + * ``user`` (optional): the user name to be used for login + * ``db_name`` (optional): the database name + * ``passwd`` (optional): the password to be used for login + * ``collection`` (required): the name of the collection in the database + * ``oid`` (optional): the ``oid`` of the array within the collection + """ + + REGISTRY_CONF = { + "name": "Rasdaman Array Wrapper", + "impl_id": "backends.rasdaman.RasdamanArrayWrapper", + "factory_ids": ( + "backends.factories.LocationFactory", + ) + } + + + def __init__(self): + super(RasdamanArrayWrapper, self).__init__() + + self.host = None + self.port = None + self.db_name = None + self.user = None + self.passwd = None + self.collection = None + self.oid = None + + def getType(self): + """ + Returns ``"rasdaman"``. + """ + + return "rasdaman" + + def getHost(self): + """ + Returns the host name of the server rasdaman runs on. + """ + if self.record: + return self.record.storage.host + else: + return self.host + + def getPort(self): + """ + Returns the port number where to reach rasdaman, or ``None`` if it + has not been defined. + """ + if self.record: + return self.record.storage.port + else: + return self.port + + def getDBName(self): + """ + Returns the rasdaman database name, or ``None`` if it has not been + defined. + """ + if self.record: + return self.record.storage.db_name + else: + return self.db_name + + def getUser(self): + """ + Returns the user name used to login to the database, or ``None`` if it + has not been defined. + """ + if self.record: + return self.record.storage.user + else: + return self.user + + def getPassword(self): + """ + Returns the password used to login to the database, or ``None`` if it + has not been defined. + """ + if self.record: + return self.record.storage.passwd + else: + return self.passwd + + def getCollection(self): + """ + Returns the collection name. + """ + if self.record: + return self.record.collection + else: + return self.collection + + def getOID(self): + """ + Returns the oid of the array within the collection. + """ + if self.record: + return self.record.oid + else: + return self.oid + + def _bind_to_storage(self): + return System.getRegistry().bind("backends.rasdaman.RasdamanStorage") + + def _validate_record(self, record): + if record.location_type != "rasdaman": + raise InternalError( + "Cannot assign '%s' type location record to rasdaman location." %\ + record.location_type + ) + + def _set_record(self, record): + if isinstance(record, RasdamanLocation): + self.record = record + else: + self.record = record.rasdamanlocation + + def _validate_attrs(self, **kwargs): + if "host" not in kwargs or "collection" not in kwargs: + raise InternalError( + "'host' and 'collection' are needed to initialize a rasdaman location instance." + ) + + def _set_attrs(self, **kwargs): + self.host = kwargs["host"] + self.collection = kwargs["collection"] + self.oid = kwargs.get("oid") + + self.port = kwargs.get("port") + self.db_name = kwargs.get("db_name") + self.user = kwargs.get("user") + self.passwd = kwargs.get("passwd") + + def _fetch_unique_record(self): + # no uniqueness constraints apply + + return None + + def _get_query(self, fields=None): + query = {} + + if fields is None or "collection" in fields: + query["collection"] = self.getCollection() + + if fields is None or "oid" in fields: + query["oid"] = self.getOID() + + if fields is None or "host" in fields: + query["storage__host"] = self.getHost() + + if fields is None or "port" in fields: + query["storage__port"] = self.getPort() + + if fields is None or "db_name" in fields: + query["storage__db_name"] = self.getDBName() + + if fields is None or "user" in fields: + query["storage__user"] = self.getUser() + + if fields is None or "passwd" in fields: + query["storage__passwd"] = self.getPassword() + + return query + + def _get_query_set(self, query): + return RasdamanLocation.objects.filter(**query) + + def _create_record(self): + storage_record = RasdamanStorageRecord.objects.get_or_create( + storage_type = RasdamanStorageRecord.STORAGE_TYPE, + host = self.host, + port = self.port, + db_name = self.db_name, + user = self.user, + passwd = self.passwd + )[0] + + self.record = RasdamanLocation.objects.create( + location_type = RasdamanLocation.LOCATION_TYPE, + storage = storage_record, + collection = self.collection, + oid = self.oid + ) + +RasdamanArrayWrapperImplementation = \ +DatabaseLocationInterface.implement(RasdamanArrayWrapper) diff -Nru eoxserver-0.4.0beta2/eoxserver/backends/storages/ftp.py eoxserver-0.3.2/eoxserver/backends/storages/ftp.py --- eoxserver-0.4.0beta2/eoxserver/backends/storages/ftp.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/backends/storages/ftp.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,92 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -from os import path -from ftplib import FTP -from urlparse import urlparse - -from django.core.exceptions import ValidationError - -from eoxserver.core import Component, implements -from eoxserver.backends.interfaces import FileStorageInterface - - -class FTPStorage(Component): - implements(FileStorageInterface) - - name = "FTP" - - def validate(self, url): - parsed = urlparse(url) - if not parsed.hostname: - raise ValidationError( - "Invalid FTP URL: could not determine hostname." - ) - if parsed.scheme and parsed.scheme.upper() != "FTP": - raise ValidationError( - "Invalid FTP URL: invalid scheme 's'." % parsed.scheme - ) - - def retrieve(self, url, location, result_path): - """ Retrieves the file referenced by `location` from the server - specified by its `url` and stores it under the `result_path`. - """ - - ftp, parsed_url = self._open(url) - - try: - cmd = "RETR %s" % path.join(parsed_url.path, location) - with open(result_path, 'wb') as local_file: - ftp.retrbinary(cmd, local_file.write) - - finally: - ftp.quit() - - - def list_files(self, url, location): - ftp, parsed_url = self._open(url) - - try: - return ftp.nlst(location) - except ftplib.error_perm, resp: - if str(resp).startswith("550"): - return [] - else: - raise - finally: - ftp.quit() - - - def _open(self, url): - parsed_url = urlparse(url) - ftp = FTP() - ftp.connect(parsed_url.hostname, parsed_url.port) - # TODO: default username/password? - ftp.login(parsed_url.username, parsed_url.password) - - return ftp, parsed_url diff -Nru eoxserver-0.4.0beta2/eoxserver/backends/storages/http.py eoxserver-0.3.2/eoxserver/backends/storages/http.py --- eoxserver-0.4.0beta2/eoxserver/backends/storages/http.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/backends/storages/http.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,46 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -from urllib import urlretrieve -from urlparse import urljoin - -from eoxserver.core import Component, implements -from eoxserver.backends.interfaces import FileStorageInterface - - -class HTTPStorage(Component): - implements(FileStorageInterface) - - - name = "HTTP" - - def validate(self, url): - pass - - def retrieve(self, url, location, path): - urlretrieve(urljoin(url, location), path) diff -Nru eoxserver-0.4.0beta2/eoxserver/backends/storages/local.py eoxserver-0.3.2/eoxserver/backends/storages/local.py --- eoxserver-0.4.0beta2/eoxserver/backends/storages/local.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/backends/storages/local.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,43 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -import os.path -from urlparse import urlparse - -from eoxserver.core import Component, implements -from eoxserver.backends.interfaces import FileStorageInterface - - -class LocalStorage(Component): - implements(FileStorageInterface) - - - name = "local" - - def retrieve(self, url, location, path): - return location diff -Nru eoxserver-0.4.0beta2/eoxserver/backends/storages/rasdaman.py eoxserver-0.3.2/eoxserver/backends/storages/rasdaman.py --- eoxserver-0.4.0beta2/eoxserver/backends/storages/rasdaman.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/backends/storages/rasdaman.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,87 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -from urlparse import urlparse - -from eoxserver.core import Component, implements -from eoxserver.backends.interfaces import ConnectedStorageInterface - - -class RasdamanStorage(Component): - implements(ConnectedStorageInterface) - - name = "rasdaman" - - def validate(self, url): - parsed = urlparse(url) - - if not parsed.hostname: - raise ValidationError( - "Invalid Rasdaman URL: could not determine hostname." - ) - if parsed.scheme and parsed.scheme.lower() != "rasdaman": - raise ValidationError( - "Invalid Rasdaman URL: invalid scheme 's'." % parsed.scheme - ) - - - def connect(self, url, location, format): - parsed = urlparse(url) - - # hostname + path -> hostname - # port -> port - # user -> user - # password -> password - # fragment -> dbname - - # location can either be an oid, collection or query - - if format == "rasdaman/oid": - query = "select ( a [$x_lo:$x_hi,$y_lo:$y_hi] ) from %s as a where oid(a)=%f" % () # TODO - elif format == "rasdaman/collection": - query = "select ( a [$x_lo:$x_hi,$y_lo:$y_hi] ) from %s as a" % location - elif format == "rasdaman/query": - query = location - - parts = { - "host": parsed.hostname + "/" + parsed.path, - "query": query - } - - if parsed.port is not None: - parts["port"] = parsed.port - if parsed.username is not None: - parts["user"] = parsed.username - if parsed.password is not None: - parts["password"] = parsed.password - if parsed.fragment: - parts["database"] = parsed.fragment - - return "rasdaman: " + " ".join( - map(lambda k, v: "%s='v'" % (k, v), parts.items()) - ) diff -Nru eoxserver-0.4.0beta2/eoxserver/backends/testbase.py eoxserver-0.3.2/eoxserver/backends/testbase.py --- eoxserver-0.4.0beta2/eoxserver/backends/testbase.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/backends/testbase.py 2013-12-10 14:57:00.000000000 +0000 @@ -1,4 +1,5 @@ #------------------------------------------------------------------------------- +# $Id$ # # Project: EOxServer # Authors: Stephan Krause @@ -10,8 +11,8 @@ # 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 +# 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 @@ -27,31 +28,130 @@ #------------------------------------------------------------------------------- import logging -from django.utils.unittest import SkipTest - -try: - from twisted.protocols.ftp import FTPFactory, FTPRealm - from twisted.cred.portal import Portal - from twisted.cred.checkers import AllowAnonymousAccess, FilePasswordDB - from twisted.internet import reactor - HAVE_TWISTED = True -except ImportError: - HAVE_TWISTED = False - +from eoxserver.core.system import System +from eoxserver.testing.core import EOxServerTestCase, BASE_FIXTURES logger = logging.getLogger(__name__) -def withFTPServer(port=2121, directory=None): - def wrap(f): - def wrapped(*args, **kwargs): - if not HAVE_TWISTED: - raise SkipTest("This test requires Twisted to run.") - # TODO: start FTP server - ret = f(args, **kwargs) - # TODO: stop ftp server - return ret - return wrapped - return wrap +BACKEND_FIXTURES = ["testing_coverages.json", "testing_backends.json"] + +class BackendTestCase(EOxServerTestCase): + fixtures = BASE_FIXTURES + BACKEND_FIXTURES +class LocationWrapperTestCase(BackendTestCase): + def setUp(self): + super(LocationWrapperTestCase,self).setUp() + + logger.info("Starting test case: %s" % self.__class__.__name__) + + self.record = self._get_record() + + self.factory = System.getRegistry().bind( + "backends.factories.LocationFactory" + ) + + self.wrapper = self._get_wrapper() + + def _get_record(self): + raise NotImplemented() + + def _get_wrapper(self): + raise NotImplemented() + +class LocationWrapperCreationTestCase(LocationWrapperTestCase): + def testType(self): + self.assertEqual(self.wrapper.getType(), self._get_type()) + + def testValues(self): + for method, value in self._get_values(): + self.assertEqual(getattr(self.wrapper, method)(), value) + + def _get_type(self): + raise NotImplementedError() + + def _get_values(self): + raise NotImplementedError() + +class LocationWrapperCreateAndSaveTestCase(LocationWrapperTestCase): + def setUp(self): + super(LocationWrapperCreateAndSaveTestCase, self).setUp() + self.wrapper.sync() + + def testType(self): + self.assertEqual( + self.wrapper.getType(), self.wrapper.getRecord().location_type + ) + + def testValues(self): + raise NotImplementedError() + + def _get_record(self): + return None + + def _get_wrapper(self): + return self.factory.create(type=self._get_type(), **self._get_arguments()) + + def _get_type(self): + raise NotImplementedError() + + def _get_arguments(self): + raise NotImplementedError() + + +# Specialized test cases for local, remote and rasdaman locations + +# local + +class LocalPathCreationTestCase(LocationWrapperCreationTestCase): + def _get_type(self): + return "local" + + def _get_values(self): + return ( + ("getPath", self.record.path), + ) + +class LocalPathCreateAndSaveTestCase(LocationWrapperCreateAndSaveTestCase): + def _get_type(self): + return "local" + +# remote + +class RemotePathCreationTestCase(LocationWrapperCreationTestCase): + def _get_type(self): + return "ftp" + + def _get_values(self): + return ( + ("getHost", self.record.storage.host), + ("getPort", self.record.storage.port), + ("getUser", self.record.storage.user), + ("getPassword", self.record.storage.passwd), + ("getPath", self.record.path) + ) + +class RemotePathCreateAndSaveTestCase(LocationWrapperCreateAndSaveTestCase): + def _get_type(self): + return "ftp" + +# rasdaman + +class RasdamanLocationCreationTestCase(LocationWrapperCreationTestCase): + def _get_type(self): + return "rasdaman" + + def _get_values(self): + return ( + ("getCollection", self.record.collection), + ("getOID", self.record.oid), + ("getHost", self.record.storage.host), + ("getPort", self.record.storage.port), + ("getUser", self.record.storage.user), + ("getPassword", self.record.storage.passwd) + ) + +class RasdamanLocationCreateAndSaveTestCase(LocationWrapperCreateAndSaveTestCase): + def _get_type(self): + return "rasdaman" diff -Nru eoxserver-0.4.0beta2/eoxserver/backends/tests.py eoxserver-0.3.2/eoxserver/backends/tests.py --- eoxserver-0.4.0beta2/eoxserver/backends/tests.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/backends/tests.py 2013-12-10 14:57:00.000000000 +0000 @@ -1,4 +1,5 @@ #------------------------------------------------------------------------------- +# $Id$ # # Project: EOxServer # Authors: Stephan Krause @@ -10,8 +11,8 @@ # 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 +# 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 @@ -30,99 +31,347 @@ from glob import glob import logging -from django.test import TestCase +from eoxserver.backends.models import ( + LocalPath, RemotePath, RasdamanLocation +) from eoxserver.backends import testbase -from eoxserver.backends import models -from eoxserver.backends.cache import CacheContext -from eoxserver.backends.access import retrieve -from eoxserver.backends.component import BackendComponent, env -from eoxserver.backends.testbase import withFTPServer logger = logging.getLogger(__name__) +# test local path wrapper: get() with record and check wrapper type and +# return values -""" "New" data models -""" - - -def create(Model, *args, **kwargs): - model = Model(*args, **kwargs) - model.full_clean() - model.save() - return model - - - -class RetrieveTestCase(TestCase): - def setUp(self): - pass - - - def tearDown(self): - pass - +class LocalPathGetWithRecordTestCase(testbase.LocalPathCreationTestCase): + def _get_record(self): + return LocalPath.objects.get(pk=1) + + def _get_wrapper(self): + return self.factory.get(record=self.record) + +# test local path wrapper: get() with primary key and check wrapper type and +# return values + +class LocalPathGetWithPrimaryKeyTestCase(testbase.LocalPathCreationTestCase): + def _get_record(self): + return LocalPath.objects.get(pk=1) + + def _get_wrapper(self): + return self.factory.get(pk=1) + +# test local path wrapper: initialize with attributes and check wrapper type +# and return values + +class LocalPathCreateWithAttributesTestCase(testbase.LocalPathCreationTestCase): + def _get_record(self): + return None + + def _get_wrapper(self): + return self.factory.create(type="local", path="some/path") + + def _get_values(self): + return ( + ("getPath", "some/path"), + ) - def test_retrieve_http(self): - import storages, packages +# test local path wrapper: initialize with attributes and save to database - storage = create(models.Storage, - url="http://eoxserver.org/export/2523/downloads", - storage_type="HTTP" +class LocalPathCreateAndSaveTestCase(testbase.LocalPathCreateAndSaveTestCase): + def testValues(self): + record = self.wrapper.getRecord() + values = self._get_arguments() + + self.assertEqual(record.path, values["path"]) + + def _get_arguments(self): + return { + "path": "some/path" + } + +# test access to local data: open + +# test access to local data: getLocalCopy + +# test access to local data: getSize + +class LocalAccessTestCase(testbase.LocationWrapperTestCase): + def _get_record(self): + return LocalPath.objects.get(pk=1) + + def _get_wrapper(self): + return self.factory.get(record=self.record) + + def testOpen(self): + f = self.wrapper.open() + + self.assert_(isinstance(f, file)) + + def testLocalCopy(self): + + target = "/tmp" + + dest_path = os.path.join(target, os.path.basename(self.wrapper.getPath())) + + target_wrapper = self.wrapper.getLocalCopy(target) + + self.assertEqual(target_wrapper.getPath(), dest_path) + + self.assert_(os.path.exists(dest_path)) + + def testSize(self): + size = os.path.getsize(self.wrapper.getPath()) + + self.assertEqual(self.wrapper.getSize(), size) + +# test access to local data: detect (without search_pattern) + +# test access to local data: detect (with search_pattern) + +class LocalDetectionTestCase(testbase.LocationWrapperTestCase): + def _get_record(self): + return LocalPath.objects.get(pk=13) + + def _get_wrapper(self): + return self.factory.get(record=self.record) + + def testDetectWithoutSearchPattern(self): + locations = self.wrapper.detect() + + paths = [location.getPath() for location in locations] + + dir_paths = glob(os.path.join(self.wrapper.getPath(), "*")) +\ + glob(os.path.join(self.wrapper.getPath(), "*", "*")) + + logger.debug("found files: %s" % ", ".join(paths)) + + logger.debug("files in dir: %s" % ", ".join(dir_paths)) + + self.assertItemsEqual(paths, dir_paths) + + def testDetectWithSearchPattern(self): + locations = self.wrapper.detect("*.tif") + + names = [os.path.basename(location.getPath()) for location in locations] + + dir_names = [ + os.path.basename(path) + for path in glob(os.path.join(self.wrapper.getPath(), "*.tif")) + ] + + self.assertItemsEqual(names, dir_names) + +# test remote path wrapper: get with record and check return values + +class RemotePathGetWithRecordTestCase(testbase.RemotePathCreationTestCase): + def _get_record(self): + return RemotePath.objects.get(pk=15) + + def _get_wrapper(self): + return self.factory.get(record=self.record) + +# test remote path wrapper: get with primary key and check return values + +class RemotePathGetWithPrimaryKeyTestCase(testbase.RemotePathCreationTestCase): + def _get_record(self): + return RemotePath.objects.get(pk=15) + + def _get_wrapper(self): + return self.factory.get(pk=15) + +# test remote path wrapper: initialize with attributes and check return values + +class RemotePathCreateWithAttributesTestCase(testbase.RemotePathCreationTestCase): + def _get_record(self): + return None + + def _get_wrapper(self): + return self.factory.create( + type="ftp", + host="ftp.example.org", + port=21, + user="anonymous", + passwd="anonymous", + path="some/path" ) - dataset = create(models.DataItem, - location="EOxServer_documentation-0.3.0.pdf", - storage=storage, - semantic="pdffile" + + def _get_values(self): + return ( + ("getHost", "ftp.example.org"), + ("getPort", 21), + ("getUser", "anonymous"), + ("getPassword", "anonymous"), + ("getPath", "some/path"), ) - with CacheContext() as c: - cache_path = retrieve(dataset, c) - self.assertTrue(os.path.exists(cache_path)) - - self.assertFalse(os.path.exists(cache_path)) - - - @withFTPServer() - def test_retrieve_ftp_zip(self): - import storages, packages +# test remote path wrapper: initialize with attributes and save to database - storage = create(models.Storage, - url="ftp://anonymous:@localhost:2121/", - storage_type="FTP" +class RemotePathCreateAndSaveTestCase(testbase.RemotePathCreateAndSaveTestCase): + def testValues(self): + record = self.wrapper.getRecord() + values = self._get_arguments() + + self.assertEqual(record.storage.host, values["host"]), + self.assertEqual(record.storage.port, values["port"]), + self.assertEqual(record.storage.user, values["user"]), + self.assertEqual(record.storage.passwd, values["passwd"]), + self.assertEqual(record.path, values["path"]) + + def _get_arguments(self): + return { + "host": "ftp.example.org", + "port": 21, + "user": "anonymous", + "passwd": "anonymous", + "path": "some/path" + } + +# test access to remote data: getLocalCopy + +# test access to remote data: getSize + +class RemoteAccessTestCase(testbase.LocationWrapperTestCase): + def _get_record(self): + return RemotePath.objects.get(pk=15) + + def _get_wrapper(self): + return self.factory.get(record=self.record) + + def testLocalCopy(self): + logger.debug("Retrieving remote file '%s'" % self.wrapper.getPath()) + + target = "/tmp" + + dest_path = os.path.join(target, os.path.basename(self.wrapper.getPath())) + + target_wrapper = self.wrapper.getLocalCopy(target) + + self.assertEqual(target_wrapper.getPath(), dest_path) + + self.assert_(os.path.exists(dest_path)) + + def testSize(self): + size = 5992628 + + self.assertEqual(self.wrapper.getSize(), size) + +# test access to remote data: detect (without search_pattern) + +# test access to remote data: detect (with search_pattern) + +class RemoteDetectionTestCase(testbase.LocationWrapperTestCase): + def _get_record(self): + return RemotePath.objects.get(pk=27) + + def _get_wrapper(self): + return self.factory.get(record=self.record) + + def testDetectWithoutSearchPattern(self): + locations = self.wrapper.detect() + + paths = [location.getPath() for location in locations] + + dir_paths = RemotePath.objects.filter(pk__in=range(15,24)).values_list( + 'path', flat=True ) - - package = create(models.Package, - location="package.zip", - format="ZIP", - storage=storage + + logger.debug("testDetectWithoutSearchPattern()") + + logger.debug("found files: %s" % ", ".join(paths)) + + logger.debug("files in dir: %s" % ", ".join(dir_paths)) + + self.assertItemsEqual(paths, dir_paths) + + def testDetectWithSearchPattern(self): + locations = self.wrapper.detect("*.tif") + + paths = [location.getPath() for location in locations] + + dir_paths = RemotePath.objects.filter(pk__in=(15,18,21)).values_list( + 'path', flat=True ) - dataset = create(models.DataItem, - location="file.txt", - package=package, - semantic="textfile" + logger.debug("testDetectWithSearchPattern()") + + logger.debug("found files: %s" % ", ".join(paths)) + + logger.debug("files in dir: %s" % ", ".join(dir_paths)) + + self.assertItemsEqual(paths, dir_paths) + + +# test rasdaman location wrapper: get with record and check return values + +class RasdamanLocationGetWithRecordTestCase(testbase.RasdamanLocationCreationTestCase): + def _get_record(self): + return RasdamanLocation.objects.get(pk=28) + + def _get_wrapper(self): + return self.factory.get(record=self.record) + +# test rasdaman location wrapper: get with primary key and check return values + +class RasdamanLocationGetWithPrimaryKeyTestCase(testbase.RasdamanLocationCreationTestCase): + def _get_record(self): + return RasdamanLocation.objects.get(pk=28) + + def _get_wrapper(self): + return self.factory.get(pk=28) + + +# test rasdaman location wrapper: initialize with attributes and check return values + +class RasdamanLocationCreateWithAttributesTestCase(testbase.RasdamanLocationCreationTestCase): + def _get_record(self): + return None + + def _get_wrapper(self): + return self.factory.create( + type="rasdaman", + host="rasdaman.example.org", + port=7001, + user="anonymous", + passwd="anonymous", + db_name="", + collection="some_other_collection", + oid=2.0 ) - - dataset2 = create(models.DataItem, - location="file2.txt", - package=package, - semantic="textfile" + + def _get_values(self): + return ( + ("getHost", "rasdaman.example.org"), + ("getPort", 7001), + ("getUser", "anonymous"), + ("getPassword", "anonymous"), + ("getCollection", "some_other_collection"), + ("getOID", 2.0) ) - with CacheContext() as c: - cache_path = retrieve(dataset, c) - cache_path2 = retrieve(dataset2, c) - self.assertTrue(os.path.exists(cache_path)) - self.assertTrue(os.path.exists(cache_path2)) - - with open(cache_path) as f: - self.assertEqual(f.read(), "test\n") - - with open(cache_path2) as f: - self.assertEqual(f.read(), "test 2\n") +# test rasdaman location wrapper: initialize with attributes and save to database - self.assertFalse(os.path.exists(cache_path)) - self.assertFalse(os.path.exists(cache_path2)) +class RasdamanLocationCreateAndSaveTestCase(testbase.RasdamanLocationCreateAndSaveTestCase): + def testValues(self): + record = self.wrapper.getRecord() + values = self._get_arguments() + + self.assertEqual(record.storage.host, values["host"]), + self.assertEqual(record.storage.port, values["port"]), + self.assertEqual(record.storage.user, values["user"]), + self.assertEqual(record.storage.passwd, values["passwd"]), + self.assertEqual(record.collection, values["collection"]) + self.assertEqual(record.oid, values["oid"]) + + def _get_arguments(self): + return { + "host": "rasdaman.example.org", + "port": 7001, + "user": "anonymous", + "passwd": "anonymous", + "db_name": "", + "collection": "some_other_collection", + "oid": 2.0, + "location_type": "rasdaman" + } +# test access to rasdaman data -> resources.coverages.tests (the rasdaman storage offers no low-level access capabilities) diff -Nru eoxserver-0.4.0beta2/eoxserver/backends/validators.py eoxserver-0.3.2/eoxserver/backends/validators.py --- eoxserver-0.4.0beta2/eoxserver/backends/validators.py 1970-01-01 00:00:00.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/backends/validators.py 2013-12-10 14:57:00.000000000 +0000 @@ -0,0 +1,37 @@ +#------------------------------------------------------------------------------- +# $Id$ +# +# Project: EOxServer +# Authors: Stephan Krause +# Stephan Meissl +# Fabian Schindler +# +#------------------------------------------------------------------------------- +# Copyright (C) 2011 EOX IT Services GmbH +# +# 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 of this Software or works derived from this 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. +#------------------------------------------------------------------------------- + +import os.path +from django.core.exceptions import ValidationError + +def validate_path(path): + if not os.path.exists(path): + raise ValidationError("Path '%s' does not reference a path " + "in the file system." % path) diff -Nru eoxserver-0.4.0beta2/eoxserver/conf/default.conf eoxserver-0.3.2/eoxserver/conf/default.conf --- eoxserver-0.4.0beta2/eoxserver/conf/default.conf 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/conf/default.conf 2013-12-10 14:57:00.000000000 +0000 @@ -1,4 +1,5 @@ #------------------------------------------------------------------------------- +# $Id$ # # Project: EOxServer # Authors: Stephan Krause @@ -10,8 +11,8 @@ # 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 +# 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 diff -Nru eoxserver-0.4.0beta2/eoxserver/conf/default_formats.conf eoxserver-0.3.2/eoxserver/conf/default_formats.conf --- eoxserver-0.4.0beta2/eoxserver/conf/default_formats.conf 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/conf/default_formats.conf 2013-12-10 14:57:00.000000000 +0000 @@ -1,4 +1,5 @@ #------------------------------------------------------------------------------- +# $Id$ # # Project: EOxServer # Authors: Martin Paces @@ -9,8 +10,8 @@ # 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 +# 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 diff -Nru eoxserver-0.4.0beta2/eoxserver/contrib/gdal_array.py eoxserver-0.3.2/eoxserver/contrib/gdal_array.py --- eoxserver-0.4.0beta2/eoxserver/contrib/gdal_array.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/contrib/gdal_array.py 2013-12-10 14:57:00.000000000 +0000 @@ -1,4 +1,5 @@ #------------------------------------------------------------------------------- +# $Id$ # # Project: EOxServer # Authors: Stephan Krause @@ -12,8 +13,8 @@ # 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 +# 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 @@ -28,8 +29,4 @@ # THE SOFTWARE. #------------------------------------------------------------------------------- -import os - - -if os.environ.get('READTHEDOCS', None) != 'True': - from osgeo.gdal_array import * +from osgeo.gdal_array import * diff -Nru eoxserver-0.4.0beta2/eoxserver/contrib/gdal.py eoxserver-0.3.2/eoxserver/contrib/gdal.py --- eoxserver-0.4.0beta2/eoxserver/contrib/gdal.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/contrib/gdal.py 2013-12-10 14:57:00.000000000 +0000 @@ -1,4 +1,5 @@ #------------------------------------------------------------------------------- +# $Id$ # # Project: EOxServer # Authors: Stephan Krause @@ -12,8 +13,8 @@ # 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 +# 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 @@ -33,103 +34,49 @@ all available drivers. """ -import os +try: + from osgeo.gdal import * +except ImportError: + from gdal import * +from django.utils.datastructures import SortedDict + + +UseExceptions() +AllRegister() + +GCI_TO_NAME = SortedDict(( + (GCI_Undefined, "Undefined"), + (GCI_GrayIndex, "GrayIndex"), + (GCI_PaletteIndex, "PaletteIndex"), + (GCI_RedBand, "RedBand"), + (GCI_GreenBand, "GreenBand"), + (GCI_BlueBand, "BlueBand"), + (GCI_AlphaBand, "AlphaBand"), + (GCI_HueBand, "HueBand"), + (GCI_SaturationBand, "SaturationBand"), + (GCI_LightnessBand, "LightnessBand"), + (GCI_CyanBand, "CyanBand"), + (GCI_MagentaBand, "MagentaBand"), + (GCI_YellowBand, "YellowBand"), + (GCI_BlackBand, "BlackBand") +)) + +NAME_TO_GCI = dict( (j.lower(),i) for (i,j) in GCI_TO_NAME.items() ) + +GDT_TO_NAME = SortedDict(( + (GDT_Byte, "Byte"), + (GDT_UInt16, "UInt16"), + (GDT_Int16, "Int16"), + (GDT_UInt32, "UInt32"), + (GDT_Int32, "Int32"), + (GDT_Float32, "Float32"), + (GDT_Float64, "Float64"), + (GDT_CInt16, "CInt16"), + (GDT_CInt32, "CInt32"), + (GDT_CFloat32, "CFloat32"), + (GDT_CFloat64, "CFloat64"), +)) + +NAME_TO_GDT = SortedDict( (j.lower(),i) for (i,j) in GDT_TO_NAME.items() ) -if os.environ.get('READTHEDOCS', None) != 'True': - try: - from osgeo.gdal import * - except ImportError: - from gdal import * - from django.utils.datastructures import SortedDict - - UseExceptions() - AllRegister() - - GCI_TO_NAME = SortedDict(( - (GCI_Undefined, "Undefined"), - (GCI_GrayIndex, "GrayIndex"), - (GCI_PaletteIndex, "PaletteIndex"), - (GCI_RedBand, "RedBand"), - (GCI_GreenBand, "GreenBand"), - (GCI_BlueBand, "BlueBand"), - (GCI_AlphaBand, "AlphaBand"), - (GCI_HueBand, "HueBand"), - (GCI_SaturationBand, "SaturationBand"), - (GCI_LightnessBand, "LightnessBand"), - (GCI_CyanBand, "CyanBand"), - (GCI_MagentaBand, "MagentaBand"), - (GCI_YellowBand, "YellowBand"), - (GCI_BlackBand, "BlackBand"), - (GCI_YCbCr_YBand, "YBand"), - (GCI_YCbCr_CbBand, "CbBand"), - (GCI_YCbCr_CrBand, "CrBand"), - )) - - NAME_TO_GCI = dict((j.lower(), i) for (i, j) in GCI_TO_NAME.items()) - - GDT_TO_NAME = SortedDict(( - (GDT_Byte, "Byte"), - (GDT_UInt16, "UInt16"), - (GDT_Int16, "Int16"), - (GDT_UInt32, "UInt32"), - (GDT_Int32, "Int32"), - (GDT_Float32, "Float32"), - (GDT_Float64, "Float64"), - (GDT_CInt16, "CInt16"), - (GDT_CInt32, "CInt32"), - (GDT_CFloat32, "CFloat32"), - (GDT_CFloat64, "CFloat64"), - )) - - NAME_TO_GDT = SortedDict((j.lower(), i) for (i, j) in GDT_TO_NAME.items()) - - GDT_NUMERIC_LIMITS = { - GDT_Byte: (0, 255), - GDT_Int16: (-32768, 32767), - GDT_UInt16: (0, 65535), - GDT_CInt16: (complex(-32768, -32768), complex(32767, 32767)), - GDT_Int32: (-2147483648, 2147483647), - GDT_UInt32: (0, 4294967295), - GDT_CInt32: ( - complex(-2147483648, -2147483648), complex(2147483647, 2147483647) - ), - GDT_Float32: (-3.40282e+38, 3.40282e+38), - GDT_CFloat32: ( - complex(-3.40282e+38, -3.40282e+38), - complex(3.40282e+38, 3.40282e+38) - ), - GDT_Float64: (-1.79769e+308, 1.79769e+308), - GDT_CFloat64: ( - complex(-1.79769e+308, -1.79769e+308), - complex(1.79769e+308, 1.79769e+308) - ) - } - - GDT_SIGNIFICANT_FIGURES = { - GDT_Byte: 3, - GDT_Int16: 5, - GDT_UInt16: 5, - GDT_CInt16: 5, - GDT_Int32: 10, - GDT_UInt32: 10, - GDT_CInt32: 10, - GDT_Float32: 38, - GDT_CFloat32: 38, - GDT_Float64: 308, - GDT_CFloat64: 308 - } - - GDT_INTEGRAL_TYPES = frozenset( - (GDT_Byte, GDT_Int16, GDT_UInt16, GDT_Int32, GDT_UInt32) - ) - - GDT_INTEGRAL_COMPLEX_TYPES = frozenset((GDT_CInt16, GDT_CInt32)) - - GDT_FLOAT_TYPES = frozenset((GDT_Float32, GDT_Float64)) - - GDT_FLOAT_COMPLEX_TYPES = frozenset((GDT_CFloat32, GDT_CFloat64)) - - GDT_COMPLEX_TYPES = frozenset( - (GDT_CInt16, GDT_CInt32, GDT_CFloat32, GDT_CFloat64) - ) diff -Nru eoxserver-0.4.0beta2/eoxserver/contrib/__init__.py eoxserver-0.3.2/eoxserver/contrib/__init__.py --- eoxserver-0.4.0beta2/eoxserver/contrib/__init__.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/contrib/__init__.py 2013-12-10 14:57:00.000000000 +0000 @@ -1,4 +1,5 @@ #------------------------------------------------------------------------------- +# $Id$ # # Project: EOxServer # Authors: Stephan Krause @@ -12,8 +13,8 @@ # 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 +# 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 @@ -31,6 +32,5 @@ """\ This package provides a common interface to contributing third party libraries -that need some special care when importing or are provided with additional -features. +that need some special care when importing. """ diff -Nru eoxserver-0.4.0beta2/eoxserver/contrib/mapserver.py eoxserver-0.3.2/eoxserver/contrib/mapserver.py --- eoxserver-0.4.0beta2/eoxserver/contrib/mapserver.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/contrib/mapserver.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,276 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -import time -import logging -from cgi import escape -import tempfile -import os - -try: - from mapscript import * -except ImportError: - # defaults for document generation - if os.environ.get('READTHEDOCS', None) == 'True': - class mapObj: pass - class layerObj: pass - class classObj: pass - class styleObj: pass - class shapeObj: pass - class colorObj: pass - MS_LAYER_RASTER = 0 - MS_LAYER_POLYGON = 1 - MS_GET_REQUEST = 0 - else: - raise -else: - msversion = msGetVersionInt() - -from lxml import etree - -from eoxserver.core.util.multiparttools import iterate -from eoxserver.contrib import gdal - - -logger = logging.getLogger(__name__) - - -class MapServerException(Exception): - def __init__(self, message, locator, code=None): - super(MapServerException, self).__init__(message) - self.locator = locator - self.code = code - - -class MetadataMixIn(object): - """ Mix-In for classes that wrap mapscript objects with associated metadata. - """ - - def __init__(self, metadata=None): - super(MetadataMixIn, self).__init__() - if metadata: - self.setMetaData(metadata) - - def __getitem__(self, key): - self.getMetaData(key) - - def __setitem__(self, key, value): - self.setMetaData(key, value) - - def setMetaData(self, key_or_params, value=None, namespace=None): - """ Convenvience method to allow setting multiple metadata values with - one call and optionally setting a 'namespace' for each entry. - """ - if value is None: - for key, value in key_or_params.items(): - if namespace: - key = "%s_%s" % (namespace, key) - - super(MetadataMixIn, self).setMetaData(key, value) - else: - if namespace: - key = "%s_%s" % (namespace, key_or_params) - else: - key = key_or_params - - return super(MetadataMixIn, self).setMetaData(key, value) - - -class Map(MetadataMixIn, mapObj): - def dispatch(self, request): - return dispatch(self, request) - - -def dispatch(map_, request): - """ Wraps the ``OWSDispatch`` method. Perfoms all necessary steps for a - further handling of the result. - """ - - logger.debug("MapServer: Installing stdout to buffer.") - msIO_installStdoutToBuffer() - - # write the map if debug is enabled - if logger.isEnabledFor(logging.DEBUG): - fd, filename = tempfile.mkstemp(text=True) - try: - with os.fdopen(fd) as f: - map_.save(filename) - logger.debug(f.read()) - finally: - os.remove(filename) - - try: - logger.debug("MapServer: Dispatching.") - ts = time.time() - # Execute the OWS request by mapserver, obtain the status in - # dispatch_status (0 is OK) - status = map_.OWSDispatch(request) - te = time.time() - logger.debug("MapServer: Dispatch took %f seconds." % (te - ts)) - except Exception, e: - raise MapServerException(str(e), "NoApplicableCode") - - raw_bytes = msIO_getStdoutBufferBytes() - - # check whether an error occurred - if status != 0: - # First try to get the error message through the error object - obj = msGetErrorObj() - if obj and obj.message: - raise MapServerException(obj.message, obj.code) - - try: - # try to parse the output as XML - _, data = iterate(raw_bytes).next() - tree = etree.fromstring(str(data)) - exception_elem = tree.xpath("*[local-name() = 'Exception']")[0] - locator = exception_elem.attrib["locator"] - code = exception_elem.attrib["exceptionCode"] - message = exception_elem[0].text - - raise MapServerException(message, locator, code) - - except (etree.XMLSyntaxError, IndexError, KeyError): - pass - - # Fallback: raise arbitrary error - raise MapServerException("Unexpected Error.", "NoApplicableCode") - - logger.debug("MapServer: Performing MapServer cleanup.") - # Workaround for MapServer issue #4369 - if msversion < 60004 or (msversion < 60200 and msversion >= 60100): - msCleanup() - else: - msIO_resetHandlers() - - return raw_bytes - - -class Layer(MetadataMixIn, layerObj): - def __init__(self, name, metadata=None, type=MS_LAYER_RASTER, mapobj=None): - layerObj.__init__(self, mapobj) - MetadataMixIn.__init__(self, metadata) - self.name = name - self.status = MS_ON - if type == MS_LAYER_RASTER: - self.dump = MS_TRUE - self.setConnectionType(MS_RASTER, "") - self.type = type - - -class Class(classObj): - def __init__(self, name, mapobj=None): - classObj.__init__(self, mapobj) - self.name = name - - -class Style(styleObj): - def __init__(self, name, mapobj=None): - styleObj.__init__(self, mapobj) - self.name = name - - -def create_request(values, request_type=MS_GET_REQUEST): - """ Creates a mapserver request from - """ - used_keys = {} - - request = OWSRequest() - request.type = request_type - - if request_type == MS_GET_REQUEST: - for key, value in values: - key = key.lower() - used_keys.setdefault(key, 0) - # addParameter() available in MapServer >= 6.2 - # https://github.com/mapserver/mapserver/issues/3973 - try: - request.addParameter(key.lower(), escape(value)) - # Workaround for MapServer 6.0 - except AttributeError: - used_keys[key] += 1 - request.setParameter( - "%s_%d" % (key, used_keys[key]), escape(value) - ) - elif request_type == MS_POST_REQUEST: - request.postrequest = value - - return request - - -def gdalconst_to_imagemode(const): - """ - This function translates a GDAL data type constant as defined in the - :mod:`gdalconst` module to a MapScript image mode constant. - """ - if const == gdal.GDT_Byte: - return MS_IMAGEMODE_BYTE - elif const in (gdal.GDT_Int16, gdal.GDT_UInt16): - return MS_IMAGEMODE_INT16 - elif const == GDT_Float32: - return MS_IMAGEMODE_FLOAT32 - else: - raise InternalError( - "MapServer is not capable to process the datatype '%s' (%d)." - % gdal.GetDataTypeName(const), const - ) - - -def gdalconst_to_imagemode_string(const): - """ - This function translates a GDAL data type constant as defined in the - :mod:`gdalconst` module to a string as used in the MapServer map file - to denote an image mode. - """ - if const == gdal.GDT_Byte: - return "BYTE" - elif const in (gdal.GDT_Int16, gdal.GDT_UInt16): - return "INT16" - elif const == gdal.GDT_Float32: - return "FLOAT32" - - -def setMetaData(obj, key_or_params, value=None, namespace=None): - """ Convenvience function to allow setting multiple metadata values with - one call and optionally setting a 'namespace' for each entry. - """ - if value is None: - for key, value in key_or_params.items(): - if namespace: - key = "%s_%s" % (namespace, key) - - obj.setMetaData(key, value) - else: - if namespace: - key = "%s_%s" % (namespace, key_or_params) - else: - key = key_or_params - - obj.setMetaData(key, value) - -# alias -set_metadata = setMetaData diff -Nru eoxserver-0.4.0beta2/eoxserver/contrib/ogr.py eoxserver-0.3.2/eoxserver/contrib/ogr.py --- eoxserver-0.4.0beta2/eoxserver/contrib/ogr.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/contrib/ogr.py 2013-12-10 14:57:00.000000000 +0000 @@ -1,4 +1,5 @@ #------------------------------------------------------------------------------- +# $Id$ # # Project: EOxServer # Authors: Stephan Krause @@ -12,8 +13,8 @@ # 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 +# 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 @@ -28,14 +29,9 @@ # THE SOFTWARE. #------------------------------------------------------------------------------- +try: + from osgeo.ogr import * +except ImportError: + from ogr import * -import os - - -if os.environ.get('READTHEDOCS', None) != 'True': - try: - from osgeo.ogr import * - except ImportError: - from ogr import * - - UseExceptions() +UseExceptions() diff -Nru eoxserver-0.4.0beta2/eoxserver/contrib/osr.py eoxserver-0.3.2/eoxserver/contrib/osr.py --- eoxserver-0.4.0beta2/eoxserver/contrib/osr.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/contrib/osr.py 2013-12-10 14:57:00.000000000 +0000 @@ -1,4 +1,5 @@ #------------------------------------------------------------------------------- +# $Id$ # # Project: EOxServer # Authors: Stephan Krause @@ -12,8 +13,8 @@ # 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 +# 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 @@ -28,75 +29,10 @@ # THE SOFTWARE. #------------------------------------------------------------------------------- -import os +try: + from osgeo.osr import * +except ImportError: + from osr import * -if os.environ.get('READTHEDOCS', None) != 'True': - try: - from osgeo.osr import * - except ImportError: - from osr import * - - UseExceptions() - - _SpatialReference = SpatialReference - - -class SpatialReference(object): - """ Extension to the original SpatialReference class. - """ - - def __init__(self, raw=None, format=None): - self.sr = sr = _SpatialReference() - if raw is not None: - format = format.upper() if format is not None else None - if format == "WKT": - sr.ImportFromWkt(raw) - elif isinstance(raw, int) or format == "EPSG": - sr.ImportFromEPSG(int(raw)) - else: - sr.SetFromUserInput(raw) - - def IsSame(self, other): - if isinstance(other, SpatialReference): - return self.sr.IsSame(other.sr) - else: - return self.sr.IsSame(other) - - @property - def proj(self): - return self.sr.ExportToProj4() - - @property - def wkt(self): - return self.sr.ExportToWkt() - - @property - def xml(self): - return self.sr.ExportToXML() - - @property - def url(self): - # TODO: what about other authorities than EPSG? - return "http://www.opengis.net/def/crs/EPSG/0/%d" % self.srid - - @property - def srid(self): - """ Convenience function that tries to get the SRID of the projection. - """ - - if self.sr.IsGeographic(): - cstype = 'GEOGCS' - else: - cstype = 'PROJCS' - - return int(self.sr.GetAuthorityCode(cstype)) - - @property - def swap_axes(self): - # TODO: - pass - - def __getattr__(self, name): - return getattr(self.sr, name) - +UseExceptions() diff -Nru eoxserver-0.4.0beta2/eoxserver/contrib/vrt.py eoxserver-0.3.2/eoxserver/contrib/vrt.py --- eoxserver-0.4.0beta2/eoxserver/contrib/vrt.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/contrib/vrt.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,172 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -from eoxserver.contrib import gdal - - -def get_vrt_driver(): - """ Convenience function to get the VRT driver. - """ - return gdal.GetDriverByName("VRT") - - -class VRTBuilder(object): - """ This class is a helper to easily create VRT datasets from various - sources. - - :param size_x: the pixel size of the X dimension - :param size_y: the pixel size of the Y dimension - :param num_bands: the initial number of bands; bands can be added afterwards - :param data_type: the GDT data type identifier - :param vrt_filename: a path the filename shall be stored at; if none is - specified the dataset will only be kept in memory - """ - - def __init__(self, size_x, size_y, num_bands=0, data_type=None, - vrt_filename=None): - driver = get_vrt_driver() - data_type = data_type if data_type is not None else gdal.GDT_Byte - self._ds = driver.Create( - vrt_filename or "", size_x, size_y, num_bands, data_type - ) - - @classmethod - def from_dataset(cls, ds, vrt_filename=None): - """ A helper function to create a VRT dataset from a given template - dataset. - - :param ds: a :class:`GDAL Dataset ` - """ - - vrt_builder = cls( - ds.RasterXSize, ds.RasterYSize, ds.RasterCount, - vrt_filename=vrt_filename - ) - - for key, value in ds.GetMetadata().items(): - vrt_builder.dataset.SetMetadataItem(key, value) - - @property - def dataset(self): - """ Returns a handle to the underlying VRT :class:`GDAL Dataset - `. - """ - return self._ds - - def copy_metadata(self, ds): - """ Copy the metadata fields and values from the given dataset. - - :param ds: a :class:`GDAL Dataset ` - """ - for key, value in ds.GetMetadata().items(): - self._ds.SetMetadataItem(key, value) - - def copy_gcps(self, ds, offset=None): - """ Copy the GCPs from the given :class:`GDAL Dataset - `, optionally offsetting them - - :param ds: a :class:`GDAL Dataset ` - :param offset: a 2-tuple of integers; the pixel offset to be applied to - any GCP copied - """ - gcps = ds.GetGCPs() - if offset: - gcps = [ - gdal.GCP( - gcp.GCPX, gcp.GCPY, gcp.GCPZ, - gcp.GCPPixel-offset[0], gcp.GCPLine-offset[1], - gcp.Info, gcp.Id - ) for gcp in gcps - ] - self._ds.SetGCPs(gcps, ds.GetGCPProjection()) - - def add_band(self, data_type=None, options=None): - """ Add a band to the VRT Dataset. - - :param data_type: the data type of the band to add. if omitted this is - determined automatically by GDAL - :param options: a list of any string options to be supplied to the new - band - """ - self._ds.AddBand(data_type, options or []) - - def _add_source_to_band(self, band_index, source): - band = self._ds.GetRasterBand(band_index) - if not band: - raise IndexError - - band.SetMetadataItem("source_0", source, "new_vrt_sources") - - def add_simple_source(self, band_index, src, src_band, - src_rect=None, dst_rect=None): - """ Add a new simple source to the VRT. - - :param band_index: the band index the source shall contribute to - :param src: either a :class:`GDAL Dataset - ` or a file path to the - source dataset - :param src_band: specify which band of the source dataset shall - contribute to the target VRT band - :param src_rect: a 4-tuple of integers in the form (offset-x, offset-y, - size-x, size-y) or a :class:`Rect - ` specifying the source - area to contribute - :param dst_rect: a 4-tuple of integers in the form (offset-x, offset-y, - size-x, size-y) or a :class:`Rect - ` specifying the target - area to contribute - """ - if isinstance(src, str): - pass - - else: - try: - src = src.GetFileList()[0] - except AttributeError: - raise ValueError("Expected string or GDAL Dataset.") - except IndexError: - raise ValueError("Supplied Dataset does not have a filename.") - - lines = [ - "", - '%s' % src, - "%d" % src_band - ] - if src_rect: - lines.append( - '' - % src_rect - ) - if dst_rect: - lines.append( - '' - % dst_rect - ) - lines.append("") - - self._add_source_to_band(band_index, "".join(lines)) diff -Nru eoxserver-0.4.0beta2/eoxserver/contrib/vsi.py eoxserver-0.3.2/eoxserver/contrib/vsi.py --- eoxserver-0.4.0beta2/eoxserver/contrib/vsi.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/contrib/vsi.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,173 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -""" This module provides Python file-object like access to VSI files. -""" - - -import os -from uuid import uuid4 - -if os.environ.get('READTHEDOCS', None) != 'True': - from eoxserver.contrib.gdal import ( - VSIFOpenL, VSIFCloseL, VSIFReadL, VSIFWriteL, VSIFSeekL, VSIFTellL, - VSIStatL, Unlink, Rename, FileFromMemBuffer - ) - - rename = Rename - - unlink = remove = Unlink - - -def open(filename, mode="r"): - """ A function mimicking the builtin function - :func:`open <__builtins__.open>` but returning a :class:`VSIFile` instead. - - :param filename: the path to the file; this might also be any VSI special - path like "/vsicurl/..." or "/vsizip/...". See the `GDAL - documentation - `_ - for reference. - :param mode: the file opening mode - :returns: a :class:`VSIFile` - """ - return VSIFile(filename) - - -class VSIFile(object): - """ File-like object interface for VSI file API. - - :param filename: the path to the file; this might also be any VSI special - path like "/vsicurl/..." or "/vsizip/...". See the `GDAL - documentation - `_ - for reference. - :param mode: the file opening mode - """ - - def __init__(self, filename, mode="r"): - self._handle = VSIFOpenL(filename, mode) - self._filename = filename - - @property - def filename(self): - """ Returns the filename referenced by this file - """ - return self._filename - - def read(self, size=None): - """ Read from the file. If no ``size`` is specified, read until the end - of the file. - - :param size: the number of bytes to be read - :returns: the bytes read as a string - """ - - if size is None: - size = self.size - self.tell() - return VSIFReadL(1, size, self._handle) - - def write(self, data): - """ Write the buffer ``data`` to the file. - - :param data: the string buffer to be written - """ - VSIFWriteL(len(data), 1, data, self._handle) - - def tell(self): - """ Return the current read/write offset of the file. - - :returns: an integer offset - """ - return VSIFTellL(self._handle) - - def seek(self, offset, whence=os.SEEK_SET): - """ Set the new read/write offset in the file. - - :param offset: the new offset - :param whence: how the offset shall be interpreted; possible options are - :const:`os.SEEK_SET`, :const:`os.SEEK_CUR` and - :const:`os.SEEK_END` - """ - VSIFSeekL(self._handle, offset, whence) - - def close(self): - """ Close the file. - """ - if self._handle is not None: - VSIFCloseL(self._handle) - self._handle = None - - @property - def closed(self): - """ Return a boolean value to indicate whether or not the file is - already closed. - """ - return (self._handle is None) - - @property - def size(self): - """ Return the size of the file in bytes - """ - stat = VSIStatL(self.filename) - return stat.size - - def __enter__(self): - return self - - def __exit__(self, etype=None, evalue=None, tb=None): - self.close() - - def __del__(self): - self.close() - - -class TemporaryVSIFile(VSIFile): - """ Subclass of VSIFile, that automatically deletes the physical file upon - deletion. - """ - - @classmethod - def from_buffer(cls, buf, mode="w", filename=None): - """ Creates a :class:`TemporaryVSIFile` from a string. - - :param buf: the supplied string - :param mode: the file opening mode - :param filename: the optional filename the file shall be stored under; - by default this is an in-memory location - """ - if not filename: - filename = "/vsimem/%s" % uuid4().hex() - FileFromMemBuffer(filename, buf) - return cls(mode) - - def close(self): - """ Close the file. This also deletes it. - """ - super(TemporaryVSIFile, self).close() - remove(self.filename) diff -Nru eoxserver-0.4.0beta2/eoxserver/core/admin.py eoxserver-0.3.2/eoxserver/core/admin.py --- eoxserver-0.4.0beta2/eoxserver/core/admin.py 1970-01-01 00:00:00.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/core/admin.py 2013-12-10 14:57:00.000000000 +0000 @@ -0,0 +1,153 @@ +#------------------------------------------------------------------------------- +# $Id$ +# +# Project: EOxServer +# Authors: Stephan Krause +# Stephan Meissl +# +#------------------------------------------------------------------------------- +# Copyright (C) 2011 EOX IT Services GmbH +# +# 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 of this Software or works derived from this 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. +#------------------------------------------------------------------------------- + +""" +This module provides the admin for the core. +""" + +from django.contrib.gis import admin +from django.contrib.admin.util import unquote +from django import template +from django.shortcuts import render_to_response + +from eoxserver.core.models import Component + +admin.site.disable_action('delete_selected') + +class ComponentAdmin(admin.ModelAdmin): + list_display = ('impl_id', 'intf_id', 'enabled') + list_editable = ('enabled', ) + readonly_fields = ('impl_id', 'intf_id') + ordering = ('impl_id', ) + search_fields = ('impl_id', 'intf_id') + + def has_add_permission(self, request): + return False + + def has_delete_permission(self, request, obj=None): + return False + +admin.site.register(Component, ComponentAdmin) + +class ConfirmationAdmin(admin.ModelAdmin): + """ + Specialized :class:`django.contrib.admin.ModelAdmin`. + The :class:`ConfirmationAdmin` allows to request a + confirmation from the user when certain changes + are made to the underlying :class:`Model`. + """ + + # template path for the confirmation form + confirmation_form_template = None + + def change_view(self, request, object_id, *args, **kwargs): + """ + This method overrides the :class:`django.contrib.admin.ModelAdmin`s + `change_view` method to hook in a confirmation page. + """ + + if request.method == 'POST' and request.POST.get('confirmation') != "done": + obj = self.get_object(request, unquote(object_id)) + + # get the changes from the model + diff = self.get_changes(request, object_id)#self.get_differences(old_values, new_values) + + # hook if the confirmation is required + msg = self.require_confirmation(diff) + + if msg: + opts = self.model._meta + context = { + 'post': request.POST.items(), + 'opts': opts, + 'root_path': self.admin_site.root_path, + 'app_label': opts.app_label, + 'original': obj, + 'object_id': object_id, + 'has_change_permission': self.has_change_permission(request, obj), + 'confirmation': msg + } + context_instance = template.RequestContext(request) + return render_to_response(self.confirmation_form_template or [ + "admin/%s/%s/change_confirmation.html" % (opts.app_label, opts.object_name.lower()), + "admin/%s/change_confirmation.html" % opts.app_label, + "admin/change_confirmation.html", + ], context, context_instance=context_instance) + + # Here we build the "normal GUI" + return super(ConfirmationAdmin, self).change_view(request, object_id, *args, **kwargs) + + def get_new_object(self, request, instance): + """ + Get the changed model with the data from the form. + Convenience method. + """ + ModelForm = self.get_form(request, instance) + form = ModelForm(request.POST, request.FILES, instance=instance) + + if form.is_valid(): + return self.save_form(request, form, change=True) + else: + return instance + + def get_changes(self, request, object_id): + obj = self.get_object(request, unquote(object_id)) + old_values = dict([(field.name, field.value_from_object(obj)) for field in obj._meta.fields]) + new_obj = self.get_new_object(request, obj) + new_values = dict([(field.name, field.value_from_object(obj)) for field in new_obj._meta.fields]) + + return self.get_differences(old_values, new_values) + + def get_differences(self, first, second): + """ + Convenience method to get the differences + between two dict-like objects. + """ + diff = {} + for key, value2 in second.iteritems(): + value1 = first.get(key) + if value1 != value2: + diff[key] = (value1, value2) + return diff + + def require_confirmation(self, diff): + """ + Hook to check if a confirmation is required. + Override this method in subclasses to enable + or disable the confirmation. + * ``diff`` is a dictionary where the keys are + the names of the changed fields and the values + are tuples with two entries: the old and the + new value. + + To enable the confirmation return a string message + which is shown to the user. + Otherwise return False. + """ + return False diff -Nru eoxserver-0.4.0beta2/eoxserver/core/commands/create_instance.py eoxserver-0.3.2/eoxserver/core/commands/create_instance.py --- eoxserver-0.4.0beta2/eoxserver/core/commands/create_instance.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/core/commands/create_instance.py 2013-12-10 14:57:00.000000000 +0000 @@ -1,5 +1,6 @@ #!/usr/bin/env python #------------------------------------------------------------------------------- +# $Id$ # # Project: EOxServer # Authors: Fabian Schindler @@ -12,8 +13,8 @@ # 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 +# 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 @@ -29,14 +30,13 @@ #------------------------------------------------------------------------------- """ -Create a new EOxServer instance. This instance will create a root directory +Create a new EOxServer instance. This instance will create a root directory with the instance name in the given (optional) directory. """ -import os -import sys +import shutil +import os, sys from optparse import make_option -from ctypes.util import find_library from django.core.management import call_command from django.core.management.base import CommandError @@ -47,93 +47,64 @@ class Command(EOxServerAdminCommand): option_list = EOxServerAdminCommand.option_list + ( - make_option('--init_spatialite', '--init-spatialite', - action='store_true', help='Flag to initialize the sqlite database.' + make_option('--init_spatialite', action='store_true', + help='Flag to initialize the sqlite database.' ), ) - - args = "INSTANCE_ID [Optional destination directory] [--init-spatialite]" + + args = "INSTANCE_ID [Optional destination directory]" help = ("Creates a new EOxServer instance with all necessary files and " - "folder structure. Optionally, a SQLite database is initiated") - + "folder structure.") + def handle(self, instance_id=None, target=None, *args, **options): if instance_id is None: raise CommandError("Instance ID not given.") - + # Locate instance template - instance_template_dir = os.path.join( - os.path.dirname(eoxserver.__file__), "instance_template" - ) + instance_template_dir = os.path.join(os.path.dirname(eoxserver.__file__), + "instance_template") if not os.path.isfile(os.path.join(instance_template_dir, "manage.py")): - instance_template_dir = os.path.join(sys.prefix, + instance_template_dir = os.path.join(sys.prefix, "eoxserver/instance_template") - if not os.path.isfile(os.path.join(instance_template_dir, + if not os.path.isfile(os.path.join(instance_template_dir, "manage.py")): sys.exit("Error: EOxServer instance template not found.") - + # Add template and extension to options - options['extensions'] = ["conf", "py"] + options['extensions'] = ["conf","py"] options['template'] = instance_template_dir - + # create the initial django folder structure print("Initializing django project folder.") call_command("startproject", instance_id, target, **options) - + if options.get('init_spatialite'): - self._init_spatialite(instance_id, target) - - def _init_spatialite(self, instance_id, target): - # initialize the spatialite database file - if target is None: - dst_data_dir = os.path.join(instance_id, instance_id, "data") - else: - dst_data_dir = os.path.join(os.path.abspath(target), - instance_id, instance_id, "data") - - os.chdir(dst_data_dir) - db_name = "config.sqlite" - print("Setting up initial database.") - try: - from pyspatialite import dbapi2 as db - conn = db.connect(db_name) - print("Using pyspatialite.") - except ImportError: + # initialize the spatialite database file + if target is None: + dst_data_dir = os.path.join(instance_id, instance_id, "data") + else: + dst_data_dir = os.path.join(os.path.abspath(target), + instance_id, instance_id, "data") + + os.chdir(dst_data_dir) + db_name = "config.sqlite" + print("Setting up initial database.") try: - from pysqlite2 import dbapi2 as db - print("Using pysqlite.") + from pyspatialite import dbapi2 as db + conn = db.connect(db_name) + rs = conn.execute('SELECT spatialite_version()') + rs = rs.fetchone()[0].split(".") + if (int(rs[0]), int(rs[1])) >= (2, 4): + print("SpatiaLite found, initializing using 'InitSpatialMetadata()'.") + conn.execute("SELECT InitSpatialMetadata()") + else: + print("SpatiaLite version <2.4 found, trying to initialize using 'init_spatialite-2.3.sql'.") + init_sql_path = "init_spatialite-2.3.sql" + with open(init_sql_path, 'r') as init_sql_file: + conn.executescript(init_sql_file.read()) + conn.commit() + conn.close() except ImportError: - from sqlite3 import dbapi2 as db - print("Using sqlite3.") - - conn = db.connect(db_name) - try: - conn.enable_load_extension(True) - except: - raise Exception( - "SQLite API does not allow loading of extensions." - ) - - spatialite_lib = find_library('spatialite') - try: - print("Trying to load extension module '%s'." % spatialite_lib) - conn.execute("SELECT load_extension('%s')" % (spatialite_lib,)) - except Exception, msg: - raise Exception( - 'Unable to load the SpatiaLite library extension ' - '"%s" because: %s' % (spatialite_lib, msg) - ) - - rs = conn.execute('SELECT spatialite_version()') - rs = rs.fetchone()[0].split(".") - if (int(rs[0]), int(rs[1])) >= (2, 4): - print("SpatiaLite found, initializing using " - "'InitSpatialMetadata()'.") - conn.execute("SELECT InitSpatialMetadata()") - else: - print("SpatiaLite version <2.4 found, trying to " - "initialize using 'init_spatialite-2.3.sql'.") - init_sql_path = "init_spatialite-2.3.sql" - with open(init_sql_path, 'r') as init_sql_file: - conn.executescript(init_sql_file.read()) - conn.commit() - conn.close() + print("SpatiaLite not found, trying to initialize using 'init_spatialite-2.3.sql'.") + init_sql_path = "init_spatialite-2.3.sql" + os.system("spatialite %s < %s" % (db_name, init_sql_path)) diff -Nru eoxserver-0.4.0beta2/eoxserver/core/commands/__init__.py eoxserver-0.3.2/eoxserver/core/commands/__init__.py --- eoxserver-0.4.0beta2/eoxserver/core/commands/__init__.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/core/commands/__init__.py 2013-12-10 14:57:00.000000000 +0000 @@ -1,4 +1,5 @@ #------------------------------------------------------------------------------- +# $Id$ # # Project: EOxServer # Authors: Stephan Krause @@ -11,8 +12,8 @@ # 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 +# 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 diff -Nru eoxserver-0.4.0beta2/eoxserver/core/component.py eoxserver-0.3.2/eoxserver/core/component.py --- eoxserver-0.4.0beta2/eoxserver/core/component.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/core/component.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,242 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2003-2011 Edgewall Software -# Copyright (C) 2003-2004 Jonas Borgström -# Copyright (C) 2004-2005 Christopher Lenz -# All rights reserved. -# -# This software is licensed as described in the file COPYING, which -# you should have received as part of this distribution. The terms -# are also available at http://trac.edgewall.org/wiki/TracLicense. -# -# This software consists of voluntary contributions made by many -# individuals. For the exact contribution history, see the revision -# history and logs, available at http://trac.edgewall.org/log/. -# -# Author: Jonas Borgström -# Christopher Lenz - -__all__ = ['Component', 'ExtensionPoint', 'UniqueExtensionPoint', 'implements', - 'Interface', 'ComponentException', 'ComponentManager'] - - -def N_(string): - """No-op translation marker, inlined here to avoid importing from - `trac.util`. - """ - return string - - -class ComponentException(Exception): - pass - - -class Interface(object): - """Marker base class for extension point interfaces.""" - - -class ExtensionPoint(property): - """Marker class for extension points in components.""" - - def __init__(self, interface): - """Create the extension point. - - :param interface: the `Interface` subclass that defines the - protocol for the extension point - """ - property.__init__(self, self.extensions) - self.interface = interface - self.__doc__ = ("List of components that implement :class:`%s.%s`" % - (self.interface.__module__, self.interface.__name__)) - - def extensions(self, component): - """Return a list of components that declare to implement the - extension point interface. - """ - classes = ComponentMeta._registry.get(self.interface, ()) - components = [component.compmgr[cls] for cls in classes] - return [c for c in components if c] - - def __repr__(self): - """Return a textual representation of the extension point.""" - return '<%s %s>' % (self.__class__.__name__, self.interface.__name__) - - -class UniqueExtensionPoint(ExtensionPoint): - """Marker class for unique extension points in components.""" - - def extensions(self, component): - """Return the single component that is implementing the interaface. If - none is found, or more than one, an exception is raised. - """ - extensions = super(UniqueExtensionPoint, self).extensions(component) - length = len(extensions) - if length == 1: - return extensions[0] - elif length > 1: - raise ComponentException( - "More than one implementation was found for this extension " - "point." - ) - else: - raise ComponentException( - "No implementation was found for this extension point." - ) - - -class ComponentMeta(type): - """Meta class for components. - - Takes care of component and extension point registration. - """ - _components = [] - _registry = {} - - def __new__(mcs, name, bases, d): - """Create the component class.""" - - new_class = type.__new__(mcs, name, bases, d) - if name == 'Component': - # Don't put the Component base class in the registry - return new_class - - if d.get('abstract'): - # Don't put abstract component classes in the registry - return new_class - - ComponentMeta._components.append(new_class) - registry = ComponentMeta._registry - for cls in new_class.__mro__: - for interface in cls.__dict__.get('_implements', ()): - classes = registry.setdefault(interface, []) - if new_class not in classes: - classes.append(new_class) - - return new_class - - def __call__(cls, *args, **kwargs): - """Return an existing instance of the component if it has - already been activated, otherwise create a new instance. - """ - # If this component is also the component manager, just invoke that - if issubclass(cls, ComponentManager): - self = cls.__new__(cls) - self.compmgr = self - self.__init__(*args, **kwargs) - return self - - # The normal case where the component is not also the component manager - compmgr = args[0] - self = compmgr.components.get(cls) - # Note that this check is racy, we intentionally don't use a - # lock in order to keep things simple and avoid the risk of - # deadlocks, as the impact of having temporarily two (or more) - # instances for a given `cls` is negligible. - if self is None: - self = cls.__new__(cls) - self.compmgr = compmgr - compmgr.component_activated(self) - self.__init__() - # Only register the instance once it is fully initialized (#9418) - compmgr.components[cls] = self - return self - - -class Component(object): - """Base class for components. - - Every component can declare what extension points it provides, as - well as what extension points of other components it extends. - """ - __metaclass__ = ComponentMeta - - @staticmethod - def implements(*interfaces): - """Can be used in the class definition of `Component` - subclasses to declare the extension points that are extended. - """ - import sys - - frame = sys._getframe(1) - locals_ = frame.f_locals - - # Some sanity checks - assert locals_ is not frame.f_globals and '__module__' in locals_, \ - 'implements() can only be used in a class definition' - - locals_.setdefault('_implements', []).extend(interfaces) - - -implements = Component.implements - - -class ComponentManager(object): - """The component manager keeps a pool of active components.""" - - def __init__(self): - """Initialize the component manager.""" - self.components = {} - self.enabled = {} - if isinstance(self, Component): - self.components[self.__class__] = self - - def __contains__(self, cls): - """Return wether the given class is in the list of active - components.""" - return cls in self.components - - def __getitem__(self, cls): - """Activate the component instance for the given class, or - return the existing instance if the component has already been - activated. - - Note that `ComponentManager` components can't be activated - that way. - """ - if not self.is_enabled(cls): - return None - component = self.components.get(cls) - if not component and not issubclass(cls, ComponentManager): - if cls not in ComponentMeta._components: - raise ComponentException( - 'Component "%s" not registered' % cls.__name__ - ) - try: - component = cls(self) - except TypeError, e: - raise ComponentException( - 'Unable to instantiate component %r (%s)' % (cls, e) - ) - return component - - def is_enabled(self, cls): - """Return whether the given component class is enabled.""" - if cls not in self.enabled: - self.enabled[cls] = self.is_component_enabled(cls) - return self.enabled[cls] - - def disable_component(self, component): - """Force a component to be disabled. - - :param component: can be a class or an instance. - """ - if not isinstance(component, type): - component = component.__class__ - self.enabled[component] = False - self.components[component] = None - - def component_activated(self, component): - """Can be overridden by sub-classes so that special - initialization for components can be provided. - """ - - def is_component_enabled(self, cls): - """Can be overridden by sub-classes to veto the activation of - a component. - - If this method returns `False`, the component was disabled - explicitly. If it returns `None`, the component was neither - enabled nor disabled explicitly. In both cases, the component - with the given class will not be available. - """ - return True diff -Nru eoxserver-0.4.0beta2/eoxserver/core/config.py eoxserver-0.3.2/eoxserver/core/config.py --- eoxserver-0.4.0beta2/eoxserver/core/config.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/core/config.py 2013-12-10 14:57:00.000000000 +0000 @@ -1,4 +1,5 @@ #------------------------------------------------------------------------------- +# $Id$ # # Project: EOxServer # Authors: Stephan Krause @@ -10,8 +11,8 @@ # 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 +# 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 @@ -28,63 +29,152 @@ """ This module provides an implementation of a system configuration that relies -on different configuration files. +on different configuration files. It is used by :mod:`eoxserver.core.system` to +store the current system configuration. """ import imp -from os.path import join, getmtime +import os.path from sys import prefix -import threading from ConfigParser import RawConfigParser -import logging -from time import time from django.conf import settings +from eoxserver.core.exceptions import InternalError -config_lock = threading.RLock() -logger = logging.getLogger(__name__) - -# configuration singleton -_cached_config = None -_last_access_time = None - - -def get_eoxserver_config(): - """ Returns the EOxServer config as a :class:`ConfigParser.RawConfigParser` +class Config(object): """ - with config_lock: - if not _cached_config or \ - getmtime(get_instance_config_path()) > _last_access_time: - reload_eoxserver_config() - - return _cached_config - - -def reload_eoxserver_config(): - """ Triggers the loading or reloading of the EOxServer config as a - :class:`ConfigParser.RawConfigParser`. + The :class:`Config` class represents a system configuration. Internally, + it relies on two configuration files: + + * the default configuration file (``eoxserver/conf/default.conf``) + * the instance configuration file (``conf/eoxserver.conf`` in the instance + directory) + + Configuration values are read from these files. """ - global _cached_config, _last_access_time - _, eoxs_path, _ = imp.find_module("eoxserver") - paths = [ - join(eoxs_path, "conf", "default.conf"), - join(prefix, "eoxserver/conf/default.conf"), - get_instance_config_path() - ] - - logger.info( - "%soading the EOxServer configuration. Using paths: %s." - % ("Rel" if _cached_config else "L", ", ".join(paths)) - ) - - with config_lock: - _cached_config = RawConfigParser() - _cached_config.read(paths) - _last_access_time = time() + + def __init__(self): + self.__eoxs_path = None + self.__default_conf = None + self.__instance_conf = None + + eoxs_path = self.getEOxSPath() + + default_conf_path = os.path.join(eoxs_path, "conf", "default.conf") + default_conf_path_alt = os.path.join(prefix, "eoxserver/conf/default.conf") + + if os.path.exists(default_conf_path): + self.__default_conf = ConfigFile(default_conf_path) + elif os.path.exists(default_conf_path_alt): + self.__default_conf = ConfigFile(default_conf_path_alt) + else: + raise InternalError("Improperly installed: could not find default configuration file.") + + instance_conf_path = os.path.join(settings.PROJECT_DIR, "conf", "eoxserver.conf") + + if os.path.exists(instance_conf_path): + self.__instance_conf = ConfigFile(instance_conf_path) + else: + raise InternalError("Improperly configured: could not find instance configuration file.") + + def getConfigValue(self, section, key): + """ + Returns a configuration parameter value. The ``section`` and ``key`` + arguments denote the parameter to be looked up. The value is searched + for first in the instance configuration file; if it is not found there + the value is read from the default configuration file. + """ + + inst_value = self.getInstanceConfigValue(section, key) + if inst_value is None: + return self.getDefaultConfigValue(section, key) + else: + return inst_value + + def getDefaultConfigValue(self, section, key): + """ + Returns a configuration parameter default value (read from the default + configuration file). The ``section`` and ``key`` arguments denote the + parameter to be looked up. + """ + + return self.__default_conf.get(section, key) + + def getInstanceConfigValue(self, section, key): + """ + Returns a configuration parameter value as defined in the instance + configuration file, or ``None`` if it is not found there. The + ``section`` and ``key`` arguments denote the parameter to be looked up. + """ + + return self.__instance_conf.get(section, key) + + def getConcurringConfigValues(self, section, key): + """ + Returns a dictionary od concurring configuration parameter values. It + may have two entries + + * ``default``: the default configuration parameter value + * ``instance``: the instance configuration value + + If there is no configuration parameter value defined in the respective + configuration file, the entry is omitted. + + The ``section`` and ``key`` arguments denote the parameter to be looked + up. + """ + + concurring_values = {} + + def_value = self.getDefaultConfigValue(section, key) + if def_value is not None: + concurring_values["default"] = def_value + + instance_value = self.getInstanceConfigValue(section, key) + if instance_value is not None: + concurring_values["instance"] = instance_value + + return concurring_values + + def getEOxSPath(self): + """ + Returns the path to the EOxServer installation (not to the instance). + """ + + if self.__eoxs_path is None: + try: + _, eoxs_path, _ = imp.find_module("eoxserver") + except ImportError: + raise InternalError("Something very strange is happening: cannot find 'eoxserver' module.") + self.__eoxs_path = eoxs_path + + return self.__eoxs_path - -def get_instance_config_path(): - """ Convenience function to get the path to the instance config. +class ConfigFile(object): + """ + This is a wrapper for a configuration file. It is based on the Python + builtin :mod:`ConfigParser` module. """ - return join(settings.PROJECT_DIR, "conf", "eoxserver.conf") + + def __init__(self, config_filename): + self.config_filename = config_filename + + self._parser = RawConfigParser() + self._parser.read(config_filename) + + def get(self, section, key): + """ + Return the configuration parameter value, or ``None`` if it is not + defined. + + The ``section`` argument denotes the section of the configuration file + where to look for the parameter named ``key``. See the + :mod:`ConfigParser` module documentation for details on the config file + syntax. + """ + + if self._parser.has_option(section, key): + return self._parser.get(section, key) + else: + return None diff -Nru eoxserver-0.4.0beta2/eoxserver/core/decoders/base.py eoxserver-0.3.2/eoxserver/core/decoders/base.py --- eoxserver-0.4.0beta2/eoxserver/core/decoders/base.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/core/decoders/base.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,106 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2014 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -""" This module provides base functionality for any other decoder class. -""" - - -from eoxserver.core.decoders import ( - ZERO_OR_ONE, ONE_OR_MORE, ANY, SINGLE_VALUES, WrongMultiplicityException, - InvalidParameterException, MissingParameterException, - MissingParameterMultipleException -) - - -class BaseParameter(property): - """ Abstract base class for XML, KVP or any other kind of parameter. - """ - - def __init__(self, type=None, num=1, default=None): - super(BaseParameter, self).__init__(self.fget) - self.type = type or str - self.num = num - self.default = default - - def select(self, decoder): - """ Interface method. - """ - raise NotImplementedError - - @property - def locator(self): - return "" - - def fget(self, decoder): - """ Property getter function. - """ - - results = self.select(decoder) - count = len(results) - - locator = self.locator - multiple = self.num not in SINGLE_VALUES - - # check the correct count of the result - if not multiple and count > 1: - raise WrongMultiplicityException(locator, "at most one", count) - - elif self.num == 1 and count == 0: - raise MissingParameterException(locator) - - elif self.num == ONE_OR_MORE and count == 0: - raise MissingParameterMultipleException(locator) - - elif isinstance(self.num, int) and count != self.num: - raise WrongMultiplicityException(locator, self.num, count) - - # parse the value/values, or return the defaults - if multiple: - if count == 0 and self.num == ANY and self.default is not None: - return self.default - - try: - return map(self.type, results) - except Exception, e: - # let some more sophisticated exceptions pass - if hasattr(e, "locator") or hasattr(e, "code"): - raise - raise InvalidParameterException(str(e), locator) - - elif self.num == ZERO_OR_ONE and count == 0: - return self.default - - elif self.type: - try: - return self.type(results[0]) - except Exception, e: - # let some more sophisticated exceptions pass - if hasattr(e, "locator") or hasattr(e, "code"): - raise - raise InvalidParameterException(str(e), locator) - - return results[0] diff -Nru eoxserver-0.4.0beta2/eoxserver/core/decoders/config.py eoxserver-0.3.2/eoxserver/core/decoders/config.py --- eoxserver-0.4.0beta2/eoxserver/core/decoders/config.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/core/decoders/config.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,176 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -""" This module contains facilities to help decoding configuration files. -It relies on the :mod:`ConfigParser` module for actually reading the file. -""" - -import sys -from ConfigParser import NoOptionError, NoSectionError - - -def section(name): - """ Helper to set the section of a :class:`Reader`. - """ - frame = sys._getframe(1) - locals_ = frame.f_locals - - # Some sanity checks - assert locals_ is not frame.f_globals and '__module__' in locals_, \ - 'implements() can only be used in a class definition' - - locals_["section"] = name - - -class Option(property): - """ The :class:`Option` is used as a :class:`property` for :class:`Reader` - subclasses. - - :param key: the lookup key; defaults to the property name of the - :class:`Reader`. - :param type: the type to parse the raw value; by default the raw - string is returned - :param separator: the separator for list options; by default no list - is assumed - :param required: if ``True`` raise an error if the option does not - exist - :param default: the default value - :param section: override the section for this option - """ - - def __init__(self, key=None, type=None, separator=None, required=False, - default=None, section=None, doc=None): - - super(Option, self).__init__(self.fget) - - self.key = key # needs to be set by the reader metaclass - self.type = type - self.separator = separator - self.required = required - self.default = default - - if section is None: - frame = sys._getframe(1) - section = frame.f_locals.get("section") - - self.section = section - - def fget(self, reader): - section = self.section or reader.section - try: - if self.type is bool: - raw_value = reader._config.getboolean(section, self.key) - else: - raw_value = reader._config.get(section, self.key) - except (NoOptionError, NoSectionError), e: - if not self.required: - return self.default - raise e - - if self.separator is not None: - return map(self.type, raw_value.split(self.separator)) - - elif self.type: - return self.type(raw_value) - - else: - return raw_value - - def check(self, reader): - # TODO: perform checking of config - # - required option? - # - can parse type? - pass - - -class ReaderMetaclass(type): - def __init__(cls, name, bases, dct): - for key, value in dct.items(): - if isinstance(value, Option) and value.key is None: - value.key = key - - value.__doc__ = "%s.%s" % (value.section, value.key) - - super(ReaderMetaclass, cls).__init__(name, bases, dct) - - -class Reader(object): - """ Base class for config readers. - - :param config: an instance of :class:`ConfigParser.RawConfigParser` - - Readers should be used as such: - :: - - from ConfigParser import RawConfigParser - from StringIO import StringIO - from textwrap import dedent - from eoxserver.core.decoders import config - - class ExampleReader(config.Reader): - section = "example_section" - string_opt = config.Option() - string_list_opt = config.Option(separator=",") - integer_opt = config.Option(type=int) - - section = "other_section" - mandatory_opt = config.Option(required=True) - optional_opt = config.Option(default="some_default") - - special_section_opt = config.Option(section="special_section") - - f = StringIO(dedent(''' - [example_section] - string_opt = mystring - string_list_opt = my,string,list - integer_opt = 123456 - [other_section] - mandatory_opt = mandatory_value - # optional_opt = no value - - [special_section] - special_section_opt = special_value - ''')) - - parser = RawConfigParser() - parser.readfp(f) - reader = ExampleReader(parser) - - print reader.string_opt - print reader.string_list_opt - print reader.integer_opt - print reader.mandatory_opt - print reader.optional_opt - ... - """ - - __metaclass__ = ReaderMetaclass - - section = None - - def __init__(self, config): - self._config = config diff -Nru eoxserver-0.4.0beta2/eoxserver/core/decoders/__init__.py eoxserver-0.3.2/eoxserver/core/decoders/__init__.py --- eoxserver-0.4.0beta2/eoxserver/core/decoders/__init__.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/core/decoders/__init__.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,268 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -import sys - - -ZERO_OR_ONE = "?" -ONE_OR_MORE = "+" -ANY = "*" - -SINGLE_VALUES = (ZERO_OR_ONE, 1) - - -class DecodingException(Exception): - """ Base Exception class to be thrown whenever a decoding failed. - """ - - def __init__(self, message, locator=None): - super(DecodingException, self).__init__(message) - self.locator = locator - - -class WrongMultiplicityException(DecodingException): - """ Decoding Exception to be thrown when the multiplicity of a parameter did - not match the expected count. - """ - - code = "InvalidParameterValue" - - def __init__(self, locator, expected, result): - super(WrongMultiplicityException, self).__init__( - "Parameter '%s': expected %s got %d" % (locator, expected, result), - locator - ) - - -class MissingParameterException(DecodingException): - """ Exception to be thrown, when a decoder could not read one parameter, - where exactly one was required. - """ - code = "MissingParameterValue" - - def __init__(self, locator): - super(MissingParameterException, self).__init__( - "Missing required parameter '%s'" % locator, locator - ) - - -class MissingParameterMultipleException(DecodingException): - """ Exception to be thrown, when a decoder could not read at least one - parameter, where one ore more were required. - """ - code = "MissingParameterValue" - - def __init__(self, locator): - super(MissingParameterMultipleException, self).__init__( - "Missing at least one required parameter '%s'" % locator, locator - ) - - -class NoChoiceResultException(DecodingException): - pass - - -class ExclusiveException(DecodingException): - pass - -# NOTE: The following exceptions may get propagated as OWS exceptions -# therefore it is necessary to set the proper OWS exception code. - - -class InvalidParameterException(DecodingException): - code = "InvalidParameterValue" - - -# Compound fields - - -class Choice(object): - """ Tries all given choices until one does return something. - """ - - def __init__(self, *choices): - self.choices = choices - - def __get__(self, decoder, decoder_class=None): - for choice in self.choices: - try: - return choice.__get__(decoder, decoder_class) - except: - continue - raise NoChoiceResultException - - -class Exclusive(object): - """ For mutual exclusive Parameters. - """ - - def __init__(self, *choices): - self.choices = choices - - def __get__(self, decoder, decoder_class=None): - result = None - num = 0 - for choice in self.choices: - try: - result = choice.__get__(decoder, decoder_class) - num += 1 - except: - continue - - if num != 1: - raise ExclusiveException - - return result - - -class Concatenate(object): - """ Helper to concatenate the results of all sub-parameters to one. - """ - def __init__(self, *choices, **kwargs): - self.choices = choices - self.allow_errors = kwargs.get("allow_errors", True) - - def __get__(self, decoder, decoder_class=None): - result = [] - for choice in self.choices: - try: - value = choice.__get__(decoder, decoder_class) - if isinstance(value, (list, tuple)): - result.extend(value) - else: - result.append(value) - except Exception: - if self.allow_errors: - # swallow exception - continue - - exc_info = sys.exc_info() - raise exc_info[0], exc_info[1], exc_info[2] - - return result - - -# Type conversion helpers - - -class typelist(object): - """ Helper for XMLDecoder schemas that expect a string that represents a - list of a type separated by some separator. - """ - - def __init__(self, typ=None, separator=" "): - self.typ = typ - self.separator = separator - - def __call__(self, value): - return map(self.typ, value.split(self.separator)) - - -class fixed(object): - """ Helper for parameters that are expected to be have a fixed value and - raises a ValueError if not. - """ - - def __init__(self, value, case_sensitive=True): - self.value = value if case_sensitive else value.lower() - self.case_sensitive = case_sensitive - - def __call__(self, value): - compare = value if self.case_sensitive else value.lower() - if self.value != compare: - raise ValueError("Value mismatch, expected %s, got %s." % - (self.value, value) - ) - - return value - - -class enum(object): - """ Helper for parameters that are expected to be in a certain enumeration. - A ValueError is raised if not. - """ - - def __init__(self, values, case_sensitive=True): - self.values = values - self.compare_values = values if case_sensitive else map(lower, values) - self.case_sensitive = case_sensitive - - def __call__(self, value): - compare = value if self.case_sensitive else value.lower() - if compare not in self.compare_values: - raise ValueError("Unexpected value '%s'. Expected one of: %s." % - (value, ", ".join(map(lambda s: "'%s'" % s, self.values))) - ) - - return value - - -def lower(value): - """ Functor to return a lower-case string. - """ - return value.lower() - - -def upper(value): - """ Functor to return a upper-case string. - """ - return value.upper() - - -def strip(value): - """ Functor to return a whitespace stripped string. - """ - return value.strip() - - -class value_range(object): - """ Helper to assert that a given parameter is within a specified range. - """ - - def __init__(self, min, max, type=float): - self._min = min - self._max = max - self._type = type - - def __call__(self, raw): - value = self._type(raw) - if value < self._min or value > self._max: - raise ValueError( - "Given value '%s' exceeds expected bounds (%s, %s)" - % (value, self._min, self._max) - ) - return value - - -def boolean(raw): - """ Functor to convert "true"/"false" to a boolean. - """ - raw = raw.lower() - if not raw in ("true", "false"): - raise ValueError("Could not parse a boolean value from '%s'." % raw) - return raw == "true" diff -Nru eoxserver-0.4.0beta2/eoxserver/core/decoders/kvp.py eoxserver-0.3.2/eoxserver/core/decoders/kvp.py --- eoxserver-0.4.0beta2/eoxserver/core/decoders/kvp.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/core/decoders/kvp.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,163 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -""" This module contains facilities to help decoding KVP strings. -""" - -from cgi import parse_qs - -from django.http import QueryDict - -from eoxserver.core.decoders.base import BaseParameter - - -class Parameter(BaseParameter): - """ Parameter for KVP values. - - :param key: the lookup key; defaults to the property name of the - :class:`Decoder` - :param type: the type to parse the raw value; by default the raw - string is returned - :param num: defines how many times the key can be present; use any - numeric value to set it to a fixed count, "*" for any - number, "?" for zero or one time or "+" for one or more - times - :param default: the default value - :param locator: override the locator in case of exceptions - """ - - key = None - - def __init__(self, key=None, type=None, num=1, default=None, locator=None): - super(Parameter, self).__init__(type, num, default) - self.key = key.lower() if key is not None else None - self._locator = locator - - def select(self, decoder, decoder_class=None): - return decoder._query_dict.get(self.key, []) - - @property - def locator(self): - return self._locator or self.key - - -class MultiParameter(Parameter): - """ Class for selecting different KVP parameters at once. - - :param selector: a function to determine if a key is used for the multi - parameter selection - :param num: defines how many times the key can be present; use any - numeric value to set it to a fixed count, "*" for any - number, "?" for zero or one time or "+" for one or more - times - :param default: the default value - :param locator: override the locator in case of exceptions - """ - - def __init__(self, selector, num=1, default=None, locator=None): - super(MultiParameter, self).__init__( - "", lambda s: s, num, default, locator - ) - self.key = selector - - def select(self, decoder): - result = [] - for key, values in decoder._query_dict.items(): - if self.key(key): - result.append((key, values)) - - return result - - -class DecoderMetaclass(type): - """ Metaclass for KVP Decoders to allow easy parameter declaration. - """ - def __init__(cls, name, bases, dct): - # set the "key" attribute of any parameter to the parameters name - # if no other key was specified. - for key, value in dct.items(): - if isinstance(value, Parameter) and value.key is None: - value.key = key.lower() - - super(DecoderMetaclass, cls).__init__(name, bases, dct) - - -class Decoder(object): - """ Base class for KVP decoders. - - :param params: an instance of either :class:`dict`, - :class:`django.http.QueryDict` or :class:`basestring` (which - will be parsed using :func:`cgi.parse_qs`) - - Decoders should be used as such: - :: - - from eoxserver.core.decoders import kvp - from eoxserver.core.decoders import typelist - - class ExampleDecoder(kvp.Decoder): - mandatory_param = kvp.Parameter(num=1) - list_param = kvp.Parameter(type=typelist(separator=",")) - multiple_param = kvp.Parameter("multi", num="+") - optional_param = kvp.Parameter(num="?", default="default_value") - - decoder = ExampleDecoder( - "mandatory_param=value" - "&list_param=a,b,c" - "&multi=a&multi=b&multi=c" - ) - - print decoder.mandatory_param - print decoder.list_param - print decoder.multiple_param - print decoder.optional_param - """ - - __metaclass__ = DecoderMetaclass - - def __init__(self, params): - query_dict = {} - if isinstance(params, QueryDict): - for key, values in params.lists(): - query_dict[key.lower()] = values - - elif isinstance(params, basestring): - tmp = parse_qs(params) - for key, values in tmp.items(): - query_dict[key.lower()] = values - - elif isinstance(params, dict): - for key, value in params.items(): - query_dict[key.lower()] = (value,) - - else: - raise ValueError( - "Decoder input '%s' not supported." % type(params).__name__ - ) - - self.kvp = params - self._query_dict = query_dict diff -Nru eoxserver-0.4.0beta2/eoxserver/core/decoders/xml.py eoxserver-0.3.2/eoxserver/core/decoders/xml.py --- eoxserver-0.4.0beta2/eoxserver/core/decoders/xml.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/core/decoders/xml.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,130 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -""" This module contains facilities to help decoding XML structures. -""" - -from lxml import etree -from eoxserver.core.decoders.base import BaseParameter - - -class Parameter(BaseParameter): - """ Parameter for XML values. - - :param selector: the node selector; if a string is passed it is - interpreted as an XPath expression, a callable will be - called with the root of the element tree and shall - yield any number of node - :param type: the type to parse the raw value; by default the raw - string is returned - :param num: defines how many times the key can be present; use any - numeric value to set it to a fixed count, "*" for any - number, "?" for zero or one time or "+" for one or more - times - :param default: the default value - :param namespaces: any namespace necessary for the XPath expression; - defaults to the :class:`Decoder` namespaces. - :param locator: override the locator in case of exceptions - """ - - def __init__(self, selector, type=None, num=1, default=None, - namespaces=None, locator=None): - super(Parameter, self).__init__(type, num, default) - self.selector = selector - self.namespaces = namespaces - self._locator = locator - - def select(self, decoder): - # prepare the XPath selector if necessary - if isinstance(self.selector, basestring): - namespaces = self.namespaces or decoder.namespaces - self.selector = etree.XPath(self.selector, namespaces=namespaces) - - results = self.selector(decoder._tree) - if isinstance(results, basestring): - results = [results] - - return results - - @property - def locator(self): - return self._locator or str(self.selector) - - -class Decoder(object): - """ Base class for XML Decoders. - - :param params: an instance of either :class:`lxml.etree.ElementTree`, - or :class:`basestring` (which will be parsed using - :func:`lxml.etree.fromstring`) - - Decoders should be used as such: - :: - - from eoxserver.core.decoders import xml - from eoxserver.core.decoders import typelist - - class ExampleDecoder(xml.Decoder): - namespaces = {"myns": "http://myns.org"} - single = xml.Parameter("myns:single/text()", num=1) - items = xml.Parameter("myns:collection/myns:item/text()", num="+") - attr_a = xml.Parameter("myns:object/@attrA", num="?") - attr_b = xml.Parameter("myns:object/@attrB", num="?", default="x") - - - decoder = ExampleDecoder(''' - - value - - a - b - c - - - - ''') - - print decoder.single - print decoder.items - print decoder.attr_a - print decoder.attr_b - """ - - namespaces = {} # must be overriden if the XPath expressions use namespaces - - def __init__(self, tree): - if isinstance(tree, basestring): - try: - tree = etree.fromstring(tree) - except etree.XMLSyntaxError, exc: - # NOTE: lxml.etree.XMLSyntaxError is incorretly identified as - # an OWS exception by the exception handler leading - # to a wrong OWS error response. This exception thus - # must be cought and replaced by another exception - # of a different type. - raise ValueError("Malformed XML document. Error was %s" % exc) - self._tree = tree diff -Nru eoxserver-0.4.0beta2/eoxserver/core/exceptions.py eoxserver-0.3.2/eoxserver/core/exceptions.py --- eoxserver-0.4.0beta2/eoxserver/core/exceptions.py 1970-01-01 00:00:00.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/core/exceptions.py 2013-12-10 14:57:00.000000000 +0000 @@ -0,0 +1,254 @@ +#------------------------------------------------------------------------------- +# $Id$ +# +# Project: EOxServer +# Authors: Stephan Krause +# Stephan Meissl +# +#------------------------------------------------------------------------------- +# Copyright (C) 2011 EOX IT Services GmbH +# +# 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 of this Software or works derived from this 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. +#------------------------------------------------------------------------------- + +""" +This module contains exception classes used throughout EOxServer. +""" + + +class EOxSException(Exception): + """ + Base class for EOxServer exceptions. Expects the error message as its + single constructor argument. + """ + + def __init__(self, msg): + super(EOxSException, self).__init__(msg) + + self.msg = msg + +class InternalError(EOxSException): + """ + :exc:`InternalError` shall be raised by EOxServer modules whenever they + detect a fault that stems from errors in the EOxServer implementation. It + shall NOT be used for error conditions that are caused by incorrect or + invalid user or service input or that originate from the individual system + configuration. + + In a web service environment, an :exc:`InternalError` should lead to the + server responding with a HTTP Status of ``500 INTERNAL SERVER ERROR``. + """ + + pass + +class ConfigError(EOxSException): + """ + This exception shall be raised if the system configuration is invalid. + """ + + pass + +class ImplementationNotFound(EOxSException): + """ + This exception shall be raised by the registry if an implementation ID is + not found. + """ + pass + +class ImplementationAmbiguous(EOxSException): + """ + This exception shall be raised by the registry if the input data matches + more than one implementation. + """ + + pass + +class ImplementationDisabled(EOxSException): + """ + This exception shall be raised by the registry if the requested + implementation is disabled. + """ + + pass + +class BindingMethodError(EOxSException): + """ + This exception shall be raised by the registry if it cannot bind to + implementations of a given interface because the binding method does not + allow it. + """ + + pass + +class TypeMismatch(InternalError): + """ + This exception shall be raised by interfaces in case they detect that + an implementation method has been called within an argument of the wrong + type. + """ + + pass + +class InvalidExpressionError(EOxSException): + """ + This exception shall be raised if a filter expression statement is invalid, + e.g. because of incorrect operands. + """ + + pass + +class UnknownAttribute(EOxSException): + """ + This exception shall be raised if an unknown or invalid attribute is + requested from a resource. + """ + + pass + +class IDInUse(EOxSException): + """ + This exception shall be raised if a requested unique ID is already in use. + """ + + pass + +class UniquenessViolation(EOxSException): + """ + This excetion shall be raised if a database record cannot be created due + to uniqueness constraints. + """ + + pass + +class IpcException(EOxSException): + """ + This exception shall be raised in case of communication faults in the IPC + system. + """ + + pass + +class UnknownParameterFormatException(EOxSException): + """ + This exception shall be raised if a parameter is not in the format + expected by the implementation. + """ + pass + +class MissingParameterException(EOxSException): + """ + This exception shall be raised if an expected parameter is not found. + """ + pass + +class InvalidParameterException(EOxSException): + """ + This exception shall be raised if a parameter is found to be invalid. + """ + pass + +class DecoderException(EOxSException): + """ + This is the base class for exceptions raised by decoders as defined in + :mod:`eoxserver.core.util.decoders`. + """ + pass + +class KVPDecoderException(DecoderException): + """ + This is the base class for exceptions raised by the KVP decoder. + """ + pass + +class KVPKeyNotFound(KVPDecoderException, MissingParameterException): + """ + This exception shall be raised if the KVP decoder does not encounter a + given key. It inherits from :exc:`KVPDecoderException` and + :exc:`MissingParameterException`. + """ + + pass + +class KVPKeyOccurrenceError(KVPDecoderException, InvalidParameterException): + """ + This exception shall be raised if the number of occurrences of a given + KVP key does not lay within the occurrence range defined by the applicable + decoding schema. It inherits from :exc:`KVPDecoderException` and + :exc:`InvalidParameterException`. + """ + + pass + +class KVPTypeError(KVPDecoderException, InvalidParameterException): + """ + This exception shall be raised if the requested KVP value is of another + type than defined in the decoding schema. It inherits from + :exc:`KVPDecoderException` and :exc:`InvalidParameterException`. + """ + + pass + +class XMLDecoderException(DecoderException): + """ + This is the base class for exceptions raised by the XML decoder. + """ + + pass + +class XMLNodeNotFound(XMLDecoderException, MissingParameterException): + """ + This exception shall be raised if the XML decoder does not encounter a + given XML node. It inherits from :exc:`XMLDecoderException` and + :exc:`MissingParameterException`. + """ + + pass + +class XMLNodeOccurrenceError(XMLDecoderException, InvalidParameterException): + """ + This exception shall be raised if the number of occurrences of a given + XML node does not lay within the occurrence range defined by the applicable + decoding schema. It inherits from :exc:`XMLDecoderException` and + :exc:`InvalidParameterException`. + """ + + pass + +class XMLTypeError(XMLDecoderException, InvalidParameterException): + """ + This exception shall be raised if the requested XML node value is of another + type than defined in the decoding schema. It inherits from + :exc:`XMLDecoderException` and :exc:`InvalidParameterException`. + """ + + pass + +class XMLEncoderException(EOxSException): + """ + This exception shall be raised if the XML encoder finds an error in an + encoding schema. + """ + + pass + +class FactoryQueryAmbiguous(EOxSException): + """ + This exception shall be raised when ... TODO + """ + pass diff -Nru eoxserver-0.4.0beta2/eoxserver/core/filters.py eoxserver-0.3.2/eoxserver/core/filters.py --- eoxserver-0.4.0beta2/eoxserver/core/filters.py 1970-01-01 00:00:00.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/core/filters.py 2013-12-10 14:57:00.000000000 +0000 @@ -0,0 +1,301 @@ +#----------------------------------------------------------------------- +# $Id$ +# +# Project: EOxServer +# Authors: Stephan Krause +# Stephan Meissl +# +#------------------------------------------------------------------------------- +# Copyright (C) 2011 EOX IT Services GmbH +# +# 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 of this Software or works derived from this 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. +#----------------------------------------------------------------------- + +""" +This module defines interfaces for filter expressions and filters. +These can be used to refine searches for resources. +""" + +from eoxserver.core.system import System +from eoxserver.core.interfaces import * +from eoxserver.core.registry import ( + RegisteredInterface, FactoryInterface +) +from eoxserver.core.exceptions import InvalidExpressionError + +class FilterExpressionInterface(RegisteredInterface): + """ + Filter expressions can be used to constrain searches for + resources using a factory. They provide a uniform way to + define these constraints without respect to the concrete + resource implementation. + + Internally, filter expressions are translated to filters (i.e. + implementations of :class:`~.FilterInterface`) that can be applied + to a resource. + + The binding method for filter expressions is ``factory``, i.e. + implementations are accessible through a factory that implements + :class:`~.FactoryInterface`. Developers have to write their own + factory implementations for each category of expressions. + + .. method:: getOpName + + This method shall return the operator name. The name can depend + on the instance data but does not have to. Depending on the + factory implementation, the name may or may not vary with the + instance data. + + .. method:: getOpSymbol + + This method shall return the operator symbol if applicable or + ``None`` otherwise. Depending on the factory implementation, + the symbol may or may not vary with the instance data + + .. method:: getNumOperands + + This method shall return the number of operands required by + the operator. Depending on the factory implementation the + number may or may not vary with the instance data. + + .. method:: getOperands + + This method shall return a tuple of operands the instance was + initialized with. + + .. method:: initialize(**kwargs) + + This method shall initialize the expression; it takes keyword + arguments; each implementation has to define the arguments it + accepts individually. + + :Interface ID: core.filters.FilterExpression + """ + REGISTRY_CONF = { + "name": "Filter Expression Interface", + "intf_id": "core.filters.FilterExpression", + "binding_method": "factory" + } + + getOpName = Method( + returns = StringArg("@return") + ) + + getOpSymbol = Method( + returns = StringArg("@return", default=None) + ) + + getNumOperands = Method( + returns = IntArg("@return") + ) + + getOperands = Method( + returns = ObjectArg("@return", arg_class=tuple) + ) + + initialize = Method( + KwArgs("kwargs") + ) + +class FilterInterface(RegisteredInterface): + """ + Filter expressions are translated to filters that can be applied + to a given :class:`~django.db.queries.QuerySet`. This is the + interface for this operation. + + Binding to implementations of this interface is possible using + key-value-pair matching. + + :Interface ID: core.filters.Filter + :KVP keys: + * ``core.filters.res_class_id``: the implementation ID of the + resource class + * ``core.filters.expr_class_id``: the implementation ID of the + filter expression class + + .. method:: applyToQuerySet(expr, qs) + + This method shall apply a given filter expression ``expr`` to a + given Django :class:`~django.db.queries.QuerySet` ``qs`` and + return the resulting :class:`~django.db.queries.QuerySet`. + + .. method:: resourceMatches(expr, res) + + This method shall return ``True`` if the resource wrapped by + ``res`` matches the filter expression ``expr``, ``False`` + otherwise. + """ + REGISTRY_CONF = { + "name": "Filter Interface", + "intf_id": "core.filters.Filter", + "binding_method": "kvp", + "registry_keys": ( + "core.filters.res_class_id", + "core.filters.expr_class_id" + ) + } + + + applyToQuerySet = Method( + ObjectArg("expr"), # a filter expression + ObjectArg("qs"), # a Django QuerySet + returns = ObjectArg("@return") # a Django QuerySet + ) + + resourceMatches = Method( + ObjectArg("expr"), # a filter expression + ObjectArg("res"), # a resource wrapper + returns = BoolArg("@return") + ) + +class SimpleExpression(object): + """ + An implementation of :class:`FilterExpressionInterface` intended to + serve as a base class for simple expressions. + """ + #: The operator name of the simple expression; has to be overridden + #: by concrete implementations + OP_NAME = "" + + #: The operator symbol of the simple expression; ``None`` by + #: default; has to be overridden by concrete implementations + OP_SYMBOL = None + + #: The expected number of operands; has to be overridden by + #: concrete implementations + NUM_OPS = 1 + + def __init__(self): + self.__operands = None + + def getOpName(self): + """ + Returns the operator name. + """ + return self.OP_NAME + + def getOpSymbol(self): + """ + Returns the operator symbol if applicable, ``None`` by default. + """ + return self.OP_SYMBOL + + def getNumOperands(self): + """ + Returns the expected number of operands. + """ + return self.NUM_OPS + + def getOperands(self): + """ + Returns the operands of the simple expression instance. + """ + return self.__operands + + def initialize(self, **kwargs): + """ + Initialize the simple expression instance. This method accepts + one optional keyword argument, namely ``operands`` which is + expected to be a tuple of operands. + + Raises :exc:`~.InternalError` in case the number of operands + does not match. + + .. note:: Further validation steps may be added by concrete + implementations. + """ + operands = kwargs.get("operands", ()) + + self._validateOperands(operands) + + self.__operands = operands + + def _validateOperands(self, operands): + if len(operands) != self.getNumOperands(): + raise InvalidExpressionError( + "Expected %d operands, got %d." % ( + self.getNumOperands(), + len(operands) + )) + +class SimpleExpressionFactory(object): + """ + This is the base class for a simple expression factory. + """ + def __init__(self): + # Create an index of expressions using their operator names + ExpressionClasses = System.getRegistry().getFactoryImplementations(self) + + self.__op_index = {} + + for ExpressionCls in ExpressionClasses: + expr = ExpressionCls() + self.__op_index[expr.getOpName()] = ExpressionCls + + logging.debug("SimpleExpressionFactory.__init__(): operator index of '%s': %s" % ( + self.__get_impl_id__(), str(self.__op_index.keys()) + )) + + def get(self, **kwargs): + """ + Returns a filter expression. The method accepts two keyword + arguments: + + * ``op_name`` (mandatory): the operator name for the expression + * ``operands`` (optional): a tuple of operands for the + expression; the number and type of expected operands is + defined by each filter expression class individually + + The method raises :exc:`~.InternalError` if the ``op_name`` + parameter is missing or unknown. + """ + if "op_name" in kwargs: + op_name = kwargs["op_name"] + else: + raise InternalError("Missing mandatory 'op_name' parameter.") + + operands = kwargs.get("operands", ()) + + if op_name in self.__op_index: + ExpressionCls = self.__op_index[op_name] + else: + raise InternalError("Unknown operator name '%s'." % op_name) + + expr = ExpressionCls() + expr.initialize(operands=operands) + + return expr + + def find(self, **kwargs): + """ + Returns a list of filter expressions. The method accepts a + single, optional keyword argument ``op_list`` which is + expected to be a list of dictionaries of the form:: + + { + "op_name": , + "operands": + } + + The dictionaries will be passed as keyword arguments to + :meth:`get` + """ + op_list = kwargs.get("op_list", []) + + return [self.get(**op_entry) for op_entry in op_list] diff -Nru eoxserver-0.4.0beta2/eoxserver/core/fixtures/initial_data.json eoxserver-0.3.2/eoxserver/core/fixtures/initial_data.json --- eoxserver-0.4.0beta2/eoxserver/core/fixtures/initial_data.json 1970-01-01 00:00:00.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/core/fixtures/initial_data.json 2013-12-10 14:57:00.000000000 +0000 @@ -0,0 +1,2012 @@ +[ + { + "pk": 1, + "model": "core.implementation", + "fields": { + "impl_id": "services.ows.wms1x.WMS111GetCapabilitiesHandler", + "intf_id": "services.interfaces.OperationHandler" + } + }, + { + "pk": 2, + "model": "core.implementation", + "fields": { + "impl_id": "services.ows.wms1x.WMS110GetCapabilitiesHandler", + "intf_id": "services.interfaces.OperationHandler" + } + }, + { + "pk": 3, + "model": "core.implementation", + "fields": { + "impl_id": "services.ows.wms1x.WMS111VersionHandler", + "intf_id": "services.interfaces.VersionHandler" + } + }, + { + "pk": 4, + "model": "core.implementation", + "fields": { + "impl_id": "services.ows.wms1x.WMS11ExceptionEncoder", + "intf_id": "services.interfaces.ExceptionEncoder" + } + }, + { + "pk": 5, + "model": "core.implementation", + "fields": { + "impl_id": "services.ows.wcs1x.WCS11DescribeCoverageHandler", + "intf_id": "services.interfaces.OperationHandler" + } + }, + { + "pk": 6, + "model": "core.implementation", + "fields": { + "impl_id": "services.ows.wcs1x.WCS11VersionHandler", + "intf_id": "services.interfaces.VersionHandler" + } + }, + { + "pk": 7, + "model": "core.implementation", + "fields": { + "impl_id": "services.ows.wms1x.WMS10ExceptionEncoder", + "intf_id": "services.interfaces.ExceptionEncoder" + } + }, + { + "pk": 8, + "model": "core.implementation", + "fields": { + "impl_id": "services.ows.wcs1x.WCS10GetCoverageHandler", + "intf_id": "services.interfaces.OperationHandler" + } + }, + { + "pk": 9, + "model": "core.implementation", + "fields": { + "impl_id": "services.ows.wcs20.WCS20GetCapabilitiesHandler", + "intf_id": "services.interfaces.OperationHandler" + } + }, + { + "pk": 10, + "model": "core.implementation", + "fields": { + "impl_id": "services.ows.wcs20.WCS20VersionHandler", + "intf_id": "services.interfaces.VersionHandler" + } + }, + { + "pk": 11, + "model": "core.implementation", + "fields": { + "impl_id": "services.ows.wcs1x.WCS10GetCapabilitiesHandler", + "intf_id": "services.interfaces.OperationHandler" + } + }, + { + "pk": 12, + "model": "core.implementation", + "fields": { + "impl_id": "services.ows.wms1x.WMS111GetMapHandler", + "intf_id": "services.interfaces.OperationHandler" + } + }, + { + "pk": 13, + "model": "core.implementation", + "fields": { + "impl_id": "services.ows.wms1x.WMSServiceHandler", + "intf_id": "services.interfaces.ServiceHandler" + } + }, + { + "pk": 14, + "model": "core.implementation", + "fields": { + "impl_id": "services.ows.wcs1x.WCS10DescribeCoverageHandler", + "intf_id": "services.interfaces.OperationHandler" + } + }, + { + "pk": 15, + "model": "core.implementation", + "fields": { + "impl_id": "services.ows.wcs1x.WCS11GetCapabilitiesHandler", + "intf_id": "services.interfaces.OperationHandler" + } + }, + { + "pk": 16, + "model": "core.implementation", + "fields": { + "impl_id": "services.ows.wcs1x.WCSServiceHandler", + "intf_id": "services.interfaces.ServiceHandler" + } + }, + { + "pk": 17, + "model": "core.implementation", + "fields": { + "impl_id": "services.ows.wms1x.WMS110VersionHandler", + "intf_id": "services.interfaces.VersionHandler" + } + }, + { + "pk": 18, + "model": "core.implementation", + "fields": { + "impl_id": "services.ows.wms1x.WMS10VersionHandler", + "intf_id": "services.interfaces.VersionHandler" + } + }, + { + "pk": 19, + "model": "core.implementation", + "fields": { + "impl_id": "services.ows.wms1x.WMS11ExceptionHandler", + "intf_id": "services.interfaces.ExceptionHandler" + } + }, + { + "pk": 20, + "model": "core.implementation", + "fields": { + "impl_id": "services.ows.wms1x.WMS110GetMapHandler", + "intf_id": "services.interfaces.OperationHandler" + } + }, + { + "pk": 21, + "model": "core.implementation", + "fields": { + "impl_id": "services.ows.wms1x.WMS13GetMapHandler", + "intf_id": "services.interfaces.OperationHandler" + } + }, + { + "pk": 22, + "model": "core.implementation", + "fields": { + "impl_id": "services.ows.wms1x.WMS10ExceptionHandler", + "intf_id": "services.interfaces.ExceptionHandler" + } + }, + { + "pk": 23, + "model": "core.implementation", + "fields": { + "impl_id": "services.ows.wcs20.WCS20GetCoverageHandler", + "intf_id": "services.interfaces.OperationHandler" + } + }, + { + "pk": 24, + "model": "core.implementation", + "fields": { + "impl_id": "services.ows.wcs1x.WCS11GetCoverageHandler", + "intf_id": "services.interfaces.OperationHandler" + } + }, + { + "pk": 25, + "model": "core.implementation", + "fields": { + "impl_id": "services.ows.wcs20.WCS20DescribeCoverageHandler", + "intf_id": "services.interfaces.OperationHandler" + } + }, + { + "pk": 26, + "model": "core.implementation", + "fields": { + "impl_id": "services.ows.wms1x.WMS13GetCapabilitiesHandler", + "intf_id": "services.interfaces.OperationHandler" + } + }, + { + "pk": 27, + "model": "core.implementation", + "fields": { + "impl_id": "services.ows.wcs1x.WCS10VersionHandler", + "intf_id": "services.interfaces.VersionHandler" + } + }, + { + "pk": 28, + "model": "core.implementation", + "fields": { + "impl_id": "services.ows.wms1x.WMS130VersionHandler", + "intf_id": "services.interfaces.VersionHandler" + } + }, + { + "pk": 29, + "model": "core.implementation", + "fields": { + "impl_id": "services.ows.wms1x.WMS10GetMapHandler", + "intf_id": "services.interfaces.OperationHandler" + } + }, + { + "pk": 30, + "model": "core.implementation", + "fields": { + "impl_id": "services.ows.wms1x.WMS10GetCapabilitiesHandler", + "intf_id": "services.interfaces.OperationHandler" + } + }, + { + "pk": 31, + "model": "core.implementation", + "fields": { + "impl_id": "services.ows.wcs20.WCS20DescribeEOCoverageSetHandler", + "intf_id": "services.interfaces.OperationHandler" + } + }, + { + "pk": 32, + "model": "core.implementation", + "fields": { + "impl_id": "services.owscommon.OWSCommonExceptionHandler", + "intf_id": "services.interfaces.ExceptionHandler" + } + }, + { + "pk": 33, + "model": "core.implementation", + "fields": { + "impl_id": "services.owscommon.OWSCommonConfigReader", + "intf_id": "core.readers.ConfigReader" + } + }, + { + "pk": 34, + "model": "core.implementation", + "fields": { + "impl_id": "services.owscommon.OWSCommon", + "intf_id": "services.interfaces.RequestHandler" + } + }, + { + "pk": 35, + "model": "core.implementation", + "fields": { + "impl_id": "services.owscommon.OWSCommonExceptionEncoder", + "intf_id": "services.interfaces.ExceptionEncoder" + } + }, + { + "pk": 36, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.filters.OrphanedCoverageExpression", + "intf_id": "core.filters.FilterExpression" + } + }, + { + "pk": 37, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.filters.ReferenceableDatasetFootprintIntersectsArea", + "intf_id": "core.filters.Filter" + } + }, + { + "pk": 38, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.filters.RectifiedDatasetIntersectingTimeInterval", + "intf_id": "core.filters.Filter" + } + }, + { + "pk": 39, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.wrappers.RectifiedDatasetWrapper", + "intf_id": "resources.coverages.interfaces.RectifiedDataset" + } + }, + { + "pk": 40, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.filters.RectifiedDatasetContainingTimeInterval", + "intf_id": "core.filters.Filter" + } + }, + { + "pk": 41, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.filters.RectifiedStitchedMosaicFootprintIntersectsArea", + "intf_id": "core.filters.Filter" + } + }, + { + "pk": 42, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.filters.RectifiedStitchedMosaicSpatialSlice", + "intf_id": "core.filters.Filter" + } + }, + { + "pk": 43, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.filters.ReferenceableDatasetContainingTimeInterval", + "intf_id": "core.filters.Filter" + } + }, + { + "pk": 44, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.filters.ContainedRectifiedStitchedMosaicFilter", + "intf_id": "core.filters.Filter" + } + }, + { + "pk": 45, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.filters.OrphanedRectifiedDataset", + "intf_id": "core.filters.Filter" + } + }, + { + "pk": 46, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.filters.RectifiedDatasetAttribute", + "intf_id": "core.filters.Filter" + } + }, + { + "pk": 47, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.filters.RectifiedDatasetSpatialSlice", + "intf_id": "core.filters.Filter" + } + }, + { + "pk": 48, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.wrappers.ReferenceableDataset", + "intf_id": "resources.coverages.interfaces.ReferenceableDataset" + } + }, + { + "pk": 49, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.filters.CoverageExpressionFactory", + "intf_id": "core.registry.Factory" + } + }, + { + "pk": 50, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.filters.TimeSliceExpression", + "intf_id": "core.filters.FilterExpression" + } + }, + { + "pk": 51, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.filters.ContainedReferenceableDatasetFilter", + "intf_id": "core.filters.Filter" + } + }, + { + "pk": 52, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.filters.ReferenceableDatasetAttribute", + "intf_id": "core.filters.Filter" + } + }, + { + "pk": 53, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.filters.RectifiedDatasetFootprintIntersectsArea", + "intf_id": "core.filters.Filter" + } + }, + { + "pk": 54, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.wrappers.DatasetSeriesWrapper", + "intf_id": "resources.coverages.interfaces.DatasetSeries" + } + }, + { + "pk": 55, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.filters.FootprintWithinAreaExpression", + "intf_id": "core.filters.FilterExpression" + } + }, + { + "pk": 56, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.filters.RectifiedDatasetFootprintWithinArea", + "intf_id": "core.filters.Filter" + } + }, + { + "pk": 57, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.filters.OrphanedReferenceableDataset", + "intf_id": "core.filters.Filter" + } + }, + { + "pk": 58, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.filters.RectifiedStitchedMosaicAttribute", + "intf_id": "core.filters.Filter" + } + }, + { + "pk": 59, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.filters.ReferenceableDatasetFootprintWithinArea", + "intf_id": "core.filters.Filter" + } + }, + { + "pk": 60, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.filters.ReferenceableDatasetIntersectingTimeInterval", + "intf_id": "core.filters.Filter" + } + }, + { + "pk": 61, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.wrappers.RectifiedStitchedMosaic", + "intf_id": "resources.coverages.interfaces.RectifiedStitchedMosaic" + } + }, + { + "pk": 62, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.wrappers.DatasetSeriesFactory", + "intf_id": "core.resources.ResourceFactoryInterface" + } + }, + { + "pk": 63, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.filters.RectifiedStitchedMosaicContainingTimeInterval", + "intf_id": "core.filters.Filter" + } + }, + { + "pk": 64, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.filters.RectifiedStitchedMosaicTimeSlice", + "intf_id": "core.filters.Filter" + } + }, + { + "pk": 65, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.filters.ContainedCoverageExpression", + "intf_id": "core.filters.FilterExpression" + } + }, + { + "pk": 66, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.filters.DatasetSeriesContains", + "intf_id": "core.filters.Filter" + } + }, + { + "pk": 67, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.filters.ContainingTimeIntervalExpression", + "intf_id": "core.filters.FilterExpression" + } + }, + { + "pk": 68, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.filters.RectifiedStitchedMosaicContains", + "intf_id": "core.filters.Filter" + } + }, + { + "pk": 69, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.filters.ReferenceableDatasetSpatialSlice", + "intf_id": "core.filters.Filter" + } + }, + { + "pk": 70, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.wrappers.EOCoverageFactory", + "intf_id": "core.resources.ResourceFactoryInterface" + } + }, + { + "pk": 71, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.filters.RectifiedDatasetTimeSlice", + "intf_id": "core.filters.Filter" + } + }, + { + "pk": 72, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.filters.FootprintIntersectsAreaExpression", + "intf_id": "core.filters.FilterExpression" + } + }, + { + "pk": 73, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.filters.ReferenceableDatasetTimeSlice", + "intf_id": "core.filters.Filter" + } + }, + { + "pk": 74, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.filters.SpatialSliceExpression", + "intf_id": "core.filters.FilterExpression" + } + }, + { + "pk": 75, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.filters.IntersectingTimeIntervalExpression", + "intf_id": "core.filters.FilterExpression" + } + }, + { + "pk": 76, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.filters.OrphanedRectifiedStitchedMosaic", + "intf_id": "core.filters.Filter" + } + }, + { + "pk": 77, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.filters.RectifiedStitchedMosaicFootprintWithinArea", + "intf_id": "core.filters.Filter" + } + }, + { + "pk": 78, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.filters.RectifiedStitchedMosaicIntersectingTimeInterval", + "intf_id": "core.filters.Filter" + } + }, + { + "pk": 79, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.filters.ContainedRectifiedDataset", + "intf_id": "core.filters.Filter" + } + }, + { + "pk": 80, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.filters.AttributeExpression", + "intf_id": "core.filters.FilterExpression" + } + }, + { + "pk": 81, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.wrappers.ReferenceableDatasetWrapper", + "intf_id": "resources.coverages.interfaces.ReferenceableDataset" + } + }, + { + "pk": 82, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.wrappers.RectifiedStitchedMosaicWrapper", + "intf_id": "resources.coverages.interfaces.RectifiedStitchedMosaic" + } + }, + { + "pk": 83, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.data.LocalDataPackageWrapper", + "intf_id": "resources.coverages.interfaces.DataPackage" + } + }, + { + "pk": 84, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.data.RemoteDataPackageWrapper", + "intf_id": "resources.coverages.interfaces.DataPackage" + } + }, + { + "pk": 85, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.data.RasdamanDataPackageWrapper", + "intf_id": "resources.coverages.interfaces.DataPackage" + } + }, + { + "pk": 86, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.data.TileIndexWrapper", + "intf_id": "resources.coverages.interfaces.TileIndex" + } + }, + { + "pk": 87, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.data.DataPackageFactory", + "intf_id": "core.registry.Factory" + } + }, + { + "pk": 88, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.data.TileIndexFactory", + "intf_id": "core.registry.Factory" + } + }, + { + "pk": 89, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.metadata.NativeMetadataFormat", + "intf_id": "resources.coverages.interfaces.EOMetadataFormat" + } + }, + { + "pk": 90, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.metadata.EOOMFormat", + "intf_id": "resources.coverages.interfaces.EOMetadataFormat" + } + }, + { + "pk": 91, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.metadata.EOMetadata", + "intf_id": "resources.coverages.interfaces.GenericEOMetadata" + } + }, + { + "pk": 92, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.metadata.XMLEOMetadataFileReader", + "intf_id": "resources.coverages.interfaces.EOMetadataReader" + } + }, + { + "pk": 93, + "model": "core.implementation", + "fields": { + "impl_id": "services.connectors.LocalFileConnector", + "intf_id": "services.mapserver.MapServerDataConnectorInterface" + } + }, + { + "pk": 94, + "model": "core.implementation", + "fields": { + "impl_id": "services.connectors.TiledPackageConnector", + "intf_id": "services.mapserver.MapServerDataConnectorInterface" + } + }, + { + "pk": 95, + "model": "core.implementation", + "fields": { + "impl_id": "services.connectors.RasdamanArrayConnector", + "intf_id": "services.mapserver.MapServerDataConnectorInterface" + } + }, + { + "pk": 96, + "model": "core.implementation", + "fields": { + "impl_id": "backends.local.LocalStorage", + "intf_id": "backends.interfaces.Storage" + } + }, + { + "pk": 97, + "model": "core.implementation", + "fields": { + "impl_id": "backends.local.LocalPathWrapper", + "intf_id": "backends.interfaces.LocalPath" + } + }, + { + "pk": 98, + "model": "core.implementation", + "fields": { + "impl_id": "backends.ftp.FTPStorage", + "intf_id": "backends.interfaces.Storage" + } + }, + { + "pk": 99, + "model": "core.implementation", + "fields": { + "impl_id": "backends.ftp.RemotePathWrapper", + "intf_id": "backends.interfaces.RemotePath" + } + }, + { + "pk": 100, + "model": "core.implementation", + "fields": { + "impl_id": "backends.rasdaman.RasdamanStorage", + "intf_id": "backends.interfaces.Storage" + } + }, + { + "pk": 101, + "model": "core.implementation", + "fields": { + "impl_id": "backends.rasdaman.RasdamanArrayWrapper", + "intf_id": "backends.interfaces.RasdamanArray" + } + }, + { + "pk": 102, + "model": "core.implementation", + "fields": { + "impl_id": "backends.factories.LocationFactory", + "intf_id": "core.registry.Factory" + } + }, + { + "pk": 103, + "model": "core.implementation", + "fields": { + "impl_id": "backends.cache.CacheConfigReader", + "intf_id": "core.readers.ConfigReader" + } + }, + { + "pk": 104, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.data.DataSourceFactory", + "intf_id": "core.records.RecordWrapperFactoryInterface" + } + }, + { + "pk": 105, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.data.DataSourceWrapper", + "intf_id": "backends.interfaces.DataSource" + } + }, + { + "pk": 106, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.managers.DatasetSeriesManager", + "intf_id": "resources.coverages.interfaces.Manager" + } + }, + { + "pk": 107, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.managers.RectifiedStitchedMosaicManager", + "intf_id": "resources.coverages.interfaces.Manager" + } + }, + { + "pk": 108, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.managers.RectifiedDatasetManager", + "intf_id": "resources.coverages.interfaces.Manager" + } + }, + { + "pk": 109, + "model": "core.implementation", + "fields": { + "impl_id": "services.ows.wcs1x.WCS112GetCapabilitiesHandler", + "intf_id": "services.interfaces.OperationHandler" + } + }, + { + "pk": 110, + "model": "core.implementation", + "fields": { + "impl_id": "services.ows.wcs1x.WCS112DescribeCoverageHandler", + "intf_id": "services.interfaces.OperationHandler" + } + }, + { + "pk": 111, + "model": "core.implementation", + "fields": { + "impl_id": "services.ows.wcs1x.WCS112GetCoverageHandler", + "intf_id": "services.interfaces.OperationHandler" + } + }, + { + "pk": 112, + "model": "core.implementation", + "fields": { + "impl_id": "services.ows.wcs1x.WCS112VersionHandler", + "intf_id": "services.interfaces.VersionHandler" + } + }, + { + "pk": 113, + "model": "core.implementation", + "fields": { + "impl_id": "services.ogc.WMS13ExceptionHandler", + "intf_id": "services.interfaces.ExceptionHandler" + } + }, + { + "pk": 114, + "model": "core.implementation", + "fields": { + "impl_id": "services.ogc.WMS13ExceptionEncoder", + "intf_id": "services.interfaces.ExceptionEncoder" + } + }, + { + "pk": 115, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.filters.LocationReferencesRectifiedDatasetFilter", + "intf_id": "core.filters.Filter" + } + }, + { + "pk": 116, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.filters.LocationReferencesReferencableDatasetFilter", + "intf_id": "core.filters.Filter" + } + }, + { + "pk": 117, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.filters.LocationReferencesDatasetExpression", + "intf_id": "core.filters.FilterExpression" + } + }, + { + "pk": 118, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.filters.ContainsCoverageExpression", + "intf_id": "core.filters.FilterExpression" + } + }, + { + "pk": 119, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.managers.ReferenceableDatasetManager", + "intf_id": "resources.coverages.interfaces.Manager" + } + }, + { + "pk": 120, + "model": "core.implementation", + "fields": { + "impl_id": "services.ows.wcs11Transaction.WCS11TransactionHandler", + "intf_id": "services.interfaces.OperationHandler" + } + }, + { + "pk": 121, + "model": "core.implementation", + "fields": { + "impl_id": "services.ows.wcs11Transaction.WCS11VersionHandler", + "intf_id": "services.interfaces.VersionHandler" + } + }, + { + "pk": 122, + "model": "core.implementation", + "fields": { + "impl_id": "services.ows.wms1x.WMS13GetFeatureInfoHandler", + "intf_id": "services.interfaces.OperationHandler" + } + }, + { + "pk": 123, + "model": "core.implementation", + "fields": { + "impl_id": "services.auth.charonpdp.CharonPDP", + "intf_id": "services.auth.base.PolicyDecisionPointInterface" + } + }, + { + "pk": 124, + "model": "core.implementation", + "fields": { + "impl_id": "services.auth.base.AuthConfigReader", + "intf_id": "core.readers.ConfigReader" + } + }, + { + "pk": 125, + "model": "core.implementation", + "fields": { + "impl_id": "services.auth.dummypdp.DummyPDP", + "intf_id": "services.auth.base.PolicyDecisionPointInterface" + } + }, + { + "pk": 126, + "model": "core.implementation", + "fields": { + "impl_id": "services.ows.wms1x.WMS13GetLegendGraphicHandler", + "intf_id": "services.interfaces.OperationHandler" + } + }, + { + "pk": 127, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.metadata.DatasetMetadataFileReader", + "intf_id": "resources.coverages.interfaces.EOMetadataReader" + } + }, + { + "pk": 128, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.metadata.EnvisatDatasetMetadataFormat", + "intf_id": "resources.coverages.interfaces.EOMetadataFormat" + } + }, + { + "pk": 129, + "model": "core.implementation", + "fields": { + "impl_id": "resources.coverages.formats.FormatLoaderStartupHandler", + "intf_id": "eoxserver.core.startup.StartupHandlerInterface" + } + }, + { + "pk": 130, + "model": "core.implementation", + "fields": { + "impl_id": "services.ows.wcs20.WCS20CorrigendumGetCapabilitiesHandler", + "intf_id": "services.interfaces.OperationHandler" + } + }, + { + "pk": 131, + "model": "core.implementation", + "fields": { + "impl_id": "services.ows.wcs20.WCS20CorrigendumDescribeCoverageHandler", + "intf_id": "services.interfaces.OperationHandler" + } + }, + { + "pk": 132, + "model": "core.implementation", + "fields": { + "impl_id": "services.ows.wcs20.WCS20CorrigendumDescribeEOCoverageSetHandler", + "intf_id": "services.interfaces.OperationHandler" + } + }, + { + "pk": 133, + "model": "core.implementation", + "fields": { + "impl_id": "services.ows.wcs20.WCS20CorrigendumGetCoverageHandler", + "intf_id": "services.interfaces.OperationHandler" + } + }, + { + "pk": 134, + "model": "core.implementation", + "fields": { + "impl_id": "services.ows.wcs20.WCS20CorrigendumVersionHandler", + "intf_id": "services.interfaces.VersionHandler" + } + }, + { + "pk": 1, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 2, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 3, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 4, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 5, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 6, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 7, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 8, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 9, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 10, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 11, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 12, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 13, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 14, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 15, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 16, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 17, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 18, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 19, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 20, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 21, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 22, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 23, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 24, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 25, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 26, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 27, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 28, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 29, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 30, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 31, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 32, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 33, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 34, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 35, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 36, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 37, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 38, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 39, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 40, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 41, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 42, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 43, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 44, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 45, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 46, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 47, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 48, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 49, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 50, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 51, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 52, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 53, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 54, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 55, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 56, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 57, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 58, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 59, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 60, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 61, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 62, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 63, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 64, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 65, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 66, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 67, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 68, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 69, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 70, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 71, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 72, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 73, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 74, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 75, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 76, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 77, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 78, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 79, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 80, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 81, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 82, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 83, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 84, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 85, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 86, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 87, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 88, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 89, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 90, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 91, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 92, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 93, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 94, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 95, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 96, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 97, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 98, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 99, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 100, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 101, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 102, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 103, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 104, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 105, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 106, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 107, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 108, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 109, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 110, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 111, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 112, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 113, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 114, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 115, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 116, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 117, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 118, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 119, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 120, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 121, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 122, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 123, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 124, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 125, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 126, + "model": "core.component", + "fields": { + "enabled": false + } + }, + { + "pk": 127, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 128, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 129, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 130, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 131, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 132, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 133, + "model": "core.component", + "fields": { + "enabled": true + } + }, + { + "pk": 134, + "model": "core.component", + "fields": { + "enabled": true + } + } +] diff -Nru eoxserver-0.4.0beta2/eoxserver/core/__init__.py eoxserver-0.3.2/eoxserver/core/__init__.py --- eoxserver-0.4.0beta2/eoxserver/core/__init__.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/core/__init__.py 2013-12-10 14:57:00.000000000 +0000 @@ -1,94 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -"""\ -The eoxserver.core package provides functionality for the initialization and -re-initialization of the component system. -For convenience, the module imports the most important items from the -:mod:`eoxserver.core.component` module and instantiates a component manager -:obj:`eoxserver.core.env`. -""" - - -import logging -import threading - -from django.utils.importlib import import_module - -from eoxserver.core.component import ( - ComponentManager, ComponentMeta, Component, - ExtensionPoint, UniqueExtensionPoint, implements -) -from eoxserver.core.util.importtools import easy_import - - -env = ComponentManager() -logger = logging.getLogger(__name__) - -__init_lock = threading.RLock() -__is_initialized = False - - -def initialize(): - """ Initialize the EOxServer plugin system by trying to import all the - plugins referenced by the `PLUGINS` configuration entry from the - settings module. If a module path ends with '*' then all direct - submodules will be imported aswell and if it ends with '**' it means - that the import will be done recursively. - """ - - global __is_initialized - - with __init_lock: - if __is_initialized: - return - __is_initialized = True - - from django.conf import settings - - logger.info("Initializing EOxServer components.") - - for plugin in getattr(settings, "COMPONENTS", ()): - easy_import(plugin) - - -def reset(): - """ Reset the EOxServer plugin system. - """ - - global __is_initialized - - with __init_lock: - if not __is_initialized: - return - __is_initialized = False - - logger.info("Resetting EOxServer components.") - ComponentMeta._registry = {} - ComponentMeta._components = [] - - initialize() diff -Nru eoxserver-0.4.0beta2/eoxserver/core/interfaces.py eoxserver-0.3.2/eoxserver/core/interfaces.py --- eoxserver-0.4.0beta2/eoxserver/core/interfaces.py 1970-01-01 00:00:00.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/core/interfaces.py 2013-12-10 14:57:00.000000000 +0000 @@ -0,0 +1,847 @@ +#------------------------------------------------------------------------------- +# $Id$ +# +# Project: EOxServer +# Authors: Stephan Krause +# Stephan Meissl +# +#------------------------------------------------------------------------------- +# Copyright (C) 2011 EOX IT Services GmbH +# +# 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 of this Software or works derived from this 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. +#------------------------------------------------------------------------------- + +""" +This module contains the core logic for interface declaration and validation. +""" + +import types +import logging + +from eoxserver.core.exceptions import InternalError, TypeMismatch, ConfigError + + +logger = logging.getLogger(__name__) + +global RUNTIME_VALIDATION_LEVEL + +RUNTIME_VALIDATION_LEVEL = "trust" + +#------------------------------------------------------------------------------- +# Argument classes +#------------------------------------------------------------------------------- + +class Arg(object): + """ + This is the common base class for arguments of any kind; it can be used + in interface declarations as well to represent an argument of arbitrary + type. + + The constructor requires a ``name`` argument which denotes the argument + name. The validation will check at class creation time if the method of an + implementing class defines an argument of the given name, so you should + always use valid Python variable names here (you can use arbitrary strings + for return value declarations though). + + Furthermore, the constructor accepts a ``default`` keyword argument which + defines a default value for the declared argument. The validation will + check at class creation time if this default value is present in the + implementing class and fail if it is not. + + Its methods are intended for internal use in runtime validation. + """ + + def __init__(self, name, **kwargs): + self.name = name + + if "default" in kwargs: + self.default = kwargs["default"] + self.optional = True + else: + self.default = None + self.optional = False + + def isOptional(self): + """ + Returns ``True`` if the argument is optional, meaning that a default + value has been defined for it, ``False`` otherwise. + """ + + return self.optional + + def isValid(self, arg_value): + """ + Returns ``True`` if ``arg_value`` is an acceptable value for the + argument, ``False`` otherwise. Acceptable values are either the default + value if it has been defined or values of the expected type. + """ + + return (self.optional and self.default == arg_value) or \ + self.isValidType(arg_value) + + def isValidType(self, arg_value): + """ + Returns ``True`` if the argument value ``arg_value`` has a valid type, + ``False`` otherwise. This method is overridden by :class:`Arg` + subclasses in order to check for individual types. The base class + implementation always returns ``True`` meaning that all types of + argument values are accepted. + """ + + return True + + def getExpectedType(self): + """ + Returns the expected type name; used in error messages only. This + method is overridden by :class:`Arg` subclasses in order to customize + error reporting. The base class implementation returns ``""``. + """ + + return "" + +class StrArg(Arg): + """ + Represents an argument of type :class:`str`. + """ + def isValidType(self, arg_value): + return isinstance(arg_value, str) + + def getExpectedType(self): + return "str" + +class UnicodeArg(Arg): + """ + Represents an argument of type :class:`unicode`. + """ + def isValidType(self, arg_value): + return isinstance(arg_value, unicode) + + def getExpectedType(self): + return "unicode" + +class StringArg(Arg): + """ + Represents an argument of types :class:`str` or :class:`unicode`. + """ + def isValidType(self, arg_value): + return (isinstance(arg_value, str) or isinstance(arg_value, unicode)) + + def getExpectedType(self): + return "str' or 'unicode" + +class BoolArg(Arg): + """ + Represents an argument of type :class:`bool`. + """ + def isValidType(self, arg_value): + return isinstance(arg_value, bool) + + def getExpectedType(self): + return "bool" + +class IntArg(Arg): + """ + Represents an argument of type :class:`int`. + """ + def isValidType(self, arg_value): + return isinstance(arg_value, int) + + def getExpectedType(self): + return "int" + +class LongArg(Arg): + """ + Represents an argument of type :class:`long`. + """ + def isValidType(self, arg_value): + return isinstance(arg_value, long) + + def getExpectedType(self): + return "long" + +class FloatArg(Arg): + """ + Represents an argument of type :class:`float`. + """ + def isValidType(self, arg_value): + return isinstance(arg_value, float) + + def getExpectedType(self): + return "float" + +class RealArg(Arg): + """ + Represents a real number argument, i.e. an argument of types :class:`int`, + :class:`long` or :class:`float`. + """ + def isValidType(self, arg_value): + return isinstance(arg_value, int) or \ + isinstance(arg_value, long) or \ + isinstance(arg_value, float) + + def getExpectedType(self): + return "int', 'long' or 'float" + +class ComplexArg(Arg): + """ + Represents a complex number argument of type :class:`complex`. + """ + def isValidType(self, arg_value): + return isinstance(arg_value, complex) + + def getExpectedType(self): + return "complex" + +class IterableArg(Arg): + """ + Represents an iterable argument. + """ + def isValidType(self, arg_value): + return hasattr(arg_value, "__iter__") + + def getExpectedType(self): + return "iterable (pseudo-type)" + +class SubscriptableArg(Arg): + """ + Represents a subscriptable argument. + """ + def isValidType(self, arg_value): + return hasattr(arg_value, "__getitem__") + + def getExpectedType(self): + return "subscriptable (pseudo-type)" + +class ListArg(IterableArg, SubscriptableArg): + """ + Represents an argument of type :class:`list`. + """ + def isValidType(self, arg_value): + return isinstance(arg_value, list) + + def getExpectedType(self): + return "list" + +class DictArg(IterableArg, SubscriptableArg): + """ + Represents an argument of type :class:`dict`. + """ + + def isValidType(self, arg_value): + return isinstance(arg_value, dict) + + def getExpectedType(self): + return "dict" + +class ObjectArg(Arg): + """ + Represents an new-style class argument. The range of accepted objects can + be restricted by providing the ``arg_class`` keyword argument to the + constructor. Runtime validation will then check if the argument value is + an instance of ``arg_class`` (or one of its subclasses) and fail otherwise. + """ + + def __init__(self, name, **kwargs): + super(ObjectArg, self).__init__(name, **kwargs) + + if "arg_class" in kwargs: + try: + issubclass(kwargs["arg_class"], object) + except: + raise InternalError("Argument class must be a new-style class.") + + self.arg_class = kwargs["arg_class"] + else: + self.arg_class = object + + def isValidType(self, arg_value): + return isinstance(arg_value, self.arg_class) + + def getExpectedType(self): + return self.arg_class.__name__ + +class PosArgs(Arg): + """ + Represents arbitrary positional arguments as supported by Python with + the ``method(self, *args)`` syntax. The range of accepted objects can + be restricted by providing the ``arg_class`` keyword argument to the + constructor. Runtime validation will then check if the argument value is + an instance of ``arg_class`` (or one of its subclasses) and fail otherwise. + + Note that a :class:`PosArgs` argument declaration can only be followed by + a :class:`KwArgs` declaration, otherwise validation will fail. + """ + def __init__(self, name, **kwargs): + self.name = name + self.optional = True + self.default = None + + if "arg_class" in kwargs: + try: + if issubclass(kwargs["arg_class"], object): + self.arg_class = kwargs["arg_class"] + else: + raise InternalError("Argument class must be a new-style class.") + except: + raise InternalError("Argument class must be a new-style class.") + else: + self.arg_class = None + + def isValidType(self, arg_value): + return self.arg_class is None or isinstance(arg_value, self.arg_class) + + def getExpectedType(self): + if self.arg_class is None: + return "any" + else: + return self.arg_class.__name__ + +class KwArgs(Arg): + """ + Represents arbitrary keyword arguments as supported by Python with the + ``method(self, **kwargs)`` syntax. Note that this must always be the + last input argument declaration in a method, otherwise validation will fail. + """ + def __init__(self, name, **kwargs): + self.name = name + self.optional = True + self.default = None + +#------------------------------------------------------------------------------- +# Method class +#------------------------------------------------------------------------------- + +class Method(object): + """ + The :class:`Method` is used for method declarations in interfaces. Its + constructor accepts an arbitrary number of positional arguments representing + input arguments to the method to be defined, and one optional keyword + argument ``returns`` which represents the methods return value, if any. + + All arguments must be instances of :class:`Arg` or one of its subclasses. + + The methods of the :class:`Method` class are intended for internal use by + the :class:`Interface` validation algorithms only. + """ + def __init__(self, *args, **kwargs): + self.validateArgs(args) + + self.named_args = [] + self.pos_args = None + self.kwargs = None + + for arg in args: + if isinstance(arg, PosArgs): + self.pos_args = arg + elif isinstance(arg, KwArgs): + self.kwargs = arg + else: + self.named_args.append(arg) + + self.returns = kwargs.get("returns", None) + + def validateArgs(self, args): + """ + Validate the input arguments. That is, check if they are in the + right order and no argument is defined more than once. Raises + :exc:`~.InternalError` if the arguments do not validate. + + Used internally by the constructor during instance creation. + """ + opt_args_flag = False + pos_args_flag = False + kwargs_flag = False + + names = [] + + for arg in args: + if not isinstance(arg, Arg): + raise InternalError("Method arguments must be instances of Arg.") + + if opt_args_flag: + if not arg.isOptional(): + raise InternalError("Mandatory arguments must precede optional arguments.") + + if pos_args_flag: + if not isinstance(arg, KwArgs): + raise InternalError("Only keyword arguments may follow optional positional argument block.") + + if kwargs_flag: + raise InternalError("No arguments allowed after keyword arguments block.") + + if arg.isOptional(): + opt_args_flag = True + if isinstance(arg, PosArgs): + pos_args_flag = True + elif isinstance(arg, KwArgs): + kwargs_flag = True + + if arg.name in names: + raise InternalError("Argument named '%s' appears multiple times." % arg.name) + else: + names.append(arg.name) + + def validateImplementation(self, impl_method): + """ + This method is at implementation class creation time to check if the + implementing class method conforms to the method declaration. It expects + the corresponding method as its single input argument ``impl_method``. + It makes extensive use of Python's great introspection capabilities. + + Raises :exc:`~.InternalError` in case the implementation does not + validate. + """ + + if len(self.named_args) != impl_method.func_code.co_argcount - 1: + raise InternalError("Number of arguments does not match") + + for i in range(0, len(self.named_args)): + if self.named_args[i].name != impl_method.func_code.co_varnames[i+1]: + raise InternalError("Expected argument named '%s', got '%s'." % (self.named_args[i].name, impl_method.func_code.co_varnames[i+1])) + + if self.pos_args is not None and impl_method.func_code.co_flags & 4 == 0: + raise InternalError("Expected positional argument block.") + + if self.kwargs is not None and impl_method.func_code.co_flags & 8 == 0: + raise InternalError("Expected keyword argument block.") + + def validateType(self, method_name, *args, **kwargs): + """ + This method is called for runtime argument type validation. It gets the + input of the implementing method and checks it against the argument + declarations. + + Raises :exc:`~.TypeMismatch` if validation fails. + """ + + # map arguments to self.named_args, self.pos_args and + # self.kwargs + + intf_named_args = {} + + for arg in self.named_args: + intf_named_args[arg.name] = arg + + impl_named_args = {} + impl_pos_args = [] + impl_kwargs = {} + + for i in range(0, min(len(self.named_args), len(args))): + impl_named_args[self.named_args[i].name] = (self.named_args[i], args[i]) + + if len(args) > len(self.named_args): + if self.pos_args is not None: + impl_pos_args = args[len(self.named_args):] + else: + self._raiseWrongNumberOfArguments(method_name, len(args)) + + for name, arg_value in kwargs.items(): + if name in intf_named_args: + if name in impl_named_args: + raise TypeError("%s() got multiple values for keyword argument '%s'" % ( + method_name, + name + )) + else: + impl_named_args[name] = (intf_named_args[name], arg_value) + else: + if self.kwargs is not None: + impl_kwargs[name] = arg_value + else: + raise TypeError("%s() got an unexpected keyword argument '%s'" % ( + method_name, + name + )) + + if len(impl_named_args) < len(filter(lambda arg: not arg.isOptional(), self.named_args)): + self._raiseWrongNumberOfArguments(method_name, len(impl_named_args), len(kwargs) > 0) + + # finally, validate + + msgs = [] + + logger.debug("validateType(): start validation") + + for arg, arg_value in impl_named_args.values(): + if not arg.isValid(arg_value): + msgs.append("%s(): Invalid type for argument '%s'. Expected '%s', got '%s'." % ( + method_name, + arg.name, + arg.getExpectedType(), + str(type(arg_value)) + )) + + if self.pos_args is not None: + if not self.pos_args.isValid(impl_pos_args): + msgs.append("%s(): Invalid type for positional arguments. Expected '%s' only." % ( + method_name, + self.pos_args.getExpectedType() + )) + + logger.debug("validateType(): finish validation") + + if len(msgs) > 0: + raise TypeMismatch("\n".join(msgs)) + + def validateReturnType(self, method_name, ret_value): + """ + This method is called for runtime argument type validation. It expects + the method name ``method_name`` and the return value ``ret_value`` as + input and checks the return value against the return value declaration, + if any. + + Raises :exc:`~.TypeMismatch` if validation fails. + """ + + if self.returns is not None and \ + not self.returns.isValid(ret_value): + raise TypeMismatch("%s(): Invalid return type. Expected '%s', got '%s'" % ( + method_name, + self.returns.getExpectedType(), + str(type(ret_value)) + )) + + def _raiseWrongNumberOfArguments(self, method_name, argcount, kwargs_present=False): + if any(map(lambda arg : arg.isOptional(), self.named_args)): + if argcount > len(self.named_args): + adverb = "at most" + count = len(self.named_args) + else: + adverb = "at least" + count = len(filter(lambda arg : not arg.isOptional(), self.named_args)) + else: + adverb = "exactly" + count = len(self.named_args) + + if kwargs_present: + prefix = "non-keyword " + else: + prefix = "" + + raise TypeError("%s() takes %s %d %sarguments (%d given)" % ( + method_name, + adverb, + count, + prefix, + argcount + )) + +#------------------------------------------------------------------------------- +# Interface class +#------------------------------------------------------------------------------- + +class InterfaceMetaClass(type): + def __new__(cls, name, bases, class_dict): + if "INTERFACE_CONF" in class_dict: + local_conf = class_dict["INTERFACE_CONF"] + else: + local_conf = {} + + class_dict["__iconf__"] = cls._mergeConfs(local_conf, bases, "__iconf__") + + return type.__new__(cls, name, bases, class_dict) + + @classmethod + def _mergeConfs(mcls, local_conf, bases, conf_name): + base_confs = [] + for base in bases: + if hasattr(base, conf_name): + base_confs.append(getattr(base, conf_name)) + + conf = {} + for base_conf in reversed(base_confs): + conf.update(base_conf) + conf.update(local_conf) + + return conf + +class Interface(object): + """ + This is the base class for all interface declarations. Derive from it or + one of its subclasses to create your own interface declaration. + + The :class:`Interface` class has only class variables (the method + declarations) and class methods. + """ + + __metaclass__ = InterfaceMetaClass + + @classmethod + def implement(InterfaceCls, ImplementationCls): + """ + This method takes an implementing class as input, validates it, and + returns the implementation. + + In the validation step, :meth:`Method.validateImplementation` + is called for each method declared in the interface. + :exc:`~.InternalError` is raised if a method is not found or if + the method signature does not match the declaration. + + If validation has passed, the implementation is getting prepared. The + implementation inherits from the implementing class. The ``__ifclass__`` + magic attribute is added to the class dictionary. If runtime + validation has been enabled, the methods of the implementing class + defined in the interface are replaced by descriptors (instances of + :class:`WarningDescriptor` or :class:`FailingDescriptor`). + + Finally, the implementation class is generated and returned. + """ + + name = InterfaceCls._getName(ImplementationCls) + bases = InterfaceCls._getBases(ImplementationCls) + + InterfaceCls._validateImplementation(bases) + + class_dict = InterfaceCls._getClassDict(ImplementationCls, bases) + + return type(name, bases, class_dict) + + @classmethod + def _getName(InterfaceCls, ImplementationCls): + return "_Impl_%s_%s" % (ImplementationCls.__module__.replace(".", "_"), ImplementationCls.__name__) + + @classmethod + def _getBases(InterfaceCls, ImplementationCls): + bases = (ImplementationCls,) + + return bases + + @classmethod + def _getClassDict(InterfaceCls, ImplementationCls, bases): + # Runtime validation levels + # * "trust" - do not enforce type checking at runtime (default) + # * "warn" - print warnings to the log file if type check fails + # * "fail" - raise exception if type check fails + runtime_validation_level = InterfaceCls._getRuntimeValidationLevel(ImplementationCls) + + if runtime_validation_level.lower() == "trust": + class_dict = {} + + elif runtime_validation_level.lower() == "warn": + class_dict = {} + + interface_methods = InterfaceCls._getMethods() + + for name, method in interface_methods.items(): + func = InterfaceCls._getBaseMethod(name, bases) + class_dict[name] = WarningDescriptor(method, func) + + elif runtime_validation_level.lower() == "fail": + class_dict = {} + + interface_methods = InterfaceCls._getMethods() + + logger.debug("Interface._getClassDict(): Interface Methods: %s" % str(interface_methods)) + + for name, method in interface_methods.items(): + func = InterfaceCls._getBaseMethod(name, bases) + class_dict[name] = FailingDescriptor(method, func) + + else: + class_dict = {} + + class_dict.update({"__ifclass__": InterfaceCls}) + + return class_dict + + @classmethod + def _validateImplementation(InterfaceCls, bases): + interface_methods = InterfaceCls._getMethods() + + implementation_dict = {} + for base in reversed(bases): + for cls in reversed(base.__mro__): # TODO: check if this gives the right MRO in the case of multiple inheritance + for name, attr in cls.__dict__.items(): + if type(attr) is types.FunctionType: + implementation_dict[name] = attr + + for name, method in interface_methods.items(): + if name in implementation_dict: + method.validateImplementation(implementation_dict[name]) + else: + raise InternalError("Method '%s' not found in implementation." % name) + + @classmethod + def _getMethods(InterfaceCls): + interface_methods = {} + + for name, attr in InterfaceCls.__dict__.items(): + if isinstance(attr, Method): + interface_methods[name] = attr + + for InterfaceBase in reversed(InterfaceCls.__mro__): + for name, attr in InterfaceBase.__dict__.items(): + if isinstance(attr, Method): + interface_methods[name] = attr + + return interface_methods + + @classmethod + def _getBaseMethod(InterfaceCls, method_name, bases): + for base in bases: + for cls in base.__mro__: + if method_name in cls.__dict__: + return cls.__dict__[method_name] + + raise InternalError("Base method %s() not found." % method_name) + + @classmethod + def _getRuntimeValidationLevel(InterfaceCls, ImplementationCls): + global_level = RUNTIME_VALIDATION_LEVEL.lower() + intf_level = InterfaceCls.__iconf__.get("runtime_validation_level") + if "IMPL_CONF" in ImplementationCls.__dict__: + impl_level = ImplementationCls.IMPL_CONF.get("runtime_validation_level") + else: + impl_level = None + + levels = (global_level, intf_level, impl_level) + + if "fail" in levels: + return "fail" + elif "warn" in levels: + return "warn" + else: + return "trust" + +#------------------------------------------------------------------------------- +# Descriptors and Wrappers +#------------------------------------------------------------------------------- + +class ValidationDescriptor(object): + """ + This is the common base class for :class:`WarningDescriptor` and + :class:`FailingDescriptor`. The constructor expects the method declaration + ``method`` and the implementing function ``func`` as input. + + The :meth:`__get__` method returns a callable wrapper around the + instance it is called with, the method declaration and the function that + implements the method. It is that object + that gets finally invoked when runtime validation is enabled. + """ + + def __init__(self, method, func): + self.method = method + self.func = func + + def __get__(self, instance, owner): + if instance is not None: + return self._wrap(instance) + else: + raise InternalError("%s() is not a class method." % self.func.func_name) + + def _wrap(self, instance): + return None + +class ValidationWrapper(object): + """ + This is the common base class for :class:`WarningWrapper` and + :class:`FailingWrapper`. Its constructor expects the method declaration, + the implementing function and the instance as input. + """ + def __init__(self, method, func, instance): + self.method = method + self.func = func + self.instance = instance + +class WarningDescriptor(ValidationDescriptor): + def _wrap(self, instance): + return WarningWrapper(self.method, self.func, instance) + +class WarningWrapper(ValidationWrapper): + """ + This wrapper is callable. Its :meth:`__call__` method expects arbitrary + positional and keyword arguments, validates them against the method + declaration using :meth:`Method.validateType`, calls the implementing + function with these arguments and returns whatever it returns, calling + :meth:`Method.validateReturnType`. + + If the validation methods raise a :exc:`~.TypeMismatch` exception the + exception text is logged as a warning, but the normal process of execution + goes on. + """ + def __call__(self, *args, **kwargs): + try: + self.method.validateType(self.func.func_name, *args, **kwargs) + except TypeMismatch, e: + logger.warning(str(e)) + + ret_value = self.func(self.instance, *args, **kwargs) + + try: + self.method.validateReturnType(self.func.func_name, ret_value) + except TypeMismatch, e: + logger.warning(str(e)) + + return ret_value + +class FailingDescriptor(ValidationDescriptor): + def _wrap(self, instance): + return FailingWrapper(self.method, self.func, instance) + +class FailingWrapper(ValidationWrapper): + """ + This wrapper is callable. Its :meth:`__call__` method expects arbitrary + positional and keyword arguments, validates them against the method + declaration using :meth:`Method.validateType`, calls the implementing + function with these arguments and returns whatever it returns, calling + :meth:`Method.validateReturnType`. + + If the validation methods raise a :exc:`~.TypeMismatch` exception it will + not be caught and thus cause the program to fail. + """ + def __call__(self, *args, **kwargs): + self.method.validateType(self.func.func_name, *args, **kwargs) + + ret_value = self.func(self.instance, *args, **kwargs) + + self.method.validateReturnType(self.func.func_name, ret_value) + + return ret_value + +#------------------------------------------------------------------------------- +# Config Reader +#------------------------------------------------------------------------------- + +class IntfConfigReader(object): + """ + This is the configuration reader for :mod:`eoxserver.core.interfaces`. + + Its constructor expects a :class:`Config` instance ``config`` as input. + """ + + def __init__(self, config): + self.config = config + + def validate(self): + """ + Validates the configuration. Raises :exc:`~.ConfigError` if the + ``runtime_validation_level`` configuration setting in the + ``core.interfaces`` section contains an invalid value. + """ + value = self.getRuntimeValidationLevel() + + if value and value not in ("trust", "warn", "fail"): + raise ConfigError("'runtime_validation_level' parameter must be one of: 'trust', 'warn' or 'fail'.") + + def getRuntimeValidationLevel(self): + """ + Returns the global runtime validation level setting or ``None`` if it + is not defined. + """ + self.config.getConfigValue("core.interfaces", "runtime_validation_level") diff -Nru eoxserver-0.4.0beta2/eoxserver/core/management.py eoxserver-0.3.2/eoxserver/core/management.py --- eoxserver-0.4.0beta2/eoxserver/core/management.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/core/management.py 2013-12-10 14:57:00.000000000 +0000 @@ -1,4 +1,5 @@ #------------------------------------------------------------------------------- +# $Id$ # # Project: EOxServer # Authors: Stephan Krause @@ -11,8 +12,8 @@ # 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 +# 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 diff -Nru eoxserver-0.4.0beta2/eoxserver/core/migrations/0001_initial.py eoxserver-0.3.2/eoxserver/core/migrations/0001_initial.py --- eoxserver-0.4.0beta2/eoxserver/core/migrations/0001_initial.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/core/migrations/0001_initial.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -from south.utils import datetime_utils as datetime -from south.db import db -from south.v2 import SchemaMigration -from django.db import models - - -class Migration(SchemaMigration): - - def forwards(self, orm): - pass - - def backwards(self, orm): - pass - - models = { - - } - - complete_apps = ['core'] \ No newline at end of file diff -Nru eoxserver-0.4.0beta2/eoxserver/core/models.py eoxserver-0.3.2/eoxserver/core/models.py --- eoxserver-0.4.0beta2/eoxserver/core/models.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/core/models.py 2013-12-10 14:57:00.000000000 +0000 @@ -1,9 +1,9 @@ #------------------------------------------------------------------------------- +# $Id$ # # Project: EOxServer -# Authors: Fabian Schindler +# Authors: Stephan Krause # Stephan Meissl -# Stephan Krause # #------------------------------------------------------------------------------- # Copyright (C) 2011 EOX IT Services GmbH @@ -11,8 +11,8 @@ # 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 +# 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 @@ -27,43 +27,34 @@ # THE SOFTWARE. #------------------------------------------------------------------------------- - -from django.contrib.gis.db import models - - -class Castable(models.Model): - """ Model mix-in for 'castable' types. With this MixIn, type information and - completed models can be retrieved. - """ - - @property - def real_type(self): - # if not saved, use the actual type - if not self.id: - return type(self) - - return self.type_registry[self.real_content_type] - - def __init__(self, *args, **kwargs): - super(Castable, self).__init__(*args, **kwargs) - if not self.id: - for type_id, cls in self.type_registry.items(): - if cls == type(self): - self.real_content_type = type_id - break - - def cast(self, refresh=False): - """'Cast' the model to its actual type, if it is not already. This - invokes a database lookup if the real type is not the same as the type - of the current model. - """ - - # don't perform a cast if not necessary - real_type = self.real_type - if real_type == type(self) and not refresh: - return self - - return self.real_type.objects.get(pk=self.pk) - - class Meta: - abstract = True +from django.db import models +from django.contrib.contenttypes.models import ContentType + +class Implementation(models.Model): + intf_id = models.CharField(max_length=256) + impl_id = models.CharField(max_length=256, unique=True) + +class Component(Implementation): + enabled = models.BooleanField(default=False) + +class ResourceClass(Implementation): + content_type = models.ForeignKey(ContentType) + +class Resource(models.Model): + pass + +class Relation(models.Model): + rel_class = models.CharField(max_length=64) + enabled = models.BooleanField(default=False) + + subj = models.ForeignKey(Component, related_name="relations") + + obj = models.ForeignKey(Resource, related_name="relations") + +class ClassRelation(models.Model): + rel_class = models.CharField(max_length=64) + enabled = models.BooleanField(default=False) + + subj = models.ForeignKey(Component, related_name="class_relations") + + obj = models.ForeignKey(ResourceClass, related_name="class_relations") diff -Nru eoxserver-0.4.0beta2/eoxserver/core/readers.py eoxserver-0.3.2/eoxserver/core/readers.py --- eoxserver-0.4.0beta2/eoxserver/core/readers.py 1970-01-01 00:00:00.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/core/readers.py 2013-12-10 14:57:00.000000000 +0000 @@ -0,0 +1,72 @@ +#------------------------------------------------------------------------------- +# $Id$ +# +# Project: EOxServer +# Authors: Stephan Krause +# Stephan Meissl +# +#------------------------------------------------------------------------------- +# Copyright (C) 2011 EOX IT Services GmbH +# +# 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 of this Software or works derived from this 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. +#------------------------------------------------------------------------------- + +""" +This module defines an interface for configuration readers. +""" + +from eoxserver.core.interfaces import Method, ObjectArg +from eoxserver.core.config import Config +from eoxserver.core.registry import RegisteredInterface + +class ConfigReaderInterface(RegisteredInterface): + """ + This interface is intended to provide a way to validate and access the + system configuration to modules which rely on configuration parameters. It + defines only one mandatory :meth:`validate` method, but developers are free + to add methods or attributes that give easy access to the underlying + configuration values. + + .. method:: validate(config) + + This method shall validate the given system configuration ``config`` (a + :class:`~.Config` instance). It shall raise a :exc:`~.ConfigError` + exception in case the configuration with respect to the sections and + parameters concerned by the implementation is invalid. It has no return + value. + + The :meth:`validate` method is called automatically at system startup or + configuration reset. If it fails system startup or reset will not + succeed. So please be careful to raise :exc:`~.ConfigError` only in + situations + + * when the components that need the parameter(s) are enabled + * when the configuration will always lead to an error + + Otherwise, configuration errors of one optional module might break the + whole system. + """ + + REGISTRY_CONF = { + "name": "Config Reader Interface", + "intf_id": "core.readers.ConfigReader", + "binding_method": "direct" + } + + validate = Method(ObjectArg("config", arg_class=Config)) diff -Nru eoxserver-0.4.0beta2/eoxserver/core/records.py eoxserver-0.3.2/eoxserver/core/records.py --- eoxserver-0.4.0beta2/eoxserver/core/records.py 1970-01-01 00:00:00.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/core/records.py 2013-12-10 14:57:00.000000000 +0000 @@ -0,0 +1,517 @@ +#------------------------------------------------------------------------------- +# $Id$ +# +# Project: EOxServer +# Authors: Stephan Krause +# Stephan Meissl +# +#------------------------------------------------------------------------------- +# Copyright (C) 2011 EOX IT Services GmbH +# +# 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 of this Software or works derived from this 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. +#------------------------------------------------------------------------------- + +""" +This module provides interfaces as well as a simple implementation for record +wrappers. The design objective for this module was to provide a more lightweight +alternative to resource wrappers based on :mod:`eoxserver.core.resources`. + +Record wrappers shall couple data stored in the database with +additional application logic. They are lazy in the sense that data assigned to +the wrapper is not written to the database immediately. They are also immutable, +i.e. once they have been initialized their data cannot be changed any more. Last +but not least, they are able to cope with non-abstract model inheritance. +""" + +from eoxserver.core.system import System +from eoxserver.core.interfaces import * +from eoxserver.core.registry import RegisteredInterface, FactoryInterface +from eoxserver.core.exceptions import InternalError, UniquenessViolation + +#------------------------------------------------------------------------------- +# Interface declarations +#------------------------------------------------------------------------------- + +class RecordWrapperInterface(RegisteredInterface): + """ + This class defines an interface for simple lazy record wrappers which are + used throughout EOxServer to couple data and metadata stored in the + configuration database with additional application logic. + + Implementations of this interface shall wrap a model record and couple it + with additional attributes and dynamic behaviour. The wrapper shall be lazy, + i.e. any changes to the model record or to the attributes will not affect + the database until the programmer explicitly calls :meth:`save`, + :meth:`getRecord`. + + .. method:: getType + + This method shall return the type of record the wrapper represents. This + method is needed especially by factories which return multiple classes + of :class:`RecordWrapperInterface` implementations that wrap models + inheriting from a common base model. + + .. method:: setRecord(record) + + This method shall initialize the record wrapper with an existing model + record. It shall raise :exc:`~.InternalError` in case the record is + of an incompatible type + + .. method:: setAttrs(**kwargs) + + This method shall initialize the record wrapper with implementation + dependent attributes. It shall raise :exc:`~.InternalError` in case + there are mandatory attributes missing. + + .. method:: sync(fetch_existing=False) + + Synchronize with the database, i.e. fetch or create a record with the + instance attributes. + + The method shall respect uniqueness constraints on the underlying model, + i.e. return an existing record matching the instance attributes if + possible and create a new one only if all constraints are satisfied. In + case neither is possible :exc:`~.UniquenessViolation` shall be raised. + + If the optional ``fetch_existing`` argument is set to ``True``, try to + get an existing record with the same attributes from the database even + if no uniqueness constraints apply. If there is none, create a new one. + + .. method:: getRecord(fetch_existing=False) + + Return the record wrapped by the implementation. If none has been defined + yet, fetch or create one with the instance attributes. + + The method shall respect uniqueness constraints on the underlying model, + i.e. return an existing record matching the instance attributes if + possible and create a new one only if all constraints are satisfied. + In case neither is possible :exc:`~.UniquenessViolation` shall be raised. + + If the optional ``fetch_existing`` argument is set to ``True``, try to + get an existing record with the same attributes from the database even + if no uniqueness constraints apply. If there is none, create a new one. + + .. method:: delete(commit=True) + + Delete the model record and perform any related logic. + + This method accepts an optional boolean parameter ``commit`` which + defaults to ``True``. If it is set to ``False`` do not actually delete + the record, but do perform the additional logic. This is useful for + bulk deletion by factories; it should be used with great care as it might + leave the system in an inconsistent state if the database record is not + removed afterwards. + + """ + + REGISTRY_CONF = { + "name": "Record Wrapper Interface", + "intf_id": "core.records.RecordWrapperInterface", + "binding_method": "factory" + } + + getType = Method( + returns = StringArg("@return") + ) + + setRecord = Method( + ObjectArg("record") + ) + + setAttrs = Method( + KwArgs("kwargs") + ) + + sync = Method( + BoolArg("fetch_existing", default=False) + ) + + getRecord = Method( + BoolArg("fetch_existing", default=False), + returns = ObjectArg("@return") + ) + + delete = Method( + BoolArg("commit", default=True) + ) + +class RecordWrapperFactoryInterface(FactoryInterface): + """ + This is the interface for factories returning record wrappers, i.e. + implementations of :class:`RecordWrapperInterface`. It inherits from + :class:`~.FactoryInterface`. + + .. method:: create(**kwargs) + + Create a record wrapper with the given attributes. The keyword arguments + accepted by this method shall correlate to the attribute keyword + arguments accepted by the underlying :class:`ResourceWrapperInterface` + implementations. + + For factories that generate different types of record wrappers a + mandatory ``type`` keyword argument shall be required that shall + correlate to the return value of the + :meth:`ResourceWrapperInterface.getType` method of the desired record + wrapper type. + + .. method:: getOrCreate(**kwargs) + + Get or create a record wrapper with the given attributes. That is, + if a database record matching all the given attributes exists, return + a wrapper with this record, otherwise create a new record. The keyword + arguments accepted by this method shall correlate to the attribute + keyword arguments accepted by the underlying + :class:`ResourceWrapperInterface` implementations. + + For factories that generate different types of record wrappers a + mandatory ``type`` keyword argument shall be required that shall + correlate to the return value of the + :meth:`ResourceWrapperInterface.getType` method of the desired record + wrapper type. + + .. method:: update(**kwargs) + + Update model records in bulk. Return the record wrappers for the + updated records. + + .. method:: delete(**kwargs) + + Delete model records in bulk and apply any additional logic defined by + the specific record wrapper implementations; see + :meth:`RecordWrapperInterface.delete`. + """ + + REGISTRY_CONF = { + "name": "Record Wrapper Factory Interface", + "intf_id": "core.records.RecordWrapperFactoryInterface", + "binding_method": "direct" + } + + create = Method( + KwArgs("kwargs"), + returns = ObjectArg("@return") + ) + + getOrCreate = Method( + KwArgs("kwargs"), + returns = ObjectArg("@return") + ) + + update = Method( + KwArgs("kwargs"), + returns = ListArg("@return") + ) + + delete = Method( + KwArgs("kwargs") + ) + +#------------------------------------------------------------------------------- +# Record wrapper base class +#------------------------------------------------------------------------------- + +class RecordWrapper(object): + """ + This is a common base class for :class:`RecordWrapperInterface` + implementations. + + Concrete implementations may derive from it overriding the respective + methods. + """ + + def __init__(self): + self.record = None + + def getType(self): + """ + This method shall return the type of record the wrapper represents. + Raises :exc:`~.InternalError` by default. + """ + + raise InternalError("Not implemented.") + + def setRecord(self, record): + """ + Assign the model record ``record`` to the instance. This method raises + :exc:`~.InternalError` if the record does not validate. + """ + self._validate_record(record) + + self._set_record(record) + + def setAttrs(self, **kwargs): + """ + Assign the attributes given as keyword arguments to the instance. + This method raises :exc:`~.InternalError` if attributes do not validate. + """ + self._validate_attrs(**kwargs) + + self._set_attrs(**kwargs) + + def sync(self, fetch_existing=False): + """ + """ + if not self.record: + unique_record = self._fetch_unique_record() + + if unique_record: + self.record = unique_record + else: + if fetch_existing: + self.record = self._fetch() + + if not self.record: + self._create_record() + + def getRecord(self, fetch_existing=False): + """ + Get the model record wrapped by the instance (i.e. an instance of a + subclass of :class:`django.db.models.Model`). This method calls + :meth:`sync` to fetch or create a record if none has been defined. + The ``fetch_existing`` argument is parsed to :meth:`sync`. + """ + self.sync(fetch_existing) + + return self.record + + def delete(self, commit=True): + """ + Delete the model record wrapped by the instance from the database and + perform any additional logic related to the deletion. Do nothing if + there is no model record defined. See + :meth:`ResourceWrapperInterface.delete` for a description of the + ``commit`` parameter. + + """ + + if self.record: + self._pre_delete() + + if commit: + self.record.delete() + + del self.record + + + def _validate_record(self, record): + # used internally by :meth:`setRecord` to validate record; expected to + # raise :exc:`~.InternalError` in case the record does not validate; + + pass + + def _set_record(self, record): + # used internally by :meth:`setRecord` to assign record to the instance + + self.record = record + + def _validate_attrs(self, **kwargs): + # used internally to validate the attributes to be assigned to the + # wrapper instance with :meth:`setAttrs`; expected to raise + # :exc:`~.InternalError` in case the attributes do not validate + + pass + + def _set_attrs(self, **kwargs): + # used internally by :meth:`setAttrs` to assign attributes to the + # instance; needs to be overridden by subclasses; raises + # :exc:`~.InternalError` by default + + raise InternalError("Not implemented.") + + def _fetch_unique_record(self): + # Fetch a record from the database and check it against the uniqueness + # constraints; returns ``None`` if there is no existing record; raise + # :exc:`~.UniquenessViolation` if uniqueness constraints cannot be + # satisfied + + raise InternalError("Not implemented.") + + def _fetch(self, *fields): + # Fetch a record from the database; the positional arguments are expected + # to be a subset of the wrapper instance attribute names; if they are + # omitted a record matching all instance attributes is searched; if + # there is no matching record, ``None`` shall be returned + + if fields: + query = self._get_query(fields) + else: + query = self._get_query() + + records = self._get_query_set(query) + + if records.count() == 0: + return None + else: + return records[0] + + def _get_query(self, fields=None): + # return a dictionary of field lookups to be submitted to a Django + # object managers :meth:`filter` method; + + raise InternalError("Not implemented.") + + def _get_query_set(self, query): + # return a query set of records matching the lookup parameters + # submitted with the ``query`` argument (see :meth:`_get_query`) + + raise InternalError("Not implemented.") + + def _create_record(self): + # used internally by :meth:`sync` and :meth:`getRecord` to create + # model record from instance attributes + + raise InternalError("Not Implemented.") + + def _pre_delete(self): + # used internally by :meth:`delete` to perform any additional logic + # before deleting the database record wrapped by the implementation; + # be sure to raise an appropriate exception if this method fails and + # record deletion shall be prevented; be sure to be able to roll back + # changes if needed in order to keep the wrapper in a consistent state + + raise InternalError("Not implemented.") + +#------------------------------------------------------------------------------- +# Record wrapper factory base class +#------------------------------------------------------------------------------- + +class RecordWrapperFactory(object): + """ + This factory gives access to record wrappers. + """ + + def __init__(self): + self.impls = {} + + for Impl in System.getRegistry().getFactoryImplementations(self): + self.impls[Impl().getType()] = Impl + + def get(self, **kwargs): + """ + Get a record wrapper for a database model record. This method + accepts either one of the following two keyword arguments: + + * ``pk``: primary key of a record + * ``record``: a model record + + :exc:`~.InternalError` is raised if none of these is given. The + data package wrapper returned will be of the right type for the given + model record. + """ + if "pk" in kwargs: + pk = kwargs["pk"] + + record = self._get_record_by_pk(pk) + + elif "record" in kwargs: + record = kwargs["record"] + + else: + raise InternalError( + "Either 'pk' or 'record' keyword arguments needed for getting a record wrapper." + ) + + wrapper = self._get_record_wrapper(record) + + wrapper.setRecord(record) + + return wrapper + + def find(self, **kwargs): + """ + Find database model records and return the corresponding record wrappers. + Not yet implemented. + """ + + pass # TODO + + def create(self, **kwargs): + """ + Create a data package wrapper instance of a given type. This method + expects a ``type`` keyword argument that indicates the data package + type of the wrapper to be created. All other keyword arguments are + passed on to the :meth:`~RecordWrapper.setAttrs` method of the + respective wrapper class. + + :exc:`~.InternalError` is raised if the ``type`` keyword argument is + missing or does not contain a valid type name. :exc:`~.InternalError` + exceptions raised by :meth:`RecordWrapper.setAttrs` are passed on as + well. + """ + + if "type" in kwargs: + wrapper_type = kwargs["type"] + else: + raise InternalError("Missing mandatory 'type' parameter.") + + if wrapper_type not in self.impls: + raise InternalError( + "Unknown wrapper type '%s'." % wrapper_type + ) + else: + wrapper = self.impls[wrapper_type]() + wrapper.setAttrs(**kwargs) + + return wrapper + + def getOrCreate(self, **kwargs): + """ + Get a wrapper for an existing record with the given attributes or + create a new one. This calls :meth:`create` and + :meth:`RecordWrapper.sync`. The returned wrapper will always + contain a database record (it is not lazy). + + :exc:`~.InternalError` is raised if there are mandatory attribute + keyword arguments missing and :exc:`~.UniquenessViolation` if the + record could not be created due to unqiueness constraints. + """ + wrapper = self.create(**kwargs) + + wrapper.sync(fetch_existing=True) + + return wrapper + + def update(self, **kwargs): + """ + Update model records in bulk. Not yet implemented. + """ + + pass # TODO + + def delete(self, **kwargs): + """ + Delete model records in bulk. Not yet implemented. + """ + + pass # TODO + + def _get_record_by_pk(self, pk): + # retrieve the model record from the respective database model (table) + # using the given primary key ``pk``; this method may return a record + # from a base model in case of non-abstract model inheritance; in the + # latter case :meth:`_get_record_wrapper` must be able to read the + # subclass type from the record + + raise InternalError("Not implemented.") + + def _get_record_wrapper(self, record): + # return the correct wrapper for the record respecting the record type + # in case of non-abstract model inheritanze and initialize it + + raise InternalError("Not implemented.") diff -Nru eoxserver-0.4.0beta2/eoxserver/core/registry.py eoxserver-0.3.2/eoxserver/core/registry.py --- eoxserver-0.4.0beta2/eoxserver/core/registry.py 1970-01-01 00:00:00.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/core/registry.py 2013-12-10 14:57:00.000000000 +0000 @@ -0,0 +1,1039 @@ +#------------------------------------------------------------------------------- +# $Id$ +# +# Project: EOxServer +# Authors: Stephan Krause +# Stephan Meissl +# +#------------------------------------------------------------------------------- +# Copyright (C) 2011 EOX IT Services GmbH +# +# 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 of this Software or works derived from this 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. +#------------------------------------------------------------------------------- + +""" +This module contains the implementation of the registry as well as associated +interface declarations. The registry is the core component of EOxServer that +links different parts of the system together. The registry allows for +components to bind to implementations of registered interfaces. It supports +modularity, extensibility and flexibility of EOxServer. +""" + +import imp +import os.path +from inspect import isclass + +from django.conf import settings + +from eoxserver.core.models import Component +from eoxserver.core.exceptions import (InternalError, ConfigError, + ImplementationNotFound, ImplementationAmbiguous, + ImplementationDisabled, BindingMethodError +) +from eoxserver.core.interfaces import * +from eoxserver.core.util.filetools import findFiles, pathToModuleName + +class Registry(object): + """ + The :class:`Registry` class implements the functionalities for detecting, + registering, finding and binding to implementations of registered + interfaces. It is instantiated by :class:`eoxserver.core.system.System` + during the startup process. + + The constructor expects a :class:`~.Config` instance as input. The values + will be validate and read using a :class:`RegistryConfigReader` instance. + """ + + def __init__(self, config): + self.config = config + + self.__intf_index = {} + self.__impl_index = {} + self.__kvp_index = {} + self.__fact_index = {} + + def bind(self, impl_id): + """ + Bind to the implementation with ID ``impl_id``. This method returns + a new instance of the requested implementation if it is enabled. + + If the implementation is disabled :exc:`~.ImplementationDisabled` will + be raised. If the ID ``impl_id`` is not known to the registry + :exc:`ImplementationNotFound` will be raised. + """ + + if impl_id in self.__impl_index: + if self.__impl_index[impl_id]["enabled"]: + return self.__impl_index[impl_id]["cls"]() + else: + raise ImplementationDisabled( + "Implementation '%s' is disabled." % impl_id + ) + else: + raise ImplementationNotFound(impl_id) + + def getFromFactory(self, factory_id, params): + """ + Get an implementation instance from the factory with ID ``factory_id`` + using the parameter dictionary ``params``. This is a shortcut which + binds to the factory and calls its :meth:`~FactoryInterface.get` method + then. + + :exc:`~.InternalError` will be raised if required arguments are missing + in the ``params`` dictionary. :exc:`~.ImplementationDisabled` will be + raised if either the factory or the appropriate implementation are + disabled. :exc:`~.ImplementationNotFound` will be raised if either + the factory or the appropriate implementation are unknown to the + registry. + """ + + factory = self.bind(factory_id) + + return factory.get(**params) + + def findAndBind(self, intf_id, params): + """ + This method finds implementations based of a registered interface + with ID ``intf_id`` using the parameter dictionary ``params`` and + returns an instance of the matching implementation. This + works only for the ``kvp`` and ``testing`` binding methods, in other + cases :exc:`~.BindingMethodError` will be raised. + + If the binding method of the interface is ``kvp`` the ``params`` + dictionary must map the registry keys defined in the interface + declaration to values. The KVP combination will be compared with the + values given in the respective implementations. If a matching + implementation is found an instance will be returned, otherwise + :exc:`~.ImplementationNotFound` is raised. If the class found is + disabled :exc:`~.ImplementationDisabled` is raised. + + If the binding method of the interface is ``testing`` the ``params`` + dictionary will be passed to the :meth:`~TestingInterface.test` method + of the respective implementations. If no implementation matches + :exc:`~.ImplementationNotFound` will be raised. If more than one are + found :exc:`~.ImplementationAmbiguous` will be raised. + """ + + if intf_id in self.__intf_index: + InterfaceCls = self.__intf_index[intf_id]["intf"] + if InterfaceCls.getBindingMethod() == "direct": + raise BindingMethodError("You have to bind directly to implementations of '%s'" % intf_id) + elif InterfaceCls.getBindingMethod() == "kvp": + ImplementationCls = self.__find_by_values( + InterfaceCls, params + ) + elif InterfaceCls.getBindingMethod() == "testing": + ImplementationCls = self.__find_by_test( + self.__intf_index[intf_id]["impls"], + params + ) + elif InterfaceCls.getBindingMethod() == "factory": + raise BindingMethodError("The registry cannot generate '%s' implementations. Use getFromFactory() instead." % intf_id) + + return ImplementationCls() + else: + raise InternalError("Unknown interface ID '%s'" % intf_id) + + def findImplementations(self, intf_id, params=None, include_disabled=False): + """ + This method returns a list of implementations of a given interface. + It requires the interface ID as a parameter. + + Furthermore, a parameter dictionary can be passed to the method. The + results will then be filtered according to these parameters. The + dictionary does not have to contain all the parameters defined by the + interface; in case some parameters are omitted, the result list may + contain several different implementations. + + Third is an optional ``include_disabled`` parameter with defaults to + ``False``. If ``True``, disabled implementations will be reported as + well. + + An :exc:`~.InternalError` is raised if parameters are passed to the + method that are not defined in the interface declaration or not + recognized by the interface :meth:`test` method. + """ + + if intf_id in self.__intf_index: + InterfaceCls = self.__intf_index[intf_id]["intf"] + if params is not None: + _params = params + else: + _params = {} + + if InterfaceCls.getBindingMethod() == "direct" or \ + InterfaceCls.getBindingMethod() == "factory": + impls = self.__find_all_impls( + self.__intf_index[intf_id]["impls"], + include_disabled + ) + elif InterfaceCls.getBindingMethod() == "kvp": + impls = self.__find_all_by_values( + self.__intf_index[intf_id]["impls"], + _params, + include_disabled + ) + elif InterfaceCls.getBindingMethod() == "testing": + impls = self.__find_all_by_test( + self.__intf_index[intf_id]["impls"], + _params, + include_disabled + ) + + return impls + else: + raise InternalError("Unknown interface ID '%s'" % intf_id) + + def getImplementationIds(self, intf_id, params=None, include_disabled=False): + """ + This method returns a list of implementation IDs for a given interface. + It requires the interface ID as a parameter. + + Furthermore, a parameter dictionary can be passed to the method. The + results will then be filtered according to these parameters. The + dictionary does not have to contain all the parameters defined by the + interface; in case some parameters are omitted, the result list may + contain several different implementations. + + Third is an optional ``include_disabled`` parameter with defaults to + ``False``. If ``True``, disabled implementations will be reported as + well. + + An :exc:`~.InternalError` is raised if parameters are passed to the + method that are not defined in the interface declaration or not + recognized by the interface :meth:`test` method. + """ + impls = self.findImplementations(intf_id, params, include_disabled) + + return [impl.__get_impl_id__() for impl in impls] + + def getRegistryValues(self, intf_id, registry_key, filter=None, include_disabled=False): + """ + This method returns a list of registry values of implementations + of an interface with ``interface_id`` and a registry key + ``registry_key`` defined in the interface declaration. + + With the ``filter`` argument you can impose certain restrictions on the + implementations (registry values) to be returned. It is expected to + contain a dictionary of registry keys and values that the implementation + must expose to be included. + + Using the ``include_disabled`` argument you can determine whether. + + This method raises :exc:`~.InternalError` if the interface ID is + unknown. + """ + if intf_id in self.__intf_index: + InterfaceCls = self.__intf_index[intf_id]["intf"] + + if InterfaceCls.getBindingMethod() == "kvp": + if registry_key not in InterfaceCls.getRegistryKeys(): + raise InternalError("Interface '%s' has no registry key '%s'" % ( + intf_id, registry_key + )) + + else: + if filter: + _filter = filter + else: + _filter = {} + + impls = self.__find_all_by_values( + self.__intf_index[intf_id]["impls"], + _filter + ) + + return [impl.__get_kvps__()[registry_key] for impl in impls] + else: + raise InternalError("Binding method of interface '%s' is '%s', not 'kvp'." % ( + intf_id, + InterfaceCls.getBindingMethod() + )) + else: + raise InternalError("Unknown interface ID '%s'" % intf_id) + + def getFactoryImplementations(self, factory): + """ + Returns a list of implementations for a given factory. + + Raises :exc:`~.InternalError` if the factory is not found in the + registry. + """ + factory_id = factory.__get_impl_id__() + + if factory_id in self.__fact_index: + return [entry["cls"] for entry in self.__fact_index[factory_id]] + else: + raise InternalError("Unknown Factory ID '%s'." % factory_id) + + def load(self): + """ + This method loads the registry, i.e. it scans the modules specified + in the configuration for interfaces and implementations. It is + invoked by the ``~.System`` class upon initialization. + + You should *never* invoke this method directly. Always use + :meth:`~.System.init` to initialize and :meth:`~.System.getRegistry` to + access the registry. + + There are three configuration settings taken into account. + + First, the ``system_modules`` setting in ``default.conf``. These + modules are always loaded and cannot be left aside in individual + instances. + + Second, the ``module_dirs`` setting in the local configuration of the + instance (``eoxserver.conf``) is taken into account. This expected to + be a comma-separated list of directories. These directories and all + the directory trees underneath them are searched for Python modules. + + Third, the ``modules`` setting in ``eoxserver.conf``. This is + expecte to be a comma-separated list of module names which shall be + loaded. + + All modules specified or detected by scanning directories will be + loaded and searched for interfaces descending from + :class:`RegisteredInterface` as well as their implementations. These + will be automatically included in the registry and accessible using + the different binding methods provided. + + As a last step, the registry is synchronized with the database. This + means that the implementation looks up the entries for the different + implementations in the database and determines whether they are + enabled or not. If it finds an implementation which has not yet been + registered it will be saved to the database but disabled by default. + """ + # get module directories from config + reader = RegistryConfigReader(self.config) + reader.validate() + + system_modules = reader.getSystemModules() + module_dirs = reader.getModuleDirectories() + modules = reader.getModules() + + # find modules + for module_dir in module_dirs: + # find .py files + # and append them to the modules list + modules.extend(self.__find_modules(module_dir)) + + # load modules + for module_name in system_modules: + self.__load_module(module_name, strict=True) + + for module_name in modules: + self.__load_module(module_name) + + self.__synchronize() + + self.validate() + + def validate(self): + """ + This method is intended to validate the component configuration. + + It looks up all implementations of :class:`ComponentManagerInterface` + and calls their respective :meth:`~ComponentManagerInterface.validate` + methods. + + At the moment, no component managers are implemented, so this + method does not have any effects. + """ + msgs = [] + + Managers = self.findImplementations("core.registry.ComponentManager") + + for Manager in Managers: + manager = Manager() + try: + manager.validate(self) + except ConfigError, e: + msgs.append(str(e)) + + if len(msgs) > 0: + raise ConfigError("\n".join(msgs)) + + def save(self): + """ + This saves the registry configuration to the database. This means + the status of the enabled / disabled flag for each implementation + will be saved overriding any previous settings stored. + """ + self.validate() + + self.__save_to_db() + + def getImplementationStatus(self, impl_id): + """ + Returns the implementation status (``True`` for enabled, ``False`` for + disabled) for the given implementation ID ``impl_id``. + + Raises :exc:`~.InternalError` if the implementation ID is + unknown. + """ + if impl_id in self.__impl_index: + return self.__impl_index[impl_id]["enabled"] + else: + raise InternalError("Unknown implementation ID '%s'" % impl_id) + + def enableImplementation(self, impl_id): + """ + Changes the implementation status to enabled for implementation ID + ``impl_id``. Note that this change is not automatically stored to + the database (you have to call :meth:`save` to do that). + + Raises :exc:`~.InternalError` if the implementation ID is unknown. + """ + if impl_id in self.__impl_index: + self.__impl_index[impl_id]["enabled"] = True + else: + raise InternalError("Unknown implementation ID '%s'" % impl_id) + + def disableImplementation(self, impl_id): + """ + Changed the implementation status to disable for implementation ID + ``impl_id``. Note that this change is not automatically stored to + the database (you have to call :meth:`save` to do that). + + Raises :exc:`~.InternalError` if the implementation ID is unknown. + """ + if impl_id in self.__impl_index: + self.__impl_index[impl_id]["enabled"] = False + else: + raise InternalError("Unknown implementation ID '%s'" % impl_id) + + def clone(self): + """ + Returns an exact copy of the registry. + """ + + registry = Registry(self.config) + + registry.__impl_index = self.__impl_index + registry.__intf_index = self.__intf_index + registry.__kvp_index = self.__kvp_index + registry.__fact_index = self.__fact_index + + return registry + + def __synchronize(self): + qs = self.__get_from_db() + + for impl_id, entry in self.__impl_index.items(): + if entry["enabled"]: + logging.debug("%s: enabled" % impl_id) + else: + logging.debug("%s: disabled" % impl_id) + + self.__save_diff_to_db(qs) + + def __get_from_db(self): + qs = Component.objects.all() + + for comp in qs: + if comp.impl_id in self.__impl_index: + + # TODO: at the moment, the 'enabled' property is stored + # in each index seperately. Make the indices point to + # the same entry, so 'enabled' does not need to be set + # three times (once for each index) + self.__impl_index[comp.impl_id]["enabled"] = comp.enabled + + intf_id = self.__impl_index[comp.impl_id]["cls"].__get_intf_id__() + entries = self.__intf_index[intf_id]["impls"] + for entry in entries: + if entry["impl_id"] == comp.impl_id: + entry["enabled"] = comp.enabled + + if self.__intf_index[intf_id]["intf"].getBindingMethod() == "kvp": + key = self.__make_kvp_index_key( + intf_id, + self.__intf_index[intf_id]["intf"].getRegistryKeys(), + self.__impl_index[comp.impl_id]["cls"].__get_kvps__() + ) + self.__kvp_index[key]["enabled"] = comp.enabled + + return qs + + def __save_to_db(self, qs=None): + if qs: + _qs = qs + else: + _qs = Component.objects.all() + + for comp in qs: + if comp.impl_id in self.__impl_index and\ + comp.enabled != self.__impl_index[comp.impl_id]["enabled"]: + comp.enabled = self.__impl_index[comp.impl_id]["enabled"] + comp.save() + + self.__save_diff_to_db(_qs) + + def __save_diff_to_db(self, qs=None): + if qs is not None: + db_impl_ids = qs.values_list("impl_id", flat=True) + else: + db_impl_ids = Component.objects.all().values_list("impl_id", flat=True) + + for impl_id in self.__impl_index.keys(): + if impl_id not in db_impl_ids: + Component.objects.create( + impl_id=impl_id, + intf_id=self.__impl_index[impl_id]["cls"].__get_intf_id__(), + enabled=self.__impl_index[impl_id]["enabled"] + ) + + def __find_all_impls(self, entries, include_disabled=False): + if include_disabled: + return [entry["cls"] for entry in entries] + else: + return [entry["cls"] for entry in filter( + lambda entry: entry["enabled"], entries + )] + + def __find_by_values(self, InterfaceCls, kvp_dict, include_disabled=False): + key = self.__make_kvp_index_key( + InterfaceCls.getInterfaceId(), + InterfaceCls.getRegistryKeys(), + kvp_dict + ) + entry = self.__kvp_index.get(key) + + if entry: + if entry["enabled"] or include_disabled: + return entry["cls"] + else: + raise ImplementationDisabled("Implementation '%s' is disabled." % entry["impl_id"]) + else: + raise ImplementationNotFound( + "No implementation found for interface '%s' and parameters: '%s'" % ( + InterfaceCls.getInterfaceId(), + str(kvp_dict) + )) + + def __find_all_by_values(self, entries, kvp_dict, include_disabled=False): + impls = [] + + for entry in entries: + if entry["enabled"] or include_disabled: + impl_dict = entry["cls"].__get_kvps__() + matches = True + + for key, value in kvp_dict.items(): + if key in impl_dict: + if value != impl_dict[key]: + matches = False + break + else: + raise InternalError("Key '%s' not found in registry values of implementation '%s'" % ( + key, entry["impl_id"] + )) + + if matches: + impls.append(entry["cls"]) + + return impls + + def __find_by_test(self, entries, test_params, include_disabled=False): + impls = self.__find_all_by_test(entries, test_params, include_disabled) + + if len(impls) == 0: + raise ImplementationNotFound("") + elif len(impls) == 1: + return impls[0] + else: + raise ImplementationAmbiguous("") + + def __find_all_by_test(self, entries, test_params, include_disabled=False): + impls = [] + + for entry in entries: + if entry["enabled"] or include_disabled: + if entry["cls"]().test(test_params): + impls.append(entry["cls"]) + + return impls + + def __register_implementation(self, ImplementationCls): + # update interface index + self.__add_to_intf_index(ImplementationCls) + + # update implementation index + self.__add_to_impl_index(ImplementationCls) + + # update KVP index + self.__add_to_kvp_index(ImplementationCls) + + # update factory index + self.__add_to_fact_index(ImplementationCls) + + def __register_interface(self, InterfaceCls): + logging.debug("Registry.__register_interface(): intf_id: %s" % InterfaceCls.getInterfaceId()) + + intf_id = InterfaceCls.getInterfaceId() + + if intf_id in self.__intf_index: + if InterfaceCls is not self.__intf_index[intf_id]["intf"]: + raise InternalError("Duplicate interface ID '%s' for '%s' in module '%s' and '%s' in module '%s'" % ( + intf_id, + InterfaceCls.__name__, + InterfaceCls.__module__, + self.__intf_index[intf_id]["intf"].__name__, + self.__intf_index[intf_id]["intf"].__module__ + )) + else: + self.__intf_index[intf_id] = { + "intf": InterfaceCls, + "impls": [] + } + + def __add_to_intf_index(self, ImplementationCls): + intf_id = ImplementationCls.__get_intf_id__() + impl_id = ImplementationCls.__get_impl_id__() + + if intf_id in self.__intf_index: + if self.__intf_index[intf_id]["intf"] is ImplementationCls.__ifclass__: + self.__intf_index[intf_id]["impls"].append( + self.__get_index_entry(ImplementationCls) + ) + else: + raise InternalError("Duplicate Interface ID '%s' for '%s' in '%s' and '%s' in '%s'." % ( + intf_id, + ImplementationCls.__ifclass__.__name__, + ImplementationCls.__ifclass__.__module__, + self.__intf_index[intf_id]["intf"].__name__, + self.__intf_index[intf_id]["intf"].__module__ + )) + else: + self.__intf_index[intf_id] = { + "intf": ImplementationCls.__ifclass__, + "impls": [self.__get_index_entry(ImplementationCls)] + } + + def __add_to_impl_index(self, ImplementationCls): + intf_id = ImplementationCls.__get_intf_id__() + impl_id = ImplementationCls.__get_impl_id__() + + if impl_id in self.__impl_index: + if self.__impl_index[impl_id] is not ImplementationCls: + raise InternalError("Duplicate implementation id '%s' defined in modules '%s' and '%s'" % ( + impl_id, + self.__impl_index[impl_id]["cls"].__module__, + ImplementationCls.__module__ + )) + else: + self.__impl_index[impl_id] = self.__get_index_entry(ImplementationCls) + + def __add_to_kvp_index(self, ImplementationCls): + + intf_id = ImplementationCls.__get_intf_id__() + impl_id = ImplementationCls.__get_impl_id__() + + if ImplementationCls.__ifclass__.getBindingMethod() == "kvp": + key = self.__make_kvp_index_key( + intf_id, + ImplementationCls.__ifclass__.getRegistryKeys(), + ImplementationCls.__get_kvps__() + ) + if key not in self.__kvp_index: + self.__kvp_index[key] = self.__get_index_entry(ImplementationCls) + else: + if self.__kvp_index[key] is not ImplementationCls: + # TODO: conflict resolution mechanisms + raise InternalError("Conflicting implementations for Interface '%s': Same KVP values for '%s' in '%s' and '%s' in '%s'" % ( + intf_id, + impl_id, + ImplementationCls.__module__, + self.__kvp_index[key]["cls"].__get_impl_id__(), + self.__kvp_index[key]["cls"].__module__ + )) + + def __add_to_fact_index(self, ImplementationCls): + if ImplementationCls.__ifclass__.getBindingMethod() == "factory": + + + for factory_id in ImplementationCls.__get_factory_ids__(): + logging.debug("Registry.__add_to_fact_index(): adding '%s' to '%s'" % ( + ImplementationCls.__get_impl_id__(), + factory_id + )) + + if factory_id in self.__fact_index: + self.__fact_index[factory_id].append( + self.__get_index_entry(ImplementationCls) + ) + else: + self.__fact_index[factory_id] = [ + self.__get_index_entry(ImplementationCls) + ] + + def __get_index_entry(self, ImplementationCls, enabled=False): + return { + "cls": ImplementationCls, + "impl_id": ImplementationCls.__get_impl_id__(), + "enabled": enabled + } + + def __make_kvp_index_key(self, intf_id, registry_keys, kvp_dict): + key_list = [intf_id] + + if registry_keys is None: + raise InternalError("Interface '%s' has no registry keys." % intf_id) + + for registry_key in registry_keys: + if registry_key in kvp_dict: + key_list.append((kvp_dict[registry_key], registry_key)) + else: + raise InternalError("Missing registry key '%s'." % registry_key) + + return tuple(key_list) + + def __find_modules(self, dir): + # check if dir is a subdirectory of the eoxserver or instance + # directory + abs_path = os.path.abspath(dir) + + if abs_path.startswith(settings.PROJECT_DIR) or \ + abs_path.startswith(self.config.getEOxSPath()): + module_files = findFiles(abs_path, "*.py") + + try: + return [pathToModuleName(module_file) for module_file in module_files] + except InternalError, e: + raise ConfigError(str(e)) + else: + raise ConfigError("Can search for extending modules and plugins in subdirecories of EOxServer distribution and instance dir only.") + + def __load_module(self, module_name, strict=False): + try: + # TODO: use imp module here (it reloads modules, which is more close to what we want) + module = __import__(module_name, globals(), locals(), []) + + for sub_module_name in module_name.split(".")[1:]: + module = getattr(module, sub_module_name) + #f, path, desc = imp.find_module(module_name) + #module = imp.load_module(module_name, f, path, desc) + + except Exception, e: + if strict: + raise InternalError("Could not load required module '%s'. Error was: %s" % ( + module_name, str(e) + )) + else: + # NOTE: a check for consistency will be applied later + # on; if an enabled component is missing, an exception + # will be raised by the final validation routine + logging.warning("Could not load module '%s'. Error was: %s" % ( + module_name, str(e) + )) + return + + for attr in module.__dict__.values(): + if isclass(attr) and issubclass(attr, RegisteredInterface): + self.__register_interface(attr) + elif hasattr(attr, "__ifclass__") and\ + hasattr(attr, "__rconf__") and\ + hasattr(attr, "__get_intf_id__") and\ + hasattr(attr, "__get_impl_id__") and\ + hasattr(attr, "__get_kvps__"): + self.__register_implementation(attr) + +class RegisteredInterfaceMetaClass(InterfaceMetaClass): + def __new__(cls, name, bases, class_dict): + if "REGISTRY_CONF" in class_dict: + local_conf = class_dict["REGISTRY_CONF"] + else: + raise InternalError("Every interface needs a 'REGISTRY_CONF' dictionary.") + + cls._validateLocalRegistryConf(local_conf) + + conf = cls._mergeConfs(local_conf, bases, "__rconf__") + + cls._validateRegistryConf(conf) + + class_dict["__rconf__"] = conf + + return InterfaceMetaClass.__new__(cls, name, bases, class_dict) + + @classmethod + def _validateLocalRegistryConf(mcls, local_conf): + if "name" not in local_conf: + raise InternalError("Missing 'name' parameter in interface configuration dictionary.") + + if "intf_id" not in local_conf: + raise InternalError("Missing 'intf_id' parameter in interface configuration dictionary.") + + + @classmethod + def _validateRegistryConf(mcls, conf): + pass + +class RegisteredInterface(Interface): + """ + This class is the base class for all interfaces to be registered in the + registry. All interfaces whose implementations shall be registered must + be derived from :class:`RegisteredInterface`. + + All interfaces derived from :class:`RegisteredInterface` must contain a + ``REGISTRY_CONF`` dictionary. See the introduction for details. + """ + + __metaclass__ = RegisteredInterfaceMetaClass + + REGISTRY_CONF = { + "name": "Abstract Registered Interface base class", + "intf_id": "core.registy.Registered", + "binding_method": "kvp" + } + + @classmethod + def _getClassDict(InterfaceCls, ImplementationCls, bases): + class_dict = super(RegisteredInterface, InterfaceCls)._getClassDict(ImplementationCls, bases) + + if hasattr(ImplementationCls, "REGISTRY_CONF"): + conf = ImplementationCls.REGISTRY_CONF + else: + raise InternalError("Missing 'REGISTRY_CONF' configuration dictionary in implementing class '%s'." % ImplementationCls.__name__) + + InterfaceCls._validateImplementationConf(conf) + + intf_id = InterfaceCls.getInterfaceId() + impl_id = conf["impl_id"] + kvps = conf.get("registry_values") + factory_ids = conf.get("factory_ids", ()) + + class_dict.update({ + "__rconf__": conf, + "__get_intf_id__": classmethod(lambda cls: intf_id), + "__get_impl_id__": classmethod(lambda cls: impl_id), + "__get_kvps__": classmethod(lambda cls: kvps), + "__get_factory_ids__": classmethod(lambda cls: factory_ids) + }) + + return class_dict + + @classmethod + def _validateImplementationConf(InterfaceCls, conf): + if "name" not in conf: + raise InternalError("Missing 'name' parameter in implementation configuration dictionary.") + + if "impl_id" not in conf: + raise InternalError("Missing 'impl_id' parameter in implementation configuration dictionary.") + + if InterfaceCls.getBindingMethod() == "kvp": + if "registry_values" not in conf: + raise InternalError("Missing 'registry_values' parameter in implementation configuration dictionary.") + + if not InterfaceCls._keysMatch(conf["registry_values"].keys(), InterfaceCls.getRegistryKeys()): + raise InternalError("Registry keys in implementation configuration dictionary for '%s' do not match interface definition" % conf["impl_id"]) + + @classmethod + def _keysMatch(InterfaceCls, impl_keys, intf_keys): + return (len(impl_keys) == 0 and len(intf_keys) == 0) or\ + (all(map(lambda key: key in intf_keys, impl_keys)) and \ + all(map(lambda key: key in impl_keys, intf_keys))) + + @classmethod + def getInterfaceId(cls): + return cls.__rconf__["intf_id"] + + @classmethod + def getBindingMethod(cls): + return cls.__rconf__["binding_method"] + + @classmethod + def getRegistryKeys(cls): + if "registry_keys" in cls.__rconf__: + return cls.__rconf__["registry_keys"] + else: + return [] + +class TestingInterface(RegisteredInterface): + """ + This class is a descendant of :class:`RegisteredInterface` that adds + a single method. It is used for binding by test, which enables binding + decisions that cannot easily be implemented by key-value-pair comparisons. + + .. method:: test(params) + + This method is invoked by the registry when determining which + implementation to bind to. Based on the parameter dictionary ``params`` + the method shall decide whether the implementation is applicable and + return ``True``. If it is not applicable the method shall return + ``False``. + """ + REGISTRY_CONF = { + "name": "Registered Testing Interface", + "intf_id": "core.registry.Testing", + "binding_method": "testing" + } + + test = Method( + DictArg("params"), + returns=BoolArg("@return") + ) + +class FactoryInterface(RegisteredInterface): + """ + This is the basic interface for factories. It is a descendant of + :class:`RegisteredInterface`. + + .. method:: get(**kwargs) + + This method shall return an instance of an implementation that matches + the parameters given as keyword arguments. The set of arguments + understood depends on the individual factory and can be found in the + respective documentation. + + The method shall raise an exception if no matching implementation or + instance thereof can be found, or if the choice is ambiguous. + + .. method:: find(**kwargs) + + This method shall return a list of implementation instances that + matches the parameters given as keyword arguments. The set of arguments + understood depends on the individual factory and can be found in the + respective documentation. + """ + + REGISTRY_CONF = { + "name": "Registered Factory Interface", + "intf_id": "core.registry.Factory", + "binding_method": "direct" + } + + get = Method( + KwArgs("kwargs"), + returns=Arg("@return") + ) + + find = Method( + KwArgs("kwargs"), + returns=ListArg("@return") + ) + +class ComponentManagerInterface(RegisteredInterface): + """ + This interface is not in use at the moment. It was intended to provide + an API for controlling the status of a larger set of implementations and + their dependencies, though the concept has never been elaborated. + """ + REGISTRY_CONF = { + "name": "Component Manager Interface", + "intf_id": "core.registry.ComponentManager", + "binding_method": "direct" + } + + getName = Method( + returns=StringArg("@returns") + ) + + getId = Method( + returns=StringArg("@returns") + ) + + enable = Method( + ObjectArg("registry", arg_class=Registry), + BoolArg("cascade", default=False) + ) + + disable = Method( + ObjectArg("registry", arg_class=Registry), + BoolArg("cascade", default=False) + ) + + createRelations = Method() + + enableRelation = Method( + StringArg("obj_id") + ) + + disableRelation = Method( + StringArg("obj_id") + ) + + notify = Method( + ObjectArg("resource"), + StringArg("event") + ) + +class RegistryConfigReader(object): + """ + This class provides some functions for reading configuration settings used + by the :class:`Registry`. + """ + def __init__(self, config): + self.config = config + + def validate(self): + """ + Validates the configuration; a no-op at the moment. + """ + pass + + def getSystemModules(self): + """ + This method returns a list of dotted names of system modules. The + values are read from ``system_modules`` setting in the + ``[core.registry]`` section of the ``default.conf`` configuration file. + + The format of the setting is expected to be a comma-separated list of + the module names. + """ + sys_mod_str = self.config.getDefaultConfigValue("core.registry", "system_modules") + + if sys_mod_str: + return [name.strip() for name in sys_mod_str.split(",")] + else: + return [] + + def getModuleDirectories(self): + """ + This method returns a list of directory paths where to look for + modules to load (see also :meth:`Registry.load`). The values are read + from the ``module_dirs`` setting in the ``[core.registry]`` section of + the instance specific ``eoxserver.conf`` configuration file. + + The format of the setting is expected to be a comma-separated list of + paths. + """ + mod_dir_str = self.config.getConfigValue("core.registry", "module_dirs") + + if mod_dir_str: + return [dir.strip() for dir in mod_dir_str.split(",")] + else: + return [] + + def getModules(self): + """ + This method returs a list of dotted names of modules to be loaded (see + also :meth:`Registry.load`). The values are read from the ``modules`` + setting in the ``[core.registry]`` section of the instance specific + ``eoxserver.conf`` configuration file. + + The format of the setting is expected to be a comma-separated list of + dotted module names. + """ + mod_str = self.config.getConfigValue("core.registry", "modules") + + if mod_str: + return [name.strip() for name in mod_str.split(",")] + else: + return [] + diff -Nru eoxserver-0.4.0beta2/eoxserver/core/resources.py eoxserver-0.3.2/eoxserver/core/resources.py --- eoxserver-0.4.0beta2/eoxserver/core/resources.py 1970-01-01 00:00:00.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/core/resources.py 2013-12-10 14:57:00.000000000 +0000 @@ -0,0 +1,1099 @@ +#------------------------------------------------------------------------------- +# $Id$ +# +# Project: EOxServer +# Authors: Stephan Krause +# Stephan Meissl +# +#------------------------------------------------------------------------------- +# Copyright (C) 2011 EOX IT Services GmbH +# +# 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 of this Software or works derived from this 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. +#------------------------------------------------------------------------------- + +from eoxserver.core.system import System +from eoxserver.core.registry import ( + RegisteredInterface, FactoryInterface +) +from eoxserver.core.interfaces import * +from eoxserver.core.models import ( + Resource, ClassRelation +) +from eoxserver.core.exceptions import ( + InternalError, UnknownAttribute, FactoryQueryAmbiguous +) + +#----------------------------------------------------------------------- +# Interfaces +#----------------------------------------------------------------------- + +class ResourceInterface(RegisteredInterface): + """ + This is the interface for resource wrappers. Resource wrappers add + application logic to database models based on + :class:`eoxserver.core.models.Resource`. + + :class:`ResourceInterface` expects two additional, mandatory + parameters in ``REGISTRY_CONF``: + + * ``model_class``: the model class for the resources wrapped by the + implementation + * ``id_field``: the name of the id field of the implementation + + In order to enforce the relation model defined in + :mod:`eoxserver.core.models` :class:`ResourceInterface` + implementations shall have two different states: if they are + mutable, any operations modifying the underlying model are allowed; + if they are immutable only non-modifying operations are enabled. + + .. method:: setModel(model) + + This method shall be used to set the resource model, which + is expected to be an instance of + :class:`~eoxserver.core.models.Resource` or one of its + subclasses. An :exc:`~.InternalError` shall be raised if the + model class does not match the one defined in the ``model_class`` + registry setting. + + .. method:: getModel + + This method shall return the resource model. In case the resource + is not mutable :exc:`~.InternalError` shall be raised. + + .. method:: createModel(params) + + This method shall create a database model with the data given in + parameter dictionary ``params``. The keys the method understands + may vary from implementation to implementation; they are the same + as for :meth:`updateModel`. In case the resource is not mutable + :exc:`~.InternalError` shall be raised. + + .. method:: updateModel(params) + + This method shall update the database model with the data given + in parameter dictionary ``params``. The keys the method + understands may vary from implementation to implementation; they + are the same as for :meth:`createModel`. Note that the new data + is not saved immediately but only when :meth:`saveModel` is + called. In case the resource is not mutable + :exc:`~.InternalError` shall be raised. + + .. method:: saveModel + + This method shall save the model to the database. In case the + resource is not mutable :exc:`~.InternalError` shall be raised. + + .. method:: deleteModel + + This method shall delete the model from the database. In case the + resource is not mutable :exc:`~.InternalError` shall be raised. + + .. method:: setMutable(mutable) + + This method shall set the mutability status of the resource. The + optional boolean argument ``mutable`` defaults to ``True``. The + implementation shall make sure the mutability status cannot be + overridden once it has been set. In case of an attempt to set it + a second time :exc:`~.InternalError` shall be raised. + + .. method:: getId + + This method shall return the ID of the resource, i.e. the + content of the resource's ``id_field`` field. + + .. method:: getAttrNames + + This method shall return a list of attribute names for the + resource. For each attribute name, a call to :meth:`getAttrField` + reveals the corresponding model field and a call to + :meth:`getAttrValue` returns the attribute value for the + resource. + + .. method:: getAttrField(attr_name) + + This method shall return a the field name for a given attribute + name. + + .. method:: getAttrValue(attr_name) + + This method shall return the attribute value for a given + attribute name. + + .. method:: setAttrValue(attr_name, value) + + This method shall set the attribute with the given name to the + given value. In case the resource is not mutable + :exc:`~.InternalError` shall be raised. + """ + + REGISTRY_CONF = { + "name": "Registered Resource Interface", + "intf_id": "core.resources.ResourceInterface", + "binding_method": "factory" + } + + @classmethod + def _getClassDict(InterfaceCls, ImplementationCls, bases): + class_dict = super(ResourceInterface, InterfaceCls)._getClassDict(ImplementationCls, bases) + + if hasattr(ImplementationCls, "REGISTRY_CONF"): + conf = ImplementationCls.REGISTRY_CONF + else: + raise InternalError("Missing 'REGISTRY_CONF' configuration dictionary in implementing class.") + + model_class = conf["model_class"] + id_field = conf["id_field"] + + class_dict.update({ + '__get_model_class__': classmethod(lambda cls: model_class), + '__get_id_field__': classmethod(lambda cls: id_field) + }) + + return class_dict + + @classmethod + def _validateImplementationConf(InterfaceCls, conf): + super(ResourceInterface, InterfaceCls)._validateImplementationConf(conf) + + if "model_class" not in conf: + raise InternalError("Missing 'model_class' parameter in implementation configuration dictionary") + + if "id_field" not in conf: + raise InternalError("Missing 'id_field' parameter in implementation configuration dictionary") + + #------------------------------------------------------------------- + # Interface declaration + #------------------------------------------------------------------- + + # the implementation should check if the model belongs to the + # correct ResourceClass + setModel = Method( + ObjectArg("model", arg_class=Resource) + ) + + getModel = Method( + returns=ObjectArg("@return", arg_class=Resource, default=None) + ) + + createModel = Method( + DictArg("params") + ) + + updateModel = Method( + DictArg("link_kwargs"), + DictArg("unlink_kwargs"), + DictArg("set_kwargs") + ) + + saveModel = Method() + + deleteModel = Method() + + setMutable = Method( + BoolArg("mutable", default=True) + ) + + getId = Method( + returns=StringArg("@return") + ) + + getAttrNames = Method( + returns=ListArg("@return", arg_class=str) + ) + + getAttrField = Method( + StringArg("attr_name"), + returns = StringArg("@return") + ) + + getAttrValue = Method( + StringArg("attr_name"), + returns=Arg("@return") + ) + + setAttrValue = Method( + StringArg("attr_name"), + Arg("value") + ) + +class ResourceFactoryInterface(FactoryInterface): + """ + This is the interface for resource factories. It extends + :class:`~.FactoryInterface` considerably by adding functionality + to create, update and delete resources. + + .. method:: create(**kwargs) + + This method shall create a resource according to the given + parameters and returns it to the caller. The range of applicable + parameters is defined by each implementation. + + .. method:: update(**kwargs) + + This method shall update a resource or a set of resources + according to the given parameters and return the updated + resources to the caller. The range of applicable parameters is + defined by each implementation. + + .. method:: delete(**kwargs) + + This method shall delete a resource or a set of resources + according to the given parameters. The range of applicable + parameters is defined by each implementation. + + .. method:: getIds(**kwargs) + + This method shall return a list of resource IDs (i.e. the + contents of the resource model's ``id_field`` field, see + :class:`ResourceInterface`) for the resources given by the + + .. method:: getAttrValues(**kwargs) + + This method shall return the values of a given attribute for a + selection of resources. + + .. method:: exists(**kwargs) + + This method shall return ``True`` if there are resources matching + the given search criteria, ``False`` otherwise. + """ + REGISTRY_CONF = { + "name": "Resource factory interface", + "intf_id": "core.resources.ResourceFactoryInterface" + } + + create = Method( + KwArgs("kwargs"), + returns=Arg("@return") + ) + + update = Method( + KwArgs("kwargs"), + returns=ListArg("@return") + ) + + delete = Method( + KwArgs("kwargs") + ) + + getIds = Method( + KwArgs("kwargs"), + returns=ListArg("@return") + ) + + getAttrValues = Method( + KwArgs("kwargs"), + returns=ListArg("@return") + ) + + exists = Method( + KwArgs("kwargs"), + returns=BoolArg("@return") + ) + +#----------------------------------------------------------------------- +# Implementations +#----------------------------------------------------------------------- + +class ResourceWrapper(object): + """ + This is the base class for resource wrapper implementations. + """ + + # FIELDS = {} + + def __init__(self): + self.__model = None + self.__mutable = None + + def setModel(self, model): + """ + Use this function to set the coverage model that shall be + wrapped. + """ + # NOTE: the class method __get_model_class__ is defined in the + # implementation only. + if isinstance(model, self.__class__.__get_model_class__()): + self.__model = model + else: + raise InternalError("Model class mismatch. Expected '%s', got '%s'." % ( + self.__class__.__get_model_class__().__name__, + model.__class__.__name__ + )) + + def getModel(self): + """ + Returns the model wrapped by this implementation. + """ + if self.__mutable: + return self.__model + else: + raise InternalError( + "Cannot access model for immutable resource." + ) + + def createModel(self, params): + """ + This method shall be used to create models for the concrete + coverage type. + """ + + if self.__mutable: + create_dict = self._get_create_dict(params) + self._create_model(create_dict) + self._post_create(params) + self.saveModel() + else: + raise InternalError( + "Cannot create model for immutable resource." + ) + + def updateModel(self, link_kwargs, unlink_kwargs, set_kwargs): + if self.__mutable: + self._updateModel(link_kwargs, unlink_kwargs, set_kwargs) + self.saveModel() + else: + raise InternalError( + "Cannot update model for immutable resource." + ) + + def saveModel(self): + """ + Save the coverage model to the database. + """ + if self.__mutable: + self.__model.full_clean() + self.__model.save() + else: + raise InternalError( + "Cannot save model for immutable resource." + ) + + def deleteModel(self): + """ + Delete the coverage model. + """ + if self.__mutable: + self.__model.delete() + else: + raise InternalError( + "Cannot delete model for immutable resource." + ) + + def getId(self): + """ + This method shall return the model ID, i.e. the content of + its ``id_field`` field. Child classes may override it in order + to implement more efficient data access. + """ + return getattr(self.__model, self.__class__.__get_id_field__()) + + def setMutable(self, mutable=True): + """ + This method sets the mutability status of the resource. It + accepts one optional boolean argument ``mutable`` which defaults + to ``True``. The mutability status can be set only once for + each resource, attempts to change it will cause an + :exc:`~.InternalError` to be raised. + """ + if self.__mutable is None: + self.__mutable = bool(mutable) + else: + raise InternalError( + "Cannot change mutability status of a resource." + ) + + def getAttrNames(self): + """ + Returns a list of names of the accessible attributes of the + resource. + """ + return self.__class__.FIELDS.keys() + + def getAttrField(self, attr_name): + """ + Returns the field name for the attribute named ``attr_name``. + An :exc:`~.UnknownAttribute` exception is raised if there is + no attribute with the given name. + """ + if attr_name in self.__class__.FIELDS: + return self.__class__.FIELDS[attr_name] + else: + raise UnknownAttribute( + "Unknown attribute '%s' for resource '%s'." % ( + attr_name, + self.__class__.__name__ + ) + ) + + def getAttrValue(self, attr_name): + """ + Returns the value of the attribute named ``attr_name``. An + :exc:`UnknownAttribute` exception is raised in case there is + no attribute with the given name. + """ + if attr_name in self.__class__.FIELDS: + obj = self.__model + for item in self.FIELDS[attr_name].split("__"): + obj = getattr(obj, item) + return obj + + else: + raise UnknownAttribute( + "Unknown attribute '%s' for resource '%s'." % ( + attr_name, + self.__class__.__name__ + ) + ) + + def setAttrValue(self, attr_name, value): + """ + Sets the value of the attribute named ``attr_name`` to + ``value``. An :exc:`~.InternalError` is raised if the resource + is not mutable. + """ + if self.__mutable: + if attr_name in self.__class__.FIELDS: + obj = self.__model + parts = self.FIELDS[attr_name].split("__") + for item in parts[:-1]: + obj = getattr(obj, item) + setattr(obj, parts[-1], value) + obj.save() + + else: + raise UnknownAttribute( + "Unknown attribute '%s' for resource '%s'." % ( + attr_name, + self.__class__.__name__ + ) + ) + else: + raise InternalError( + "Cannot set attributes on immutable resources." + ) + + def _get_create_dict(self, params): + return {} + + def _create_model(self, create_dict): + raise InternalError("Not implemented.") + + def _post_create(self, params): + pass + + def _updateModel(self, link_kwargs, unlink_kwargs, set_kwargs): + pass + + def _getAttrValue(self, attr_name): + raise InternalError("Not implemented.") + + def _setAttrValue(self, attr_name, value): + raise InternalError("Not implemented.") + +class ResourceFactory(object): + """ + This is the base class for implementations of + :class:`ResourceFactoryInterface`. + """ + def get(self, **kwargs): + """ + Returns the resource instance wrapping the resource model + defined by the input parameters. This method accepts three + optional keyword arguments: + + * ``subj_id``: the id of the calling component + * ``obj_id``: the resource ID of the resource + * ``filter_exprs``: a list of filter expressions that define + the resource + + Note that ``obj_id`` and ``filter_exprs`` are mutually + exclusive, but exactly one of them must be provided. The + ``subj_id`` argument will be used to check for relations to + the resource (not yet implemented). + """ + + subj_id = kwargs.get("subj_id") + obj_id = kwargs.get("obj_id") + filter_exprs = kwargs.get("filter_exprs") + + if obj_id is None and filter_exprs is None: + raise InternalError("Invalid call to ResourceFactory.get(): Either 'obj_id' or 'filter_exprs' keyword argument must be provided.") + elif obj_id is not None and filter_exprs is not None: + raise InternalError("Invalid call to ResourceFactory.get(): Can provide only one of 'obj_id' and 'filter_exprs'") + + model = None + ImplementationCls = None + + for Cls in System.getRegistry().getFactoryImplementations(self): + if obj_id is not None: # query by obj_id + new_model = self._getById(Cls, obj_id) + else: # query using filters + new_model = self._getByFilters(Cls, filter_exprs) + + if new_model is not None: + if model is None: + model = new_model + ImplementationCls = Cls + else: + if isinstance(new_model, model.__class__): + model = new_model + ImplementationCls = Cls + elif isinstance(model, new_model.__class__): + pass + else: + raise FactoryQueryAmbiguous("") + + if model is not None: + # TODO: implement relation management + + #if subj_id is not None: + #try: + #rel = model.relations.get(rel_class="rel_get", subj__impl_id=subj_id) + #except Relation.DoesNotExist: + #try: + #rel = ClassRelation.objects.get( + #rel_class="rel_get", + #subj__impl_id=subj_id, + #obj__impl_id=ImplementationCls.__get_impl_id__() + #) + #except ClassRelation.DoesNotExist: + #return None + + #if rel.enabled: + #return self._getResourceImplementation(ImplementationCls, model) + #else: + + # --- END TODO + + return self._getResourceImplementation(ImplementationCls, model) + else: + return None + + + def find(self, **kwargs): + """ + Returns a list of resource instances matching the given search + criteria. This method accepts three optional arguments: + + * ``subj_id``: the id of the calling component + * ``impl_ids``: the implementation IDs of the resource classes + to be taken into account + * ``filter_exprs``: a list of filter expressions that constrain + the resources + + The ``subj_id`` argument will be used to check for relations to + the resources (not yet implemented). + """ + subj_id = kwargs.get("subj_id") + impl_ids = kwargs.get("impl_ids") + filter_exprs = kwargs.get("filter_exprs", []) + + impl_and_models = [] + + for ImplementationCls in self._getImplementationClasses(impl_ids): + models = self._filter(ImplementationCls, filter_exprs) + + # TODO: implement relation management + + #if self.subj_id is not None: + #models = self._getRelated( + #subj_id, ImplementationCls, models + #) + + # --- END TODO + + for AnotherImplementationCls, other_models in impl_and_models: + if issubclass( + ImplementationCls.__get_model_class__(), + AnotherImplementationCls.__get_model_class__() + ) and\ + ImplementationCls.__get_id_field__() == \ + AnotherImplementationCls.__get_id_field__(): + other_models = other_models.exclude( + **{"%s__in" % AnotherImplementationCls.__get_id_field__(): models.values_list(ImplementationCls.__get_id_field__(), flat=True)} + ) + elif issubclass( + AnotherImplementationCls.__get_model_class__(), + ImplementationCls.__get_model_class__() + ) and\ + ImplementationCls.__get_id_field__() == \ + AnotherImplementationCls.__get_id_field__(): + models = models.exclude( + **{"%s__in" % ImplementationCls.__get_id_field__(): other_models.values_list(AnotherImplementationCls.__get_id_field__(), flat=True)} + ) + + impl_and_models.append((ImplementationCls, models)) + + resources = [] + + for ImplementationCls, models in impl_and_models: + resources.extend( + [self._getResourceImplementation(ImplementationCls, model) for model in models] + ) + + return resources + + def create(self, **kwargs): + """ + This method creates a resource according to the given + parameters and returns it to the caller. It accepts one + mandatory and two optional parameters: + + * ``subj_id``: the id of the calling component (optional) + * ``impl_id``: the implementation ID of the resource to be + created (mandatory) + * ``params``: a dictionary of parameters to initialize the + resource with; the format of this dictionary is specific to + the resource class + + The ``subj_id`` argument will be used to check for relations to + the resources (not yet implemented). + """ + subj_id = kwargs.get("subj_id") + + if "impl_id" in kwargs: + impl_id = kwargs["impl_id"] + else: + raise InternalError("Missing required 'impl_id' parameter.") + + params = kwargs.get("params", {}) + + ImplementationCls = self._getImplementationClass(impl_id) + + # TODO: implement relation management + + #if subj_id is not None: + #try: + #rel = ClassRelation.objects.get( + #rel_class="rel_create", + #subj__impl_id=subj_id, + #obj__impl_id=ImplementationCls.__get_impl_id__() + #) + + #if rel.enabled: + #resource = ImplementationCls() + #resource.createModel(params) + + #for comp_mgr in System.getRegistry().getComponentMangagers(subj_id): + #comp_mgr.notify(resource, "created") + + #return resource + #else: + #return None + + #except ClassRelation.DoesNotExist: + #return None + + # --- END TODO + + resource = ImplementationCls() + resource.setMutable() # required for creating model + + resource.createModel(params) + + return resource + + def update(self, **kwargs): + """ + This method runs updates on a selection of resources and + returns the updated resources. It accepts the following + parameters: + + * ``subj_id``: the id of the calling component + * ``obj_id``: the resource ID of the resource + * ``impl_ids``: the implementation IDs of the resource classes + to be taken into account + * ``filter_exprs``: a list of filter expressions that constrain + the resources + * ``attrs``: a dictionary of attribute names and values; the + attribute names are specific to the resource classes + * ``params``: a dictionary of parameters to update the + resource with; the format of this dictionary is specific to + the resource classes + + The ``subj_id`` argument will be used to check for relations to + the resources (not yet implemented). + + The ``obj_id`` argument and the ``impl_ids`` and + ``filter_exprs`` arguments on the other hand are mutually + exclusive. The ``attrs`` and ``params`` arguments are mutually + exclusive as well, exactly one of them has to be specified. + :exc:`InternalError` is raised if these conditions are not + met. + """ + subj_id = kwargs.get("subj_id") # TODO: implement relation management + + obj_id = kwargs.get("obj_id") + impl_ids = kwargs.get("impl_ids") + filter_exprs = kwargs.get("filter_exprs", []) + + attrs = kwargs.get("attrs", {}) + params = kwargs.get("params", {}) + + if obj_id is not None and \ + (impl_ids is not None or len(filter_exprs) > 0): + raise InternalError( + "ResourceFactory.update() accepts either 'obj_id' or 'impl_ids' and/or 'filter_exprs' as arguments." + ) + + if len(attrs) > 0 and len(params) > 0: + raise InternalError( + "ResourceFactory.update() accepts either 'attrs' or 'params' as arguments." + ) + elif len(attrs) + len(params) == 0: + raise InternalError( + "ResourceFactory.update() expects either 'attrs' or 'params' as arguments." + ) + + if obj_id is not None: + resource = self.get(subj_id=subj_id, obj_id=obj_id) + + if len(attrs) > 0: + for attr_name, attr_value in attrs.items(): + resource.setAttrValue(attr_name, attr_value) + + resource.saveModel() + + else: + resource.updateModel(params) + + resource.saveModel() + + return [resource] + else: + if len(attrs) > 0: + return self._updateByAttrs( + impl_ids, filter_exprs, attrs + ) + + else: + return self._updateByParams( + impl_ids, filter_exprs, params + ) + + def delete(self, **kwargs): + """ + This method deletes a selection of resources. It accepts the + following parameters: + + * ``subj_id``: the id of the calling component + * ``obj_id``: the resource ID of the resource + * ``impl_ids``: the implementation IDs of the resource classes + to be taken into account + * ``filter_exprs``: a list of filter expressions that constrain + the resources + + The ``subj_id`` argument will be used to check for relations to + the resources (not yet implemented). + + The ``obj_id`` argument and the ``impl_ids`` and + ``filter_exprs`` arguments on the other hand are mutually + exclusive. :exc:`InternalError` is raised if these conditions + are not met. + """ + subj_id = kwargs.get("subj_id") # TODO: implement relation management + + obj_id = kwargs.get("obj_id") + impl_ids = kwargs.get("impl_ids") + filter_exprs = kwargs.get("filter_exprs", []) + + if obj_id is not None and \ + (impl_ids is not None or len(filter_exprs) > 0): + raise InternalError( + "ResourceFactory.delete() accepts either 'obj_id' or 'impl_ids' and/or 'filter_exprs' as arguments." + ) + + if obj_id is not None: + resource = self.get(subj_id=subj_id, obj_id=obj_id) + + resource.deleteModel() + + else: + for ImplementationCls in self._getImplementationClasses(impl_ids): + models = self._filter(ImplementationCls, filter_exprs) + + models.delete() + + def getIds(self, **kwargs): + """ + This method returns the IDs of a selection of resources. It + accepts the following parameters: + + * ``subj_id``: the id of the calling component + * ``impl_ids``: the implementation IDs of the resource classes + to be taken into account + * ``filter_exprs``: a list of filter expressions that constrain + the resources + + The ``subj_id`` argument will be used to check for relations to + the resources (not yet implemented). + """ + subj_id = kwargs.get("subj_id") + impl_ids = kwargs.get("impl_ids") + filter_exprs = kwargs.get("filter_exprs", []) + + ids = set() + + for ImplementationCls in self._getImplementationClasses(impl_ids): + models = self._filter(ImplementationCls, filter_exprs) + + if subj_id is not None: + models = self._getRelated( + subj_id, ImplementationCls, models + ) + + ids.union(set(models.values_list(ImplementationCls.__get_id_field__(), flat=True))) + + return list(ids) + + def getAttrValues(self, **kwargs): + """ + This method returns the values of a given attribute for a + selection of resources. + + * ``subj_id``: the id of the calling component + * ``impl_ids``: the implementation IDs of the resource classes + to be taken into account + * ``filter_exprs``: a list of filter expressions that constrain + the resources + * ``attr_name``: the attribute name (mandatory) + + The ``subj_id`` argument will be used to check for relations to + the resources (not yet implemented). + + Raises :exc:`~.InternalError` if the ``attr_name`` argument is + missing, or :exc:`~.UnknownAttribute` if the attribute name is + not known to a resource. + """ + subj_id = kwargs.get("subj_id") # TODO: relation management + + impl_ids = kwargs.get("impl_ids") + filter_exprs = kwargs.get("filter_exprs", []) + + if "attr_name" in kwargs: + attr_name = kwargs["attr_name"] + else: + raise InternalError( + "ResourceFactory.getAttrValues() requires 'attr_name' argument." + ) + + attr_values = [] + + for ImplementationCls in self._getImplementationClasses(impl_ids): + models = self._filter(ImplementationCls, filter_exprs) + + impl = ImplementationCls() + + attr_values.extend(models.values_list( + impl.getAttrField(attr_name), flat=True + )) + + return attr_values + + def exists(self, **kwargs): + """ + Returns ``True`` if there are resources matching the given + criteria, or ``False`` otherwise. + + * ``subj_id``: the id of the calling component + * ``obj_id``: the id of the requested resource + * ``impl_ids``: the implementation IDs of the resource classes + to be taken into account + * ``filter_exprs``: a list of filter expressions that constrain + the resources + + Note that ``filter_exprs`` will not be taken into account when + ``obj_id`` is given. + + The ``subj_id`` argument will be used to check for relations to + the resources (not yet implemented). + """ + + subj_id = kwargs.get("subj_id") # TODO: relation management + + obj_id = kwargs.get("obj_id") + impl_ids = kwargs.get("impl_ids") + filter_exprs = kwargs.get("filter_exprs", []) + + if obj_id is not None: + return any([ + self._getById(ImplementationCls, obj_id) is not None + for ImplementationCls in self._getImplementationClasses(impl_ids) + ]) + + for ImplementationCls in self._getImplementationClasses(impl_ids): + models = self._filter(ImplementationCls, filter_exprs) + + if models.count() > 0: + return True + + return False + + def _getById(self, ImplementationCls, obj_id): + ModelClass = ImplementationCls.__get_model_class__() + + try: + return ModelClass.objects.get( + **{ImplementationCls.__get_id_field__(): obj_id} + ) + except ModelClass.DoesNotExist: + return None + except ModelClass.MultipleObjectsReturned: + raise FactoryQueryAmbiguous("") + + def _getByFilters(self, ImplementationCls, filter_exprs): + models = self._filter(ImplementationCls, filter_exprs) + + if len(models) == 0: + return None + elif len(models) == 1: + return models[1] + else: + raise FactoryQueryAmbiguous("") + + def _filter(self, ImplementationCls, filter_exprs): + ModelClass = ImplementationCls.__get_model_class__() + + qs = ModelClass.objects.all() + + for filter_expr in filter_exprs: + filter = System.getRegistry().findAndBind( + intf_id = "core.filters.Filter", + params = { + "core.filters.res_class_id": ImplementationCls.__get_impl_id__(), + "core.filters.expr_class_id": filter_expr.__class__.__get_impl_id__(), + } + ) + + qs = filter.applyToQuerySet(filter_expr, qs) + + return qs + + def _getResourceImplementation(self, ImplementationCls, model): + res = ImplementationCls() + res.setModel(model) + res.setMutable() # TODO: see ticket + + return res + + def _getImplementationClass(self, impl_id): + ImplClasses = System.getRegistry().getFactoryImplementations(self) + + if impl_id: + matching_classes = filter( + lambda impl_class : impl_class.__get_impl_id__() == impl_id, + ImplClasses + ) + if len(matching_classes) == 0: + raise InternalError("Unknown or incompatible resource class '%s'" % impl_id) + elif len(matching_classes) > 1: + raise InternalError("Ambiguous implementation id '%s'" % impl_id) + else: + return matching_classes[0] + elif len(ImplClasses) == 1: + return ImplClasses[0] + else: + raise InternalError("Missing 'impl_id' parameter") + + def _getImplementationClasses(self, impl_ids): + if impl_ids is None: + return System.getRegistry().getFactoryImplementations(self) + elif not (isinstance(impl_ids, list) or isinstance(impl_ids, tuple)): + raise InternalError("Invalid value for 'impl_ids': must be list or tuple") + + return filter( + lambda Cls: Cls.__get_impl_id__() in impl_ids, + System.getRegistry().getFactoryImplementations(self) + ) + + def _getRelated(self, subj_id, ImplementationCls, models): + try: + class_rel = ClassRelation.objects.get( + rel_class="rel_get", + subj__impl_id=subj_id, + obj__impl_id=ImplementationCls.__get_impl_id__() + ) + + class_enabled = class_rel.enabled + except ClassRelation.DoesNotExist: + class_enabled = False + + if class_enabled: + models = models.exclude( + relations__subj__impl_id=subj_id, + relations__enabled=False + ) + else: + models = models.filter( + relations__subj__impl_id=subj_id, + relations__enabled=True + ) + + return models + + def _updateByAttrs(self, impl_ids, filter_exprs, attrs): + resources = [] + + for ImplementationCls in self._getImplementationClasses(impl_ids): + models = self._filter(ImplementationCls, filter_exprs) + + impl = ImplementationCls() + + update_dict = {} + for attr_name, attr_value in attrs.items(): + try: + field_name = impl.getAttrField(attr_name) + + update_dict[field_name] = attr_value + except UnknownAttribute: + continue + + if len(update_dict) > 0: + models.update(**update_dict) + + resources.extend([ + self._getResourceImplementation( + ImplementationCls, model + ) + for model in models + ]) + + return resources + + def _updateByParams(self, impl_ids, filter_exprs, params): + resources = [] + + for ImplementationCls in self._getImplementationClasses(impl_ids): + models = self._filter(ImplementationCls, filter_exprs) + + new_resources = [ + self._getResourceImplementation( + ImplementationCls, model + ) + for model in models + ] + + for resource in new_resources: + resource.updateModel(params) + resource.saveModel() + + resources.extend(new_resources) + + return resources diff -Nru eoxserver-0.4.0beta2/eoxserver/core/startup.py eoxserver-0.3.2/eoxserver/core/startup.py --- eoxserver-0.4.0beta2/eoxserver/core/startup.py 1970-01-01 00:00:00.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/core/startup.py 2013-12-10 14:57:00.000000000 +0000 @@ -0,0 +1,79 @@ +#------------------------------------------------------------------------------- +# $Id$ +# +# Project: EOxServer +# Authors: Stephan Krause +# Stephan Meissl +# +#------------------------------------------------------------------------------- +# Copyright (C) 2011 EOX IT Services GmbH +# +# 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 of this Software or works derived from this 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. +#------------------------------------------------------------------------------- + +""" +This module defines an interface for startup handlers that are called during +system startup or reset. +""" + +from eoxserver.core.registry import Registry, RegisteredInterface +from eoxserver.core.config import Config +from eoxserver.core.interfaces import Method, ObjectArg + +class StartupHandlerInterface(RegisteredInterface): + """ + This is an interface for startup handlers. These handlers are called + automatically in the startup sequence; see the :mod:`eoxserver.core.system` + module documentation. It is intended to be implemented by modules or + components that need additional global system setup operations. + + .. method:: startup(config, registry) + + This method is called in the startup sequence after the configuration + has been validated and the registry has been set up. Those are passed + as ``config`` and ``registry`` parameters respectively. + + It may perform any additional logic needed for the setup of the + components concerned by the implementation. + + .. method:: reset(config, registry) + + This method is called in the reset sequence after the new configuration + has been validated and the registry has been set up. Those are passed + as ``config`` and ``registry`` parameters respectively. + + It may perform any additional logic needed by the components concerned + by the implementation for switching to the new configuration. + """ + + REGISTRY_CONF = { + "name": "Startup Handler Interface", + "intf_id": "core.startup.StartupHandler", + "binding_method": "direct" + } + + startup = Method( + ObjectArg("config", arg_class=Config), + ObjectArg("registry", arg_class=Registry), + ) + + reset = Method( + ObjectArg("config", arg_class=Config), + ObjectArg("registry", arg_class=Registry), + ) diff -Nru eoxserver-0.4.0beta2/eoxserver/core/system.py eoxserver-0.3.2/eoxserver/core/system.py --- eoxserver-0.4.0beta2/eoxserver/core/system.py 1970-01-01 00:00:00.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/core/system.py 2013-12-10 14:57:00.000000000 +0000 @@ -0,0 +1,328 @@ +#------------------------------------------------------------------------------- +# $Id$ +# +# Project: EOxServer +# Authors: Stephan Krause +# Stephan Meissl +# +#------------------------------------------------------------------------------- +# Copyright (C) 2011 EOX IT Services GmbH +# +# 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 of this Software or works derived from this 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. +#------------------------------------------------------------------------------- + +import os +import re +from threading import RLock, Condition, local +import logging +import warnings + +from django.conf import settings + +from eoxserver.core.config import Config +from eoxserver.core.exceptions import InternalError, ConfigError +from eoxserver.core.registry import Registry, RegistryConfigReader +from eoxserver.core.interfaces import RUNTIME_VALIDATION_LEVEL, IntfConfigReader + + +logger = logging.getLogger(__name__) + +class Environment(local): + def __init__(self, config, registry): + self.config = config + self.registry = registry + +class System(object): + """ + TODO + """ + UNCONFIGURED = 0 + STARTING = 10 + RESETTING = 20 + CONFIGURED = 30 + ERROR = 40 + + __lock = RLock() + + __state_cond = Condition() + __state = UNCONFIGURED + + __thread_env = None + __registry = None + __config = None + + @classmethod + def startRequest(cls): + warnings.warn("Use of System.startRequest() is deprecated use System.init() instead.", DeprecationWarning) + return cls.init() + + @classmethod + def init(cls): + """ + TODO + """ + cls.__state_cond.acquire() + + try: + if cls.__state == cls.UNCONFIGURED: + cls.__state = cls.STARTING + cls.__state_cond.notifyAll() + cls.__state_cond.release() + try: + config, registry = cls.__load() + cls.__state_cond.acquire() + cls.__state = cls.CONFIGURED + cls.__config = config + cls.__registry = registry + cls.__thread_env = Environment(config, registry) + except: + cls.__state_cond.acquire() + cls.__state = cls.ERROR + raise + elif cls.__state == cls.STARTING or cls.__state == cls.RESETTING: + while not (cls.__state == cls.CONFIGURED or cls.__state == cls.ERROR): + cls.__state_cond.wait() + + if cls.__state == cls.CONFIGURED: + cls.__thread_env.config = cls.__config + cls.__thread_env.registry = cls.__registry + else: + raise InternalError("Could not load system config.") + elif cls.__state == cls.ERROR: + cls.__state = cls.RESETTING + cls.__state_cond.notifyAll() + cls.__state_cond.release() + + try: + config, registry = cls.__load(reset=True) + cls.__state_cond.acquire() + cls.__config = config + cls.__registry = registry + if cls.__thread_env is None: + cls.__thread_env = Environment(config, registry) + else: + cls.__thread_env.config = config + cls.__thread_env.registry = registry + cls.__state = cls.CONFIGURED + except: + cls.__state_cond.acquire() + cls.__state = cls.ERROR + raise + finally: + # try to release the state condition lock; if it has not + # been acquired, ignore the resulting RuntimeError + try: + cls.__state_cond.notifyAll() + cls.__state_cond.release() + except RuntimeError: + pass + + @classmethod + def reset(cls): + cls.__state_cond.acquire() + + try: + if cls.__state == cls.UNCONFIGURED: + cls.__state = cls.STARTING + cls.__state_cond.notifyAll() + cls.__state_cond.release() + try: + config, registry = cls.__load() + cls.__state_cond.acquire() + cls.__config = config + cls.__registry = registry + cls.__thread_env = Environment(cls.__config, cls.__registry) + cls.__state = cls.CONFIGURED + except: + cls.__state_cond.acquire() + cls.__state = cls.ERROR + raise + else: + while cls.__state == cls.STARTING or cls.__state == cls.RESETTING: + cls.__state_cond.wait() + + prev_state = cls.__state + + cls.__state = cls.RESETTING + cls.__state_cond.notifyAll() + cls.__state_cond.release() + + try: + config, registry = cls.__load(reset=True) + cls.__state_cond.acquire() + cls.__config = config + cls.__registry = registry + if cls.__thread_env is None: + cls.__thread_env = Environment(config, registry) + else: + cls.__thread_env.config = config + cls.__thread_env.registry = registry + cls.__state = cls.CONFIGURED + except: + cls.__state_cond.acquire() + cls.__state = prev_state + raise + + finally: + # try to release the state condition lock; if it has not + # been acquired, ignore the resulting RuntimeError + try: + cls.__state_cond.notifyAll() + cls.__state_cond.release() + except RuntimeError: + pass + + @classmethod + def getRegistry(cls): + if cls.__thread_env: + return cls.__thread_env.registry + else: + raise InternalError("Could not access thread environment. System is in error state.") + + @classmethod + def getConfig(cls): + if cls.__thread_env: + return cls.__thread_env.config + else: + raise InternalError("Could not access thread environment. System is in error state") + + @classmethod + def __load(cls, reset=False): + try: + # first load the config + config = Config() + + # validate the system configuration + system_reader = SystemConfigReader(config) + system_reader.validate() + + # TODO: remove the logging setup and use the default django logging setup! + # configure the logging module + logging.basicConfig( + filename=system_reader.getLoggingFilename(), + format=system_reader.getLoggingFormat(), + level=system_reader.getLoggingLevel() + ) + + # TODO: integrate IPC + + # set up interfaces + cls.__setup_interfaces(config) + + # set up registry + registry = cls.__setup_registry(config) + + # invoke extending configuration validators + cls.__validate_ext(config, registry) + + # invoke extending startup handlers + if reset: + cls.__reset_ext(config, registry) + else: + cls.__startup_ext(config, registry) + + return (config, registry) + + except Exception, e: + logger.error("Could not start up system due to exception: %s" % str(e)) + + raise + + @classmethod + def __setup_interfaces(cls, config): + # validate interface configuration + intf_reader = IntfConfigReader(config) + intf_reader.validate() + + # set runtime validation level + level = intf_reader.getRuntimeValidationLevel() + if level: + RUNTIME_VALIDATION_LEVEL = level + + @classmethod + def __setup_registry(cls, config): + # validate registry configuration + registry_reader = RegistryConfigReader(config) + registry_reader.validate() + + # load registry + registry = Registry(config) + registry.load() + + return registry + + @classmethod + def __validate_ext(cls, config, registry): + Readers = registry.findImplementations(intf_id="core.readers.ConfigReader") + + for Reader in Readers: + Reader().validate(config) + + @classmethod + def __startup_ext(cls, config, registry): + Handlers = registry.findImplementations(intf_id="core.startup.StartupHandler") + + for Handler in Handlers: + Handler().startup(config, registry) + + @classmethod + def __reset_ext(cls, config, registry): + Handlers = registry.findImplementations(intf_id="core.startup.StartupHandler") + + for Handler in Handlers: + Handler().reset(config, registry) + +class SystemConfigReader(object): + def __init__(self, config): + self.config = config + + def validate(self): + instance_id = self.getInstanceID() + if not instance_id: + raise ConfigError("Missing mandatory 'instance_id' parameter") + elif not re.match("[A-Za-z_][A-Za-z0-9_.-]*", instance_id): + raise ConfigError("'instance_id' parameter must be NCName") + + if not self.getLoggingFilename(): + raise ConfigError("Missing mandatory 'logging_filename' parameter") + + def getInstanceID(self): + return self.config.getInstanceConfigValue("core.system", "instance_id") + + def getLoggingFilename(self): + return self.config.getInstanceConfigValue("core.system", "logging_filename") + + def getLoggingFormat(self): + return self.config.getConfigValue("core.system", "logging_format") + + def getLoggingLevel(self): + LEVELS = { + "DEBUG": logging.DEBUG, + "INFO": logging.INFO, + "WARNING": logging.WARNING, + "ERROR": logging.ERROR, + "CRITICAL": logging.CRITICAL + } + + level_name = self.config.getInstanceConfigValue("core.system", "logging_level") + + if not level_name or level_name.upper() not in LEVELS: + level_name = self.config.getDefaultConfigValue("core.system", "logging_level") + + return LEVELS[level_name.upper()] diff -Nru eoxserver-0.4.0beta2/eoxserver/core/templates/eoxserver_index.html eoxserver-0.3.2/eoxserver/core/templates/eoxserver_index.html --- eoxserver-0.4.0beta2/eoxserver/core/templates/eoxserver_index.html 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/core/templates/eoxserver_index.html 2013-12-10 14:57:00.000000000 +0000 @@ -1,5 +1,6 @@ diff -Nru eoxserver-0.4.0beta2/eoxserver/instance_template/project_name/conf/outline_template_footer.html eoxserver-0.3.2/eoxserver/instance_template/project_name/conf/outline_template_footer.html --- eoxserver-0.4.0beta2/eoxserver/instance_template/project_name/conf/outline_template_footer.html 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/instance_template/project_name/conf/outline_template_footer.html 2013-12-10 14:57:00.000000000 +0000 @@ -1,5 +1,5 @@ - - +
    + diff -Nru eoxserver-0.4.0beta2/eoxserver/instance_template/project_name/conf/template.map eoxserver-0.3.2/eoxserver/instance_template/project_name/conf/template.map --- eoxserver-0.4.0beta2/eoxserver/instance_template/project_name/conf/template.map 1970-01-01 00:00:00.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/instance_template/project_name/conf/template.map 2013-12-10 14:57:00.000000000 +0000 @@ -0,0 +1,70 @@ +#------------------------------------------------------------------------------- +# +# Project: EOxServer +# Authors: Stephan Krause +# Stephan Meissl +# +#------------------------------------------------------------------------------- +# Copyright (C) 2011 EOX IT Services GmbH +# +# 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 of this Software or works derived from this 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. +#------------------------------------------------------------------------------- + +MAP + IMAGECOLOR 0 0 0 +# MAXSIZE 2048 # Defaults to 2048 in MapServer + +# CONFIG "MS_ERRORFILE" "stderr" +# DEBUG 5 + + WEB + METADATA + "ows_updatesequence" "20110529T133000Z" + "ows_title" "Test configuration of MapServer used to demonstrate EOxServer" + "ows_abstract" "Test configuration of MapServer used to demonstrate EOxServer" + "ows_service_onlineresource" "http://eoxserver.org" + "ows_accessconstraints" "None" + "ows_fees" "None" + "ows_addresstype" "postal" + "ows_address" "
    " + "ows_city" "" + "ows_stateorprovince" "" + "ows_postcode" "" + "ows_country" "" + "ows_contactelectronicmailaddress" "" + "ows_contactperson" "" + "ows_contactorganization" "" + "ows_contactposition" "" + "ows_contactvoicetelephone" "" + "ows_contactfacsimiletelephone" "" + "ows_hoursofservice" "" + "ows_contactinstructions" "" + "ows_role" "" + "ows_keywordlist" "" + "wcs_keywordlist" "" + "wcs_label" "" + "wcs_name" "" + "wcs_fees" "None" + "ows_enable_request" "*" +# "ows_schemas_location" "http://localhost/schemas" + END + END + + CONFIG "GDAL_PAM_ENABLED" "No" +END # MAP diff -Nru eoxserver-0.4.0beta2/eoxserver/instance_template/project_name/settings.py eoxserver-0.3.2/eoxserver/instance_template/project_name/settings.py --- eoxserver-0.4.0beta2/eoxserver/instance_template/project_name/settings.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/instance_template/project_name/settings.py 2013-12-10 14:57:00.000000000 +0000 @@ -1,4 +1,5 @@ #------------------------------------------------------------------------------- +# $Id$ # # Project: EOxServer # Authors: Stephan Krause @@ -10,8 +11,8 @@ # 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 +# 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 @@ -41,7 +42,7 @@ PROJECT_DIR = dirname(abspath(__file__)) PROJECT_URL_PREFIX = '' -#TEST_RUNNER = 'eoxserver.testing.core.EOxServerTestRunner' +TEST_RUNNER = 'eoxserver.testing.core.EOxServerTestRunner' DEBUG = True TEMPLATE_DEBUG = DEBUG @@ -54,9 +55,9 @@ DATABASES = { 'default': { - 'ENGINE': 'django.contrib.gis.db.backends.spatialite', # Use 'spatialite' or change to 'postgis'. - 'NAME': join(PROJECT_DIR, 'data/config.sqlite'), # Or path to database file if using spatialite. - #'TEST_NAME': join(PROJECT_DIR, 'data/test-config.sqlite'), # Required for certain test cases, but slower! + 'ENGINE': 'django.contrib.gis.db.backends.spatialite', # Use 'spatialite' or change to 'postgis'. + 'NAME': '{{ project_directory }}/{{ project_name }}/data/config.sqlite', # Or path to database file if using spatialite. + #'TEST_NAME': '{{ project_directory }}/{{ project_name }}/data/test-config.sqlite', # Required for certain test cases, but slower! 'USER': '', # Not used with spatialite. 'PASSWORD': '', # Not used with spatialite. 'HOST': '', # Set to empty string for localhost. Not used with spatialite. @@ -66,10 +67,6 @@ SPATIALITE_SQL = join(PROJECT_DIR, 'data/init_spatialite-2.3.sql') -# Hosts/domain names that are valid for this site; required if DEBUG is False -# See https://docs.djangoproject.com/en/1.5/ref/settings/#allowed-hosts -ALLOWED_HOSTS = [] - # Local time zone for this installation. Choices can be found here: # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name # although not all choices may be available on all operating systems. @@ -100,22 +97,22 @@ USE_TZ = True # Absolute filesystem path to the directory that will hold user-uploaded files. -# Example: "/var/www/example.com/media/" +# Example: "/home/media/media.lawrence.com/media/" MEDIA_ROOT = '' # URL that handles the media served from MEDIA_ROOT. Make sure to use a # trailing slash. -# Examples: "http://example.com/media/", "http://media.example.com/" +# Examples: "http://media.lawrence.com/media/", "http://example.com/media/" MEDIA_URL = '' # Absolute path to the directory static files should be collected to. # Don't put anything in this directory yourself; store your static files # in apps' "static/" subdirectories and in STATICFILES_DIRS. -# Example: "/var/www/example.com/static/" +# Example: "/home/media/media.lawrence.com/static/" STATIC_ROOT = join(PROJECT_DIR, 'static') # URL prefix for static files. -# Example: "http://example.com/static/", "http://static.example.com/" +# Example: "http://media.lawrence.com/static/" STATIC_URL = '/{{ project_name }}_static/' # Additional locations of static files @@ -151,9 +148,6 @@ 'django.contrib.messages.middleware.MessageMiddleware', # Uncomment the next line for simple clickjacking protection: # 'django.middleware.clickjacking.XFrameOptionsMiddleware', - - # For management of the per/request cache system. - 'eoxserver.backends.middleware.BackendsCacheMiddleware', ) ROOT_URLCONF = '{{ project_name }}.urls' @@ -182,8 +176,7 @@ 'django.contrib.admindocs', # Enable the databrowse: #'django.contrib.databrowse', - # Enable for debugging - #'django_extensions', +# 'django_extensions', # Enable EOxServer: 'eoxserver.core', 'eoxserver.services', @@ -191,39 +184,9 @@ 'eoxserver.resources.processes', 'eoxserver.backends', 'eoxserver.testing', - 'eoxserver.webclient', - # Enable EOxServer autotests - #'autotest_services', -) - - -# The configured EOxServer components. Components add specific functionality -# to the EOxServer and must adhere to a given interface. In order to activate -# a component, its module must be included in the following list or imported at -# some other place. To help configuring all required components, each module -# path can end with either a '*' or '**'. The single '*' means that all direct -# modules in the package will be included. With the double '**' a recursive -# search will be done. -COMPONENTS = ( - # backends - 'eoxserver.backends.storages.*', - 'eoxserver.backends.packages.*', - - # metadata readers/writers - 'eoxserver.resources.coverages.metadata.formats.*', - - # service handlers - 'eoxserver.services.ows.wcs.**', - 'eoxserver.services.ows.wms.**', - 'eoxserver.services.ows.wps.**', - - # renderer components etc. - 'eoxserver.services.native.**', - 'eoxserver.services.gdal.**', - 'eoxserver.services.mapserver.**', + 'eoxserver.webclient' ) - # A sample logging configuration. The only tangible logging # performed by this configuration is to send an email to # the site admins on every HTTP 500 error when DEBUG=False. @@ -250,15 +213,9 @@ 'level': 'DEBUG', 'class': 'logging.FileHandler', 'filename': join(PROJECT_DIR, 'logs', 'eoxserver.log'), - 'formatter': 'verbose' if DEBUG else 'simple', + 'formatter': 'verbose', 'filters': [], - }, - 'stderr_stream': { - 'level': 'INFO', - 'class': 'logging.StreamHandler', - 'formatter': 'simple', - 'filters': [], - }, + } }, 'loggers': { 'eoxserver': { @@ -266,11 +223,6 @@ 'level': 'DEBUG' if DEBUG else 'INFO', 'propagate': False, }, - 'django': { - 'handlers': ['stderr_stream'], - 'level': 'WARNING', - 'propagate': False, - }, } } diff -Nru eoxserver-0.4.0beta2/eoxserver/instance_template/project_name/templates/404.html eoxserver-0.3.2/eoxserver/instance_template/project_name/templates/404.html --- eoxserver-0.4.0beta2/eoxserver/instance_template/project_name/templates/404.html 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/instance_template/project_name/templates/404.html 1970-01-01 00:00:00.000000000 +0000 @@ -1,13 +0,0 @@ - - -  - - Page not found. - -  - -

    Page not found.

    - -  - diff -Nru eoxserver-0.4.0beta2/eoxserver/instance_template/project_name/templates/500.html eoxserver-0.3.2/eoxserver/instance_template/project_name/templates/500.html --- eoxserver-0.4.0beta2/eoxserver/instance_template/project_name/templates/500.html 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/instance_template/project_name/templates/500.html 1970-01-01 00:00:00.000000000 +0000 @@ -1,13 +0,0 @@ - - -  - - Internal Server Error - -  - -

    Internal Server Error

    - -  - diff -Nru eoxserver-0.4.0beta2/eoxserver/instance_template/project_name/templates/admin/base_site.html eoxserver-0.3.2/eoxserver/instance_template/project_name/templates/admin/base_site.html --- eoxserver-0.4.0beta2/eoxserver/instance_template/project_name/templates/admin/base_site.html 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/instance_template/project_name/templates/admin/base_site.html 2013-12-10 14:57:00.000000000 +0000 @@ -1,5 +1,6 @@ bands[1:] - if len(datas) == 1: - if len(range_type) == 1: - semantics = ["bands[1]"] - else: - semantics = ["bands[1:%d]" % len(range_type)] - - else: - semantics = ["bands[%d]" % i for i in range(len(datas))] - - for data, semantic in zip(datas, semantics): - storage, package, format, location = self._get_location_chain(data) - data_item = backends.DataItem( - location=location, format=format or "", semantic=semantic, - storage=storage, package=package, - ) - data_item.full_clean() - data_item.save() - all_data_items.append(data_item) - - # TODO: other opening methods than GDAL - ds = gdal.Open(connect(data_item, cache)) - reader = metadata_component.get_reader_by_test(ds) - if reader: - values = reader.read(ds) - - format = values.pop("format", None) - if format: - data_item.format = format - data_item.full_clean() - data_item.save() - - for key, value in values.items(): - if key in metadata_keys: - retrieved_metadata.setdefault(key, value) - ds = None - - if len(metadata_keys - set(retrieved_metadata.keys())): - raise CommandError( - "Missing metadata keys %s." - % ", ".join(metadata_keys - set(retrieved_metadata.keys())) - ) - - # replace any already registered dataset - if kwargs["replace"]: - try: - # get a list of all collections the coverage was in. - coverage = models.Coverage.objects.get( - identifier=retrieved_metadata["identifier"] - ) - additional_ids = [ - c.identifier - for c in models.Collection.objects.filter( - eo_objects__in=[coverage.pk] - ) - ] - coverage.delete() - - self.print_msg( - "Replacing previous dataset '%s'." - % retrieved_metadata["identifier"] - ) - - collection_ids = kwargs["collection_ids"] or [] - for identifier in additional_ids: - if identifier not in collection_ids: - collection_ids.append(identifier) - kwargs["collection_ids"] = collection_ids - except models.Coverage.DoesNotExist: - self.print_msg( - "Could not replace previous dataset '%s'." - % retrieved_metadata["identifier"] - ) - - try: - # TODO: allow types of different apps - CoverageType = getattr(models, retrieved_metadata["coverage_type"]) - except AttributeError: - raise CommandError( - "Type '%s' is not supported." % kwargs["coverage_type"] - ) - - try: - coverage = CoverageType() - coverage.range_type = range_type - - proj = retrieved_metadata.pop("projection") - if isinstance(proj, int): - retrieved_metadata["srid"] = proj - else: - definition, format = proj - - # Try to identify the SRID from the given input - try: - sr = osr.SpatialReference(definition, format) - retrieved_metadata["srid"] = sr.srid - except Exception, e: - prj = models.Projection.objects.get( - format=format, definition=definition - ) - retrieved_metadata["projection"] = prj - - # TODO: bug in models for some coverages - for key, value in retrieved_metadata.items(): - setattr(coverage, key, value) - - coverage.visible = kwargs["visible"] - - coverage.full_clean() - coverage.save() - - for data_item in all_data_items: - data_item.dataset = coverage - data_item.full_clean() - data_item.save() - - # link with collection(s) - if kwargs["collection_ids"]: - ignore_missing_collection = kwargs["ignore_missing_collection"] - call_command("eoxs_collection_link", - collection_ids=kwargs["collection_ids"], - add_ids=[coverage.identifier], - ignore_missing_collection=ignore_missing_collection - ) - - except Exception as e: - self.print_traceback(e, kwargs) - raise CommandError("Dataset registration failed: %s" % e) - - self.print_msg( - "Dataset with ID '%s' registered sucessfully." - % coverage.identifier - ) - - def _get_overrides(self, identifier=None, size=None, extent=None, - begin_time=None, end_time=None, footprint=None, - projection=None, coverage_type=None, **kwargs): - - overrides = {} - - if coverage_type: - overrides["coverage_type"] = coverage_type - - if identifier: - overrides["identifier"] = identifier - - if extent: - overrides["extent"] = map(float, extent.split(",")) - - if size: - overrides["size"] = map(int, size.split(",")) - - if begin_time: - overrides["begin_time"] = parse_datetime(begin_time) - - if end_time: - overrides["end_time"] = parse_datetime(end_time) - - if footprint: - footprint = geos.GEOSGeometry(footprint) - if footprint.hasz: - raise CommandError( - "Invalid footprint geometry! 3D geometry is not supported!" - ) - if footprint.geom_type == "MultiPolygon": - overrides["footprint"] = footprint - elif footprint.geom_type == "Polygon": - overrides["footprint"] = geos.MultiPolygon(footprint) - else: - raise CommandError( - "Invalid footprint geometry type '%s'!" - % (footprint.geom_type) - ) - - if projection: - try: - overrides["projection"] = int(projection) - except ValueError: - overrides["projection"] = projection - - return overrides - - def _get_location_chain(self, items): - """ Returns the tuple - """ - component = BackendComponent(env) - storage = None - package = None - - storage_type, url = self._split_location(items[0]) - if storage_type: - storage_component = component.get_storage_component(storage_type) - else: - storage_component = None - - if storage_component: - storage, _ = backends.Storage.objects.get_or_create( - url=url, storage_type=storage_type - ) - - # packages - for item in items[1 if storage else 0:-1]: - type_or_format, location = self._split_location(item) - package_component = component.get_package_component(type_or_format) - if package_component: - package, _ = backends.Package.objects.get_or_create( - location=location, format=format, - storage=storage, package=package - ) - storage = None # override here - else: - raise Exception( - "Could not find package component for format '%s'" - % type_or_format - ) - - format, location = self._split_location(items[-1]) - return storage, package, format, location - - def _split_location(self, item): - """ Splits string as follows: : where format can be - None. - """ - p = item.find(":") - if p == -1: - return None, item - return item[:p], item[p + 1:] - - -def save(model): - model.full_clean() - model.save() - return model diff -Nru eoxserver-0.4.0beta2/eoxserver/resources/coverages/management/commands/eoxs_deregister_dataset.py eoxserver-0.3.2/eoxserver/resources/coverages/management/commands/eoxs_deregister_dataset.py --- eoxserver-0.4.0beta2/eoxserver/resources/coverages/management/commands/eoxs_deregister_dataset.py 1970-01-01 00:00:00.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/resources/coverages/management/commands/eoxs_deregister_dataset.py 2013-12-10 14:57:00.000000000 +0000 @@ -0,0 +1,187 @@ +#------------------------------------------------------------------------------- +# $Id$ +# +# Project: EOxServer +# Authors: Martin Paces +# +#------------------------------------------------------------------------------- +# Copyright (C) 2011 EOX IT Services GmbH +# +# 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 of this Software or works derived from this 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. +#------------------------------------------------------------------------------- + +import sys +import traceback + +from optparse import make_option + +from django.core.management.base import BaseCommand, CommandError +from django.db import transaction + +#------------------------------------------------------------------------------ + +from eoxserver.core.system import System + +#------------------------------------------------------------------------------ + +from eoxserver.resources.coverages.managers import CoverageIdManager +from eoxserver.resources.coverages.managers import getRectifiedDatasetManager +from eoxserver.resources.coverages.managers import getReferenceableDatasetManager + +#------------------------------------------------------------------------------ + +from eoxserver.resources.coverages.management.commands import CommandOutputMixIn + +#------------------------------------------------------------------------------ + + +class Command(CommandOutputMixIn, BaseCommand): + + option_list = BaseCommand.option_list + ( + make_option('--force', + dest='force_dereg_autods', + action='store_true', + default=False, + help=("Optional. Force deregistration of automatic datasets. " + "This option should be avoided or used with extreme " + "caution. (forced deregistration is disabled by default)") + ), + ) + + args = " [ ...]" + + help = ( + """ + Deregister one or more datasets corresponding to the given ids. + unless overridden by the '--force' option the removal of automatic datasets + will be prevented. + + """ % ({"name": __name__.split(".")[-1]}) + ) + + #-------------------------------------------------------------------------- + + def _error( self , ds , msg ): + self.print_err( "Failed to deregister dataset '%s'!" + " Reason: %s"%( ds, msg ) ) + + #-------------------------------------------------------------------------- + + def handle(self, *args, **options): + + # Collect parameters + + self.verbosity = int(options.get('verbosity', 1)) + + force_dereg_autods = bool(options.get('force_dereg_autods',False)) + + # dataset's (coverages') ids + datasets = args + + #---------------------------------------------------------------------- + # initialize EOxServer binding + + System.init() + + #---------------------------------------------------------------------- + # prepare managers + + dsMngr = { + "RectifiedDataset" : getRectifiedDatasetManager() , + "ReferenceableDataset" : getReferenceableDatasetManager() } + + cidMngr = CoverageIdManager() + + #---------------------------------------------------------------------- + # remove datasets one by one + + success_count = 0 # count successfull actions + + for dataset in datasets : + + self.print_msg( "Deregistering dataset: '%s'"%dataset ) + + # check the dataset type + dsType = cidMngr.getType( dataset ) + + self.print_msg( "Dataset type: %s"%str(dsType) , 2 ) + + # check the dataset type + + if not dsMngr.has_key( dsType ) : + self.print_msg( "'%s' is neither rectified nor referenceable " + "dataset."%dataset,2) + self._error( dataset , "Invalid dataset identifier." ) + continue # continue by next dataset + + # check whether the dataset is automatic or manual + + if dsMngr[dsType].is_automatic( dataset ) : + + self.print_msg( "'%s' is labeled as AUTOMATIC!"%dataset,2) + + if not force_dereg_autods: + + self._error( dataset, "Dataset is labeled as automatic. " + "Deregistration of the automatic dataset is not allowed!" ) + continue # continue by next dataset + + self.print_msg( "Removal of AUTOMATIC datasets is allowed!",2) + + # removing the dataset + + try: + + with transaction.commit_on_success(): + self.print_msg( "Removing dataset from the DB ...",2) + dsMngr[dsType].delete( dataset ) + + except Exception as e: + + self.print_msg( "Dataset removal failed with an exception.",2) + + # print stack trace if required + if options.get("traceback", False): + self.print_msg(traceback.format_exc()) + + self._error( dataset, "%s: %s"%(type(e).__name__, str(e)) ) + + continue # continue by next dataset + + success_count += 1 #increment the success counter + self.print_msg( "Dataset successfully removed.",2) + + #---------------------------------------------------------------------- + # print the final statistics + + count = len(datasets) + error_count = count - success_count + + if ( error_count > 0 ) : + self.print_msg( "Failed to deregistered %d dataset%s." % ( + error_count , ("","s")[error_count!=1] ) , 1 ) + + if ( success_count > 0 ) : + self.print_msg( "Successfully deregistered %d of %s dataset%s." % ( + success_count , count , ("","s")[count!=1] ) , 1 ) + else : + self.print_msg( "No dataset deregistered." ) + + if ( error_count > 0 ) : + raise CommandError("Not all datasets could be deregistered.") diff -Nru eoxserver-0.4.0beta2/eoxserver/resources/coverages/management/commands/eoxs_id_check.py eoxserver-0.3.2/eoxserver/resources/coverages/management/commands/eoxs_id_check.py --- eoxserver-0.4.0beta2/eoxserver/resources/coverages/management/commands/eoxs_id_check.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/resources/coverages/management/commands/eoxs_id_check.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,89 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Martin Paces -# Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2014 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -import sys -from optparse import make_option - -from django.core.management.base import CommandError, BaseCommand - -from eoxserver.resources.coverages.management.commands import ( - CommandOutputMixIn, _variable_args_cb -) - -from eoxserver.resources.coverages import models - - -class Command(CommandOutputMixIn, BaseCommand): - option_list = BaseCommand.option_list + ( - make_option("-t", "--type", - dest="type_name", action="store", default="EOObject", - help=("Optional. Restrict the listed identifiers to given type.") - ), - ) - - args = " [ ...] [-t ]" - - help = """ - Check whether one or more identifier are used by existing EOObjects or - objects of a specified subtype. - - The existence is indicated by the returned exit-code. A non-zero value - indicates that any of the supplied identifiers is already in use. - """ - - def handle(self, *identifiers, **kwargs): - if not identifiers: - raise CommandError("Missing the mandatory identifier(s).") - - type_name = kwargs["type_name"] - - try: - # TODO: allow types residing in different apps - ObjectType = getattr(models, type_name) - if not issubclass(ObjectType, models.EOObject): - raise CommandError("Unsupported type '%s'." % type_name) - except AttributeError: - raise CommandError("Unsupported type '%s'." % type_name) - - - used = False - for identifier in identifiers: - try: - obj = ObjectType.objects.get(identifier=identifier) - self.print_msg( - "The identifier '%s' is already in use by a '%s'." - % (identifier, obj.real_type.__name__) - ) - used = True - except ObjectType.DoesNotExist: - self.print_msg( - "The identifier '%s' is currently not in use." % identifier - ) - - if used: - sys.exit(1) diff -Nru eoxserver-0.4.0beta2/eoxserver/resources/coverages/management/commands/eoxs_id_list.py eoxserver-0.3.2/eoxserver/resources/coverages/management/commands/eoxs_id_list.py --- eoxserver-0.4.0beta2/eoxserver/resources/coverages/management/commands/eoxs_id_list.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/resources/coverages/management/commands/eoxs_id_list.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,87 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Martin Paces -# Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2014 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -from optparse import make_option - -from django.core.management.base import CommandError, BaseCommand - -from eoxserver.resources.coverages import models - -INDENT=" " - -class Command(BaseCommand): - option_list = BaseCommand.option_list + ( - make_option("-t", "--type", - dest="type_name", action="store", default="EOObject", - help=("Optional. Restrict the listed identifiers to given type.") - ), - make_option("-r", "--recursive", - dest="recursive", action="store_true", default=False, - help=("Optional. Recursive listing for collections.") - ), - ) - - args = "[ [ ...]] [-t ] [-r]" - - help = """ - Print a list of all objects in the database. Alternatively the list - can be filtered by a give set of identifiers or a given object type. - - The listing can also be done recursively with the `-r` option - """ - - def handle(self, *identifiers, **kwargs): - type_name = kwargs["type_name"] - - try: - # TODO: allow types residing in different apps - ObjectType = getattr(models, type_name) - if not issubclass(ObjectType, models.EOObject): - raise CommandError("Unsupported type '%s'." % type_name) - except AttributeError: - raise CommandError("Unsupported type '%s'." % type_name) - - eo_objects = ObjectType.objects.all() - - if identifiers: - eo_objects = eo_objects.filter(identifier__in=identifiers) - - for eo_object in eo_objects: - self.print_object(eo_object, kwargs["recursive"]) - - - def print_object(self, eo_object, recursive=False, level=0): - indent = INDENT * level - eo_object = eo_object.cast() - print("%s%s %s" % (indent, eo_object.identifier, - eo_object.__class__.__name__)) - - if recursive and models.iscollection(eo_object): - for sub_eo_object in eo_object.eo_objects.all(): - self.print_object(sub_eo_object, recursive, level+1) diff -Nru eoxserver-0.4.0beta2/eoxserver/resources/coverages/management/commands/eoxs_insert_into_series.py eoxserver-0.3.2/eoxserver/resources/coverages/management/commands/eoxs_insert_into_series.py --- eoxserver-0.4.0beta2/eoxserver/resources/coverages/management/commands/eoxs_insert_into_series.py 1970-01-01 00:00:00.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/resources/coverages/management/commands/eoxs_insert_into_series.py 2013-12-10 14:57:00.000000000 +0000 @@ -0,0 +1,45 @@ +#------------------------------------------------------------------------------- +# $Id$ +# +# Project: EOxServer +# Authors: Stephan Krause +# Stephan Meissl +# Fabian Schindler +# +#------------------------------------------------------------------------------- +# Copyright (C) 2011 EOX IT Services GmbH +# +# 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 of this Software or works derived from this 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. +#------------------------------------------------------------------------------- + +from eoxserver.resources.coverages.management.commands import ManageDatasetSeriesCommand + +class Command(ManageDatasetSeriesCommand): + + help=("Insert one or more datasets (DS) into one or more specified dataset" + " series (DSS).") + + def manage_series(self, manager, dataset_ids, datasetseries_ids): + """Main method for dataset handling.""" + + #TODO: check if the datasets have not been already inserted + + for dssid in datasetseries_ids: + + manager.update( obj_id=dssid, link={"coverage_ids":dataset_ids}) diff -Nru eoxserver-0.4.0beta2/eoxserver/resources/coverages/management/commands/eoxs_list_ids.py eoxserver-0.3.2/eoxserver/resources/coverages/management/commands/eoxs_list_ids.py --- eoxserver-0.4.0beta2/eoxserver/resources/coverages/management/commands/eoxs_list_ids.py 1970-01-01 00:00:00.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/resources/coverages/management/commands/eoxs_list_ids.py 2013-12-10 14:57:00.000000000 +0000 @@ -0,0 +1,133 @@ +#------------------------------------------------------------------------------- +# $Id$ +# +# Project: EOxServer +# Authors: Martin Paces +# +#------------------------------------------------------------------------------- +# Copyright (C) 2011 EOX IT Services GmbH +# +# 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 of this Software or works derived from this 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. +#------------------------------------------------------------------------------- + +import sys +import traceback +from optparse import make_option +from django.core.management.base import BaseCommand, CommandError +from eoxserver.core.system import System +from eoxserver.resources.coverages.management.commands import CommandOutputMixIn +from eoxserver.resources.coverages.management.commands import _variable_args_cb + +#------------------------------------------------------------------------------ + +from eoxserver.resources.coverages.managers import CoverageIdManager +from eoxserver.resources.coverages.managers import getRectifiedDatasetManager +from eoxserver.resources.coverages.managers import getReferenceableDatasetManager +from eoxserver.resources.coverages.managers import getRectifiedStitchedMosaicManager +from eoxserver.resources.coverages.managers import getDatasetSeriesManager + +#------------------------------------------------------------------------------ + +class Command(CommandOutputMixIn, BaseCommand): + + option_list = BaseCommand.option_list + ( + make_option("-f","--filter","--type-filter", dest="filter_enabled", + action='store_true', default=False, + help=("Optional. List of types to restrict the listed IDs.") + ), + ) + + args = "-f [ ...]| [ ...]" + + help = ( + """ + Print either list of all or selected dataset (Coverage/EO) indentifiers + and their types. The selection passed as command-line arguments. + + In case of listing of all registered IDs, the output can be filtered + by a given selection of types. + + The commands prints list of pairs. The type can be either + "RectifiedDataset", "ReferenceableDataset", "RectifiedStitchedMosaic", + "DatasetSeries", "Reserved" (reserved ID), "None" (non-existing id). + + """ % ({"name": __name__.split(".")[-1]}) + ) + + #-------------------------------------------------------------------------- + + def handle(self, *args, **options): + + # Collect parameters + + self.verbosity = int(options.get('verbosity', 1)) + + + # get filter + if options.get("filter_enabled",False) : + + # arguments interpreted as filters + ids = [] # dataset's (coverages') ids + type_filter = args # get filter + + else : + + # argument interpreted as ids + ids = args # dataset's (coverages') ids + type_filter = [] # get filter + + + #---------------------------------------------------------------------- + + # initialize EOxServer binding + + System.init() + + # prepare managers + + id_manager = CoverageIdManager() + + ds_managers = { + "RectifiedDataset" : getRectifiedDatasetManager() , + "ReferenceableDataset" : getReferenceableDatasetManager() , + "RectifiedStitchedMosaic" : getRectifiedStitchedMosaicManager() , + "DatasetSeries" : getDatasetSeriesManager() , + } + + #---------------------------------------------------------------------- + # check the input rangetype names + + if not ids : + + # if no IDs specified get all identifiers + for ds_type in ds_managers : + if not type_filter or ds_type in type_filter : + for id in ds_managers[ds_type].get_all_ids() : + print id , ds_type + + # in addition print all reserved IDs + if not type_filter or "Reserved" in type_filter : + for id in id_manager.getAllReservedIds() : + print id , "Reserved" + + else : + + # get info for the specified identifiers + for id in ids : + print id , id_manager.getType(id) diff -Nru eoxserver-0.4.0beta2/eoxserver/resources/coverages/management/commands/eoxs_list_rangetypes.py eoxserver-0.3.2/eoxserver/resources/coverages/management/commands/eoxs_list_rangetypes.py --- eoxserver-0.4.0beta2/eoxserver/resources/coverages/management/commands/eoxs_list_rangetypes.py 1970-01-01 00:00:00.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/resources/coverages/management/commands/eoxs_list_rangetypes.py 2013-12-10 14:57:00.000000000 +0000 @@ -0,0 +1,268 @@ +#------------------------------------------------------------------------------- +# $Id$ +# +# Project: EOxServer +# Authors: Martin Paces +# +#------------------------------------------------------------------------------- +# Copyright (C) 2011 EOX IT Services GmbH +# +# 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 of this Software or works derived from this 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. +#------------------------------------------------------------------------------- + +import sys +import traceback + +from optparse import make_option + +from django.core.management.base import BaseCommand, CommandError + +# try the python default json module +try : import json +except ImportError: + #try the original simplejson module + try: import simplejson as json + except ImportError: + #try the simplejson module packed in django + try: import django.utils.simplejson as json + except ImportError: + raise ImportError( "Failed to import any usable json module!" ) + +#------------------------------------------------------------------------------ + +from eoxserver.core.system import System + +#------------------------------------------------------------------------------ + +from eoxserver.resources.coverages.rangetype import getAllRangeTypeNames +from eoxserver.resources.coverages.rangetype import isRangeTypeName +from eoxserver.resources.coverages.rangetype import getRangeType + +#------------------------------------------------------------------------------ + +from eoxserver.resources.coverages.management.commands import CommandOutputMixIn + +#------------------------------------------------------------------------------ + +class Command(CommandOutputMixIn, BaseCommand): + + option_list = BaseCommand.option_list + ( + make_option('--details', + dest='details', + action='store_true', + default=False, + help=("Optional. Print details of the reangetypes." ) + ), + make_option('--json', + dest='json_dump', + action='store_true', + default=False, + help=("Optional. Dump rangetype(s) in JSON format. This JSON " + "dump can be loaded by another instance of EOxServer." ) + ), + make_option('-o','--output', + dest='filename', + action='store', type='string', + default='-', + help=("Optional. Write output to a file rather than to the default" + " standard output." ) + ), + + ) + + args = "[ [ ...]]" + + help = ( + """ + Print either list of all rangetype indentifiers and their details. + When the range-type identifiers are specified than only these rangetypes + are selected. In addition complete rangetypes cans be dumped in JSON + format which can be then loaded by another EOxServer instance. + """ % ({"name": __name__.split(".")[-1]}) + ) + + #-------------------------------------------------------------------------- + + def handle(self, *args, **options): + + # Collect parameters + + self.verbosity = int(options.get('verbosity', 1)) + + print_details = bool(options.get('details',False)) + + print_json = bool(options.get('json_dump',False)) + + filename = options.get('filename','-') + + # dataset's (coverages') ids + rt_list = args + + + #---------------------------------------------------------------------- + # initialize EOxServer binding + + System.init() + + #---------------------------------------------------------------------- + # check the input rangetype names + + if not rt_list : + + # if no IDs specified get all identifiers + + rt_list = getAllRangeTypeNames() + + else : + + # filter existing range-type names + + def __checkRangeType( rt ) : + rv = isRangeTypeName( rt ) + if not rv : + self.print_err( "Invalid range-type identifier '%s' !"%rt ) + return rv + + rt_list = filter( __checkRangeType , rt_list ) + + #---------------------------------------------------------------------- + # output + + # select the right output driver + + if print_json : output = OutputJSON + elif print_details : output = OutputDetailed + else : output = OutputBrief + + + # write the output + + def _write_out( fout ) : + fout.write( output.lead() ) + for i,rt_name in enumerate(rt_list) : + if i > 0 : fout.write( output.separator() ) + fout.write( output.object( rt_name ) ) + fout.write( output.trail() ) + + # output file + try : + + if filename == "-" : + + # write to stdout + _write_out( sys.stdout ) + + else : + + # write to a file + with open(filename,"w") as fout : + _write_out( fout ) + + except IOError as e : + + raise CommandError( "Failed to open the output file '%s' ! " + "REASON: %s" % ( filename , str(e) ) ) + + +#------------------------------------------------------------------------------ +# output drivers + +class OutputBase: + """ base output driver class class """ + + @classmethod + def lead(cls): return "" + + @classmethod + def object( cls, rt_name ) : return "" + + @classmethod + def trail(cls): return "" + + @classmethod + def separator(cls) : return "" + + +class OutputBrief( OutputBase ): + """ brief text output - RT name only """ + + @classmethod + def object( cls, rt_name ) : return rt_name + + @classmethod + def separator(cls) : return "\n" + + @classmethod + def trail(cls): return "\n" + + +class OutputDetailed( OutputBase ): + """ detailed text output """ + + @classmethod + def lead(cls) : return "\n" + + @classmethod + def trail(cls) : return "\n\n" + + @classmethod + def separator(cls) : return "\n\n" + + @classmethod + def object( cls, rt_name ) : + + rt = getRangeType( rt_name ) + + out = [] + + out.append("Range-Type: %s" % rt.name ) + out.append("\tType:\t\t%s" % rt.getDataTypeAsString()) + out.append("\tNr. of Bands:\t%d" % len(rt.bands)) + out.append("\tBands:") + + for band in rt.bands : + out.append( "\t\t%s"%(band.identifier) ) + + return "\n".join( out ) + + +class OutputJSON( OutputBase ) : + """ JSON output """ + + @classmethod + def lead(cls): + return "[" + + @classmethod + def trail(cls): + return "]\n" + + @classmethod + def separator(cls) : return ",\n" + + @classmethod + def object( cls, rt_name ) : + + # get rangetype as dictionary + out = getRangeType(rt_name).asDict() + + # dump the json + return json.dumps(out,indent=4,separators=(',',': '),sort_keys=True) + + diff -Nru eoxserver-0.4.0beta2/eoxserver/resources/coverages/management/commands/eoxs_load_rangetypes.py eoxserver-0.3.2/eoxserver/resources/coverages/management/commands/eoxs_load_rangetypes.py --- eoxserver-0.4.0beta2/eoxserver/resources/coverages/management/commands/eoxs_load_rangetypes.py 1970-01-01 00:00:00.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/resources/coverages/management/commands/eoxs_load_rangetypes.py 2013-12-10 14:57:00.000000000 +0000 @@ -0,0 +1,188 @@ +#------------------------------------------------------------------------------- +# $Id$ +# +# Project: EOxServer +# Authors: Martin Paces +# +#------------------------------------------------------------------------------- +# Copyright (C) 2011 EOX IT Services GmbH +# +# 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 of this Software or works derived from this 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. +#------------------------------------------------------------------------------- + +import sys +import traceback + +from optparse import make_option + +from django.core.management.base import BaseCommand, CommandError + +# try the python default json module +try : import json +except ImportError: + #try the original simplejson module + try: import simplejson as json + except ImportError: + #try the simplejson module packed in django + try: import django.utils.simplejson as json + except ImportError: + raise ImportError( "Failed to import any usable json module!" ) + +#------------------------------------------------------------------------------ + +from eoxserver.core.system import System + +#------------------------------------------------------------------------------ + +from eoxserver.resources.coverages.rangetype import isRangeTypeName +from eoxserver.resources.coverages.rangetype import setRangeType + +#------------------------------------------------------------------------------ + +from eoxserver.resources.coverages.management.commands import CommandOutputMixIn + +#------------------------------------------------------------------------------ + +class Command(CommandOutputMixIn, BaseCommand): + + option_list = BaseCommand.option_list + ( + make_option('-i','--input', + dest='filename', + action='store', type='string', + default='-', + help=("Optional. Read input from a file rather than from the " + "default standard input." ) + ), + + ) + + help = ( """ Load rangetypes stored in JSON format from standard input.""" ) + + #-------------------------------------------------------------------------- + + def _error( self , rt_name , msg ): + self.print_err( "Failed to register rangetype '%s'!" + " Reason: %s"%( rt_name, msg ) ) + + #-------------------------------------------------------------------------- + + def handle(self, *args, **options): + + # Collect parameters + self.traceback = bool(options.get("traceback", False) ) + self.verbosity = int(options.get('verbosity', 1)) + filename = options.get('filename','-') + + # dataset's (coverages') ids + rt_list = args + + #---------------------------------------------------------------------- + # load and parse the input data + + try : + + if filename == "-" : + + # standard input + rts = json.load( sys.stdin ) + + else : + + # file input + with open(filename,"r") as fin : + rts = json.load( fin ) + + except IOError as e : + + # print stack trace if required + if self.traceback : + self.print_msg(traceback.format_exc()) + + raise CommandError( "Failed to open the input file '%s' ! " + "REASON: %s " % ( filename , str(e) ) ) + + #---------------------------------------------------------------------- + # initialize EOxServer binding + + System.init() + + #---------------------------------------------------------------------- + # insert the range types to DB + + success_count = 0 # success counter - counts finished syncs + + for i,rt in enumerate(rts) : + + # extract RT name + + rt_name = rt.get('name',None) + + if not ( isinstance(rt_name, basestring) and rt_name ) : + + self.print_err( "Range type #%d rejected as it has no valid" + " name."%(i+1) ) + continue + + if isRangeTypeName( rt_name ): + + self.print_err( "The name '%s' is already used by another " + "range type! Import of range type #%d aborted!" \ + %( rt_name , (i+1) ) ) + + continue + + #------------------------------------------------------------------ + + try : + + # create rangetype record + setRangeType( rt ) + + success_count += 1 # increment success counter + + except Exception as e: + + # print stack trace if required + if self.traceback : + self.print_msg(traceback.format_exc()) + + self._error( rt['name'], "%s: %s"%(type(e).__name__, str(e)) ) + + continue # continue by next dataset + + + self.print_msg( "Range type '%s' loaded."%rt['name']) + + #---------------------------------------------------------------------- + # print the final info + + count = len(rts) + error_count = count - success_count + + if ( error_count > 0 ) : + self.print_msg( "Failed to load %d range types." % ( + error_count ) , 1 ) + + if ( success_count > 0 ) : + self.print_msg( "Successfully loaded %d of %s range types." % ( + success_count , count ) , 1 ) + else : + self.print_msg( "No range type loaded." ) + +#------------------------------------------------------------------------------ diff -Nru eoxserver-0.4.0beta2/eoxserver/resources/coverages/management/commands/eoxs_rangetype_list.py eoxserver-0.3.2/eoxserver/resources/coverages/management/commands/eoxs_rangetype_list.py --- eoxserver-0.4.0beta2/eoxserver/resources/coverages/management/commands/eoxs_rangetype_list.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/resources/coverages/management/commands/eoxs_rangetype_list.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,263 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Martin Paces -# -#------------------------------------------------------------------------------- -# Copyright (C) 2011 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -import sys -import traceback - -from optparse import make_option - -from django.core.management.base import BaseCommand, CommandError - -# try the python default json module -try : import json -except ImportError: - #try the original simplejson module - try: import simplejson as json - except ImportError: - #try the simplejson module packed in django - try: import django.utils.simplejson as json - except ImportError: - raise ImportError( "Failed to import any usable json module!" ) - -#------------------------------------------------------------------------------ - -#from eoxserver.core.system import System - -#------------------------------------------------------------------------------ - -from eoxserver.resources.coverages.rangetype import getAllRangeTypeNames -from eoxserver.resources.coverages.rangetype import isRangeTypeName -from eoxserver.resources.coverages.rangetype import getRangeType - -#------------------------------------------------------------------------------ - -from eoxserver.resources.coverages.management.commands import CommandOutputMixIn - -#------------------------------------------------------------------------------ - -class Command(CommandOutputMixIn, BaseCommand): - - option_list = BaseCommand.option_list + ( - make_option('--details', - dest='details', - action='store_true', - default=False, - help=("Optional. Print details of the reangetypes." ) - ), - make_option('--json', - dest='json_dump', - action='store_true', - default=False, - help=("Optional. Dump rangetype(s) in JSON format. This JSON " - "dump can be loaded by another instance of EOxServer." ) - ), - make_option('-o','--output', - dest='filename', - action='store', type='string', - default='-', - help=("Optional. Write output to a file rather than to the default" - " standard output." ) - ), - ) - - args = "[ [ ...]]" - - help = ( - """ - Print either list of all rangetype indentifiers and their details. - When the range-type identifiers are specified than only these rangetypes - are selected. In addition complete rangetypes cans be dumped in JSON - format which can be then loaded by another EOxServer instance. - - NOTE: JSON format of the range-types has slightly changed with the new - range-type data model introduced in the EOxServer version v0.4. - The produced JSON is not backward comatible and cannot be loaded - to EOxServer 0.3.* and earlier. - """ % ({"name": __name__.split(".")[-1]}) - ) - - #-------------------------------------------------------------------------- - - def handle(self, *args, **options): - - # Collect parameters - - self.verbosity = int(options.get('verbosity', 1)) - - print_details = bool(options.get('details',False)) - - print_json = bool(options.get('json_dump',False)) - - filename = options.get('filename','-') - - # dataset's (coverages') ids - rt_list = args - - #---------------------------------------------------------------------- - # check the input rangetype names - - if not rt_list : - - # if no IDs specified get all identifiers - - rt_list = getAllRangeTypeNames() - - else : - - # filter existing range-type names - - def __checkRangeType( rt ) : - rv = isRangeTypeName( rt ) - if not rv : - self.print_err( "Invalid range-type identifier '%s' !"%rt ) - return rv - - rt_list = filter( __checkRangeType , rt_list ) - - #---------------------------------------------------------------------- - # output - - # select the right output driver - - if print_json : output = OutputJSON - elif print_details : output = OutputDetailed - else : output = OutputBrief - - - # write the output - - def _write_out( fout ) : - fout.write( output.lead() ) - for i,rt_name in enumerate(rt_list) : - if i > 0 : fout.write( output.separator() ) - fout.write( output.object( rt_name ) ) - fout.write( output.trail() ) - - # output file - try : - - if filename == "-" : - - # write to stdout - _write_out( sys.stdout ) - - else : - - # write to a file - with open(filename,"w") as fout : - _write_out( fout ) - - except IOError as e : - - raise CommandError( "Failed to open the output file '%s' ! " - "REASON: %s" % ( filename , str(e) ) ) - - -#------------------------------------------------------------------------------ -# output drivers - -class OutputBase: - """ base output driver class class """ - - @classmethod - def lead(cls): return "" - - @classmethod - def object( cls, rt_name ) : return "" - - @classmethod - def trail(cls): return "" - - @classmethod - def separator(cls) : return "" - - -class OutputBrief( OutputBase ): - """ brief text output - RT name only """ - - @classmethod - def object( cls, rt_name ) : return rt_name - - @classmethod - def separator(cls) : return "\n" - - @classmethod - def trail(cls): return "\n" - - -class OutputDetailed( OutputBase ): - """ detailed text output """ - - @classmethod - def lead(cls) : return "\n" - - @classmethod - def trail(cls) : return "\n\n" - - @classmethod - def separator(cls) : return "\n\n" - - @classmethod - def object( cls, rt_name ) : - - rt = getRangeType( rt_name ) - - out = [] - - out.append("Range-Type: %s" % rt.name ) - out.append("\tType:\t\t%s" % rt.getDataTypeAsString()) - out.append("\tNr. of Bands:\t%d" % len(rt.bands)) - out.append("\tBands:") - - for band in rt.bands : - out.append( "\t\t%s"%(band.identifier) ) - - return "\n".join( out ) - - -class OutputJSON( OutputBase ) : - """ JSON output """ - - @classmethod - def lead(cls): - return "[" - - @classmethod - def trail(cls): - return "]\n" - - @classmethod - def separator(cls) : return ",\n" - - @classmethod - def object( cls, rt_name ) : - - # get rangetype as dictionary and dump the json - return json.dumps( getRangeType(rt_name), indent=4, - separators=(',',': '), sort_keys=True ) - - diff -Nru eoxserver-0.4.0beta2/eoxserver/resources/coverages/management/commands/eoxs_rangetype_load.py eoxserver-0.3.2/eoxserver/resources/coverages/management/commands/eoxs_rangetype_load.py --- eoxserver-0.4.0beta2/eoxserver/resources/coverages/management/commands/eoxs_rangetype_load.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/resources/coverages/management/commands/eoxs_rangetype_load.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,188 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Martin Paces -# -#------------------------------------------------------------------------------- -# Copyright (C) 2011 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -import sys -import traceback - -from optparse import make_option - -from django.core.management.base import BaseCommand, CommandError - -# try the python default json module -try : import json -except ImportError: - #try the original simplejson module - try: import simplejson as json - except ImportError: - #try the simplejson module packed in django - try: import django.utils.simplejson as json - except ImportError: - raise ImportError( "Failed to import any usable json module!" ) - -#------------------------------------------------------------------------------ - -from eoxserver.resources.coverages.rangetype import isRangeTypeName -from eoxserver.resources.coverages.rangetype import setRangeType - -#------------------------------------------------------------------------------ - -from eoxserver.resources.coverages.management.commands import CommandOutputMixIn - -#------------------------------------------------------------------------------ - -class Command(CommandOutputMixIn, BaseCommand): - - option_list = BaseCommand.option_list + ( - make_option('-i','--input', - dest='filename', - action='store', type='string', - default='-', - help=("Optional. Read input from a file rather than from the " - "default standard input." ) - ), - ) - - help = ( """ - Load rangetypes stored in JSON format from standard input (defualt) or from - a file (-i option). - - NOTE: This command is supports JSON formats produced by both the new - (>=v0.4) and old (<0.4) versions of the EOxServer. - It is thus possible to export range types from an older EOxServer - instances and import them to a new one. - """ ) - - #-------------------------------------------------------------------------- - - def _error( self , rt_name , msg ): - self.print_err( "Failed to register rangetype '%s'!" - " Reason: %s"%( rt_name, msg ) ) - - #-------------------------------------------------------------------------- - - def handle(self, *args, **options): - - # Collect parameters - self.traceback = bool(options.get("traceback", False) ) - self.verbosity = int(options.get('verbosity', 1)) - filename = options.get('filename','-') - - # dataset's (coverages') ids - rt_list = args - - #---------------------------------------------------------------------- - # load and parse the input data - - try : - - if filename == "-" : - - # standard input - rts = json.load( sys.stdin ) - - else : - - # file input - with open(filename,"r") as fin : - rts = json.load( fin ) - - except IOError as e : - - # print stack trace if required - if self.traceback : - self.print_msg(traceback.format_exc()) - - raise CommandError( "Failed to open the input file '%s' ! " - "REASON: %s " % ( filename , str(e) ) ) - - # allow single range-type objects - if isinstance(rts, dict): - rts = [rts] - #---------------------------------------------------------------------- - # insert the range types to DB - - success_count = 0 # success counter - counts finished syncs - - for i,rt in enumerate(rts) : - - # extract RT name - - rt_name = rt.get('name',None) - - if not ( isinstance(rt_name, basestring) and rt_name ) : - - self.print_err( "Range type #%d rejected as it has no valid" - " name."%(i+1) ) - continue - - if isRangeTypeName( rt_name ): - - self.print_err( "The name '%s' is already used by another " - "range type! Import of range type #%d aborted!" \ - %( rt_name , (i+1) ) ) - - continue - - #------------------------------------------------------------------ - - try : - - # create rangetype record - setRangeType( rt ) - - success_count += 1 # increment success counter - - except Exception as e: - - # print stack trace if required - if self.traceback : - self.print_msg(traceback.format_exc()) - - self._error( rt['name'], "%s: %s"%(type(e).__name__, str(e)) ) - - continue # continue by next dataset - - - self.print_msg( "Range type '%s' loaded."%rt['name']) - - #---------------------------------------------------------------------- - # print the final info - - count = len(rts) - error_count = count - success_count - - if ( error_count > 0 ) : - self.print_msg( "Failed to load %d range types." % ( - error_count ) , 1 ) - - if ( success_count > 0 ) : - self.print_msg( "Successfully loaded %d of %s range types." % ( - success_count , count ) , 1 ) - else : - self.print_msg( "No range type loaded." ) - -#------------------------------------------------------------------------------ diff -Nru eoxserver-0.4.0beta2/eoxserver/resources/coverages/management/commands/eoxs_register_dataset.py eoxserver-0.3.2/eoxserver/resources/coverages/management/commands/eoxs_register_dataset.py --- eoxserver-0.4.0beta2/eoxserver/resources/coverages/management/commands/eoxs_register_dataset.py 1970-01-01 00:00:00.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/resources/coverages/management/commands/eoxs_register_dataset.py 2013-12-10 14:57:00.000000000 +0000 @@ -0,0 +1,681 @@ +#------------------------------------------------------------------------------- +# $Id$ +# +# Project: EOxServer +# Authors: Stephan Krause +# Stephan Meissl +# Fabian Schindler +# Martin Paces +# +#------------------------------------------------------------------------------- +# Copyright (C) 2011 EOX IT Services GmbH +# +# 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 of this Software or works derived from this 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. +#------------------------------------------------------------------------------- + +import os.path +from copy import copy +import traceback +from optparse import make_option, OptionValueError + +from django.core.management.base import BaseCommand, CommandError +from django.db import transaction +from django.contrib.gis.geos.geometry import GEOSGeometry +from django.contrib.gis.geos.polygon import Polygon + +from eoxserver.contrib import gdal +from eoxserver.core.system import System +from eoxserver.core.util.timetools import getDateTime +from eoxserver.resources.coverages.geo import ( + GeospatialMetadata, getExtentFromRectifiedDS, getExtentFromReferenceableDS +) +from eoxserver.resources.coverages.management.commands import ( + CommandOutputMixIn, _variable_args_cb, StringFormatCallback, + _footprint, _size, _extent, +) +from eoxserver.resources.coverages.metadata import EOMetadata + +from eoxserver.processing.gdal import reftools as rt + +#------------------------------------------------------------------------------ + +from eoxserver.resources.coverages.managers import getRectifiedDatasetManager +from eoxserver.resources.coverages.managers import getReferenceableDatasetManager +from eoxserver.resources.coverages.managers import getRectifiedStitchedMosaicManager +from eoxserver.resources.coverages.managers import getDatasetSeriesManager + +from eoxserver.resources.coverages.rangetype import isRangeTypeName + +#------------------------------------------------------------------------------ + +def _extract_geo_md( fname, default_srid = None ) : + """ + Extract geo-meta-data from the source data file if possible. + The ``default_srid`` parameter can be specified as an fallback + when GDAL fails determine EPSG code of image or GCP projection. + """ + + # TODO: for rasdaman build identifiers + # TODO: FTP input + + ds = gdal.Open(fname) + + if ds is None : return None + + return GeospatialMetadata.readFromDataset(ds, default_srid) + + +#------------------------------------------------------------------------------ + +class Command(CommandOutputMixIn, BaseCommand): + option_list = BaseCommand.option_list + ( + make_option('-d', '--data-file', '--data-files', + '--collection', '--collections', + dest='datafiles', + action='callback', callback=_variable_args_cb, + default=[], + help=('Mandatory. One or more input data specifications.' + 'The data specification can be either path to a local file, ' + 'FTP path, or rasdaman collection name.') + ), + make_option('-m', '--metadata-file', '--metadata-files', + dest='metadatafiles', + action='callback', callback=_variable_args_cb, + default=[], + help=("Optional. One or more EO meta-data specifications." + "By default, the mata-data is either retrieved directly " + "form the input data or from the accompaninig XML file " + "(having the same location and basename as the data files " + "but '.xml' extension instead of the original one)." + "The external XML file overides the metada stored" + "directly in the datafile." ) + ), + make_option('-r', '--range-type', '--rangetype', + dest='rangetype', + default=None, + help=('Mandatory identifier of the range-type of all the datasets' + 'being registered.' ) + ), + make_option('--series','--dataset-series', + dest='ids_series', + action='callback', callback=_variable_args_cb, + default=[], + help=('Optional. One or more ids of dataset series ' + 'to which the registerd dataset(s) shall be added.') + ), + make_option('--mosaic','--stitched-mosaic', + dest='ids_mosaic', + action='callback', callback=_variable_args_cb, + default=[], + help=('Optional. One or more ids of rectified stitched mosaics ' + 'to which the registered dataset(s) shall be added.') + ), + make_option('-i', '--id' , '--ids', '--coverage-id', '--coverage-ids', + dest='ids', + action='callback', callback=_variable_args_cb, + default=[], + help=('Optional. One or more identifiers for each dataset ' + 'that shall be added. The default name is derived either ' + 'from the EO metadata or from the data file base name.') + ), + make_option('-s','--source-type','--mode', + dest='source_type', + choices=['local', 'ftp', 'rasdaman'], + default='local', + help=("Optional. The source type of the datasets to be " + "registered. Currently 'local', 'ftp', or 'rasdaman' " + "backends are supported. Defaults to 'local'.") + ), + make_option('--host', + dest='host', + default=None, + help=("Mandatory for non-local backends. The host address " + "where the remote datasets are located.") + ), + make_option('--port', + dest='port', type='int', + default=None, + help=("Optional. Non-default port to access remote datasets.") + ), + make_option('--user', + dest='user', + default=None, + help=("Optional. Username needed to access remote datasets.") + ), + make_option('--password', + dest='password', + default=None, + help=("Optional. Plain text password needed to access remote " + "datasets.") + ), + make_option('--database','--rasdb', + dest='rasdb', + default=None, + help=("Optional. Name of the rasdaman holding the registered " + "data.") + ), + make_option('--oid', '--oids', + dest='oids', + action='callback', callback=_variable_args_cb, + default=[], + help=("Optional. List of rasdaman oids for each dataset " + "to be inserted.") + ), + make_option('--srid', '--default-srid', + dest='srid', + default=None, + help=("Optional. SRID (EPSG code) of the dataset if it cannot be " + "determined automatically.") + ), + make_option('--size', '--default-size', + dest='size', + action="callback", callback=StringFormatCallback(_size), + default=None, + help=("Optional. Dataset pixel size if it cannot be determined " + "automatically. Format: ,") + ), + make_option('--extent', '--default-extent', + dest='extent', + action="callback", callback=StringFormatCallback(_extent), + default=None, + help=("Optional. Dataset extent if it cannot be determined " + "automatically. Format: ,,,") + ), + make_option('--begin-time', '--default-begin-time', + dest='begin_time', + action="callback", callback=StringFormatCallback(getDateTime), + + default=None, + help=("Optional. Acquisition begin timestamp if not available " + "from the EO metadata in ISO-8601 format.") + ), + make_option('--end-time', '--default-end-time', + dest='end_time', + action="callback", callback=StringFormatCallback(getDateTime), + default=None, + help=("Optional. Acquisition end timestamp if not available " + "from the EO metadata in ISO-8601 format.") + ), + make_option('--footprint', '--default-footprint', + dest='footprint', + action="callback", callback=StringFormatCallback(_footprint), + default=None, + help=("Optional. Footprint of the dataset if not available " + "from the EO metadata in WKT format.") + ), + make_option('--visible', + dest='visible', + action="store_true", + default=True, + help=("Optional. It enables the visibility flag for all datasets " + "being registered. (Visibility enabled by default)") + ), + make_option('--invisible','--hidden', + dest='visible', + action="store_false", + help=("Optional. It disables the visibility flag for all datasets " + "being registered. (Visibility enabled by default)") + ), + + make_option('--ref','--referenceable', + dest='is_ref_ds', + action="store_true", + help=("Optional. It indicates that the created dataset is " + "a Referenceable dataset. (Relevanat only for manually " + "specified geo-meta-data. Ignored for automatically detected" + " local datasets)") + ), + make_option('--rect','--rectified', + dest='is_ref_ds', + action="store_false", + default=False, + help=("Optional. Default. It indicates that the created dataset is" + " a Rectified dataset. (Relevanat only for manually " + "specified geo-meta-data. Ignored for automatically detected" + " local datasets)") + ), + + ) + + help = ( + """ + Registers one or more datasets from each data and meta-data file. + + Examples: + Using shell expansion of filenames and automatic metadata retrieval: + python manage.py %(name)s \\ + --data-files data/meris/mosaic_MER_FRS_1P_RGB_reduced/*.tif \\ + --range-type RGB --dataset-series MER_FRS_1P_RGB_reduced \\ + --stitched-mosaic mosaic_MER_FRS_1P_RGB_reduced -v3 + + Manual selection of data/metadata files: + python manage.py %(name)s \\ + --data-files 1.tif 2.tif 3.tif \\ + --metadata-files 1.xml 2.xml 3.xml \\ + --ids a b c --range-type RGB -v3 + + Registering a rasdaman coverage: + python manage.py %(name)s \\ + --source-type=rasdaman --host=some.host.com --port=8080 \\ + --user=db_user --password=secret \\ + --collection MER_FRS_1PNPDE..._reduced \\ + --srid=4326 --size=539,448 \\ + --extent=11.361066,32.201446,28.283846,46.252026 \\ + --begin-time "`date -u --iso-8601=seconds`" \\ + --end-time "`date -u --iso-8601=seconds`" \\ + --footprint "POLYGON ((11.3610659999999992 + 32.2014459999999971, 11.3610659999999992 + 46.2520260000000007, 28.2838460000000005 + 46.2520260000000007, 28.2838460000000005 + 32.2014459999999971, 11.3610659999999992 + 32.2014459999999971))" \\ + --id MER_FRS_1PNPDE..._reduced --range-type RGB -v3 + """ % ({"name": __name__.split(".")[-1]}) + ) + args = '--data-file --range-type ' + + #-------------------------------------------------------------------------- + + def _error( self , ds , msg ): + self.print_err( "Failed to register dataset '%s'!" + " Reason: %s"%( ds, msg ) ) + + #-------------------------------------------------------------------------- + + def handle(self, *args, **opt): + + System.init() + + # prepare dataset managers + + dsMngr = { + "RectifiedDataset" : getRectifiedDatasetManager() , + "ReferenceableDataset" : getReferenceableDatasetManager() } + + dsMngr_mosaic = getRectifiedStitchedMosaicManager() + dsMngr_series = getDatasetSeriesManager() + + #----------------------------------------------------------------------- + # extract some of the inputs + + self.verbosity = int(opt.get('verbosity', 1)) + self.traceback = bool( opt.get("traceback", False) ) + + src_data = opt.get('datafiles',[]) + src_meta = opt.get('metadatafiles',[]) + src_ids = opt.get('ids',[]) + + range_type = opt.get('rangetype',None) + + source_type = opt.get('source_type','local') + + visibility = opt.get("visible", True ) + + ids_mosaic = list( set( opt["ids_mosaic"] ) ) + ids_series = list( set( opt["ids_series"] ) ) + + ids_cont = ids_mosaic + ids_series # merged containers + + _has_explicit_md = any([ ( opt[i] is not None ) for i in + ('size','extent','begin_time','end_time','footprint') ]) + + # ... the rest of the options stays in the ``opt`` dictionary + + #----------------------------------------------------------------------- + # check the required inputs + + if not src_data : + raise CommandError( "Missing specification of the data to be " + "registered!") + + if not range_type : + raise CommandError( "Missing the mandatory range type" + " specification." ) + + if src_meta and ( len(src_meta) != len(src_data) ) : + raise CommandError( "The number of metadata files does not match" + " the number of input data items!" ) + + if src_ids and ( len(src_ids) != len(src_data) ) : + raise CommandError( "The number of IDs does not match " + " the number of input data items!" ) + + if source_type == "rasdaman" : + + if opt['oids'] and ( len(opt['oids']) != len(src_data) ) : + raise CommandError("The number of Rasdaman OIDs does not match" + " the number of input data items!" ) + + if ( len(src_data) > len(src_ids) ) : + raise CommandError("Rasdaman datasets require explicite " + "specification of coverage/EO-IDs !") + + if ( len(src_data) > len(src_meta) ) and \ + ( ( len(src_data) > 1 ) or ( not _has_explicit_md ) ) : + raise CommandError("Rasdaman datasets require explicite " + "specification of metadata stored in files or passed " + "as commandline arguments!") + else: + + if opt['oids'] : + raise CommandError( "The Rasdaman OIDs are not expected to be" + " provided for %s data source!"%source_type ) + + if opt['rasdb'] : + raise CommandError( "The Rasdaman DB is not expected to be" + " provided for %s data source!"%source_type ) + + if source_type != "local" : + + if not opt['host'] : + raise CommandError( "The host must be specified for non-local" + " data sources!" ) + + if (opt['port'] is not None) and \ + ((opt['port']<1) or (opt['port']>65535)) : + raise CommandError("Invalid port number! port=%d"%opt['port']) + + #----------------------------------------------------------------------- + # check that metadata specified via the CLI + # note that these can be set for one DS only + + if ( 1 < len(src_data) ) and _has_explicit_md : + raise CommandError( "Specification of meta-data via the CLI is " + "allowed for single dataset only!" ) + + if ( opt["size"] is None ) != ( opt["extent"] is None ) : + raise CommandError( "Both 'size' and 'extent' metadata must be " + "specified but only one of them is actually provided!" ) + + if ( opt["extent"] is not None ) and ( opt["srid"] is None ) : + raise CommandError( "The 'extent' metadata require SRID to be " + "specified!" ) + + if ( opt["begin_time"] is None ) != ( opt["end_time"] is None ) : + raise CommandError( "Both 'begin_time' and 'end_time' metadata " + "must be specified but only one of them is actually provided!") + + #----------------------------------------------------------------------- + # handle the user specified geo-meta-data + + if ( opt["extent"] is not None ) : + + geo_metadata = GeospatialMetadata( opt["srid"], opt["size"][0], + opt["size"][1], opt["extent"] , opt["is_ref_ds"] ) + + else : + + geo_metadata = None + + #----------------------------------------------------------------------- + # handle the user specified EO-meta-data + + if (opt['begin_time'] is not None) and (opt['end_time'] is not None): + + footprint = opt['footprint'] + + # try to extract the missing footprint + if footprint is None : + + # try to extract missing geo-metadata for local file + if source_type == "local" : + + # read the geo-metada if not given manually + if geo_metadata is None : + + geo_metadata = _extract_geo_md(src_data[0],opt["srid"]) + + # if some geo-metadata are given we try to get the FP + if geo_metadata is not None : + + # referenceable DS - trying to extract FP from GCPs + if geo_metadata.is_referenceable : + + rt_prm = rt.suggest_transformer(src_data[0]) + fp_wkt = rt.get_footprint_wkt(src_data[0],**rt_prm) + footprint = GEOSGeometry( fp_wkt ) + + # for referenceable DSs we extract footprint from extent + else : + + footprint = Polygon.from_bbox(geo_metadata.extent) + + else : # remote data + + # we rely on the manually given extent for rectified DSs + if ( geo_metadata is not None ) and \ + ( not geo_metadata.is_referenceable ) : + + footprint = Polygon.from_bbox( geo_metadata.extent ) + + + # raise an error if the footprint extraction failed + if footprint is None : + + raise CommandError( "Cannot extract 'footprint' from the " + "dataset. It must be set explicitely via CLI!" ) + + # create EOMetadata object + + eo_metadata = EOMetadata( src_ids[0], opt["begin_time"], + opt["end_time"], opt["footprint"], None ) + + else : + + eo_metadata = None + + #----------------------------------------------------------------------- + # create the automatic IDs, filenames, and OIDs + + def __make_id( src ) : + return os.path.splitext( os.path.basename( src ) )[0] + + def __make_md( src ) : + fname = "%s.xml"%os.path.splitext( src )[0] + if not os.path.exists( fname ) : + fname = src + return fname + + if not src_ids : + src_ids = [ __make_id(fn) for fn in src_data ] + + if not src_meta : + src_meta = [ __make_md(fn) for fn in src_data ] + + if ( source_type == "rasdaman" ) and not opt['oids'] : + opt['oids'] = [ None for i in xrange(len(src_data)) ] + + #----------------------------------------------------------------------- + # verify the identifiers agaist the DB + + # range-type + + if not isRangeTypeName( range_type ) : + raise CommandError( "Invalid range-type identifier '%s' !" \ + % range_type ) + + # check rectified stitched mosaics + + for mosaic in ids_mosaic : + if not dsMngr_mosaic.check_id( mosaic ) : + raise CommandError( "Invalid Rectified Stitched Mosaic " + "identifier '%s' !" % mosaic ) + + # check datasets series + + for series in ids_series : + if not dsMngr_series.check_id( series ) : + raise CommandError( "Invalid Dataset Series identifier " + "'%s' !" % series ) + + #----------------------------------------------------------------------- + # debug print + + self.print_msg("Range type: %s"%(range_type),2) + self.print_msg("Visibility: %s"%(["HIDDEN","VISIBLE"][visibility]),2) + self.print_msg("Data IDs: %s"%(" ".join(src_ids)),2) + self.print_msg("Source data: %s"%(" ".join(src_data)),2) + self.print_msg("Metadata: %s"%(" ".join(src_meta)),2) + self.print_msg("Mosaics: %s"%(" ".join(ids_mosaic)),2) + self.print_msg("Series: %s"%(" ".join(ids_series)),2) + + if source_type == "rasdaman" : + self.print_msg("Rasd. OIDs: %s"%(" ".join(opt["oids"])),2) + self.print_msg("Rasd. DB: %s"%( opt["rasdb"] ),2) + + self.print_msg("Source type: %s"%(source_type),2) + + if source_type != "local" : + self.print_msg("- host: %s"%(opt["host"]),2) + self.print_msg("- port: %s"%(opt["port"]),2) + self.print_msg("- user: %s"%(opt["user"]),2) + self.print_msg("- passwd: %s"%(opt["password"]),2) + + self.print_msg("CLI metadata:",2) + + for i in ('srid','size','extent','begin_time','end_time','footprint'): + self.print_msg("- %-8s\t%s" % ( "%s:"%i , opt[i] ) , 2 ) + + #----------------------------------------------------------------------- + # register the idividual datasets + + success_count = 0 # count successfull actions + + for df, mdf, cid in zip( src_data, src_meta, src_ids ) : + + self.print_msg( "Registering dataset: '%s'" % cid ) + + # store geo-metadata in a local variable + _geo_metadata = geo_metadata + + # commmon parameters + + prm = {} + + prm["obj_id"] = cid + prm["range_type_name"] = range_type + prm["default_srid"] = opt["srid"] + prm["container_ids"] = ids_cont + prm["visible"] = opt["visible"] + + # source specific parameters + + if source_type == "local" : + + prm["local_path"] = df + prm["md_local_path"] = mdf + + # try to extract geo-metadata + # NOTE: The geo-metadata can be extracted from local data only! + if _geo_metadata is None : + try: + _geo_metadata = _extract_geo_md( df, opt["srid"] ) + except Exception as e: + # print stack trace if required + if self.traceback : + self.print_msg(traceback.format_exc()) + + self._error( cid, "%s: %s"%(type(e).__name__, str(e)) ) + + continue # continue by next dataset + + elif source_type == "ftp" : + + prm["remote_path"] = df + prm["md_remote_path"] = mdf + prm["ftp_host"] = opt["host"] + prm["ftp_port"] = opt["port"] + prm["ftp_user"] = opt["user"] + prm["ftp_passwd"] = opt["password"] + + elif source_type == "rasdaman" : + + prm["collection"] = df, + prm["md_local_path"] = mdf, + prm["oid"] = opt["oids"].pop(0) + prm["ras_host"] = opt["host"] + prm["ras_port"] = opt["port"] + prm["ras_user"] = opt["user"] + prm["ras_passwd"] = opt["password"] + prm["ras_db"] = opt["rasdb"] + + #------------------------------------------------------------------- + # insert metadata if available + + if eo_metadata is not None: + prm["eo_metadata"] = eo_metadata + + if _geo_metadata is not None: + prm["geo_metadata"] = _geo_metadata + + #------------------------------------------------------------------- + # select dataset manager + + # TODO: Fix the ReferenceableDataset selection! + # What will happen in case of rectified DS and _geo_metadata being None + + # unless changed we assume rectified DS + dsType = "RectifiedDataset" + + if (_geo_metadata is not None) and _geo_metadata.is_referenceable : + + # the DS is refereanceable + dsType = "ReferenceableDataset" + + #------------------------------------------------------------------- + # perform the actual dataset registration + + try: + + with transaction.commit_on_success(): + self.print_msg( "Creating new dataset ...",2) + dsMngr[dsType].create( **prm ) + + except Exception as e: + + # print stack trace if required + if self.traceback : + self.print_msg(traceback.format_exc()) + + self._error( cid, "%s: %s"%(type(e).__name__, str(e)) ) + + continue # continue by next dataset + + success_count += 1 #increment the success counter + self.print_msg( "Dataset successfully registered.",2) + + + #----------------------------------------------------------------------- + # print the final info + + count = len(src_data) + error_count = count - success_count + + if ( error_count > 0 ) : + self.print_msg( "Failed to register %d dataset%s." % ( + error_count , ("","s")[error_count!=1] ) , 1 ) + + if ( success_count > 0 ) : + self.print_msg( "Successfully registered %d of %s dataset%s." % ( + success_count , count , ("","s")[count!=1] ) , 1 ) + else : + self.print_msg( "No dataset registered." ) + + if ( error_count > 0 ) : + raise CommandError("Not all datasets could be registered.") diff -Nru eoxserver-0.4.0beta2/eoxserver/resources/coverages/management/commands/eoxs_remove_from_series.py eoxserver-0.3.2/eoxserver/resources/coverages/management/commands/eoxs_remove_from_series.py --- eoxserver-0.4.0beta2/eoxserver/resources/coverages/management/commands/eoxs_remove_from_series.py 1970-01-01 00:00:00.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/resources/coverages/management/commands/eoxs_remove_from_series.py 2013-12-10 14:57:00.000000000 +0000 @@ -0,0 +1,45 @@ +#------------------------------------------------------------------------------- +# $Id$ +# +# Project: EOxServer +# Authors: Stephan Krause +# Stephan Meissl +# Fabian Schindler +# +#------------------------------------------------------------------------------- +# Copyright (C) 2011 EOX IT Services GmbH +# +# 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 of this Software or works derived from this 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. +#------------------------------------------------------------------------------- + +from eoxserver.resources.coverages.management.commands import ManageDatasetSeriesCommand + +class Command(ManageDatasetSeriesCommand): + + help=("Remove one or more datasets (DS) from one or more specified dataset" + " series (DSS).") + + def manage_series(self, manager, dataset_ids, datasetseries_ids): + """Main method for dataset handling.""" + + #TODO: check if the removed datasets are contained by the series + + for dssid in datasetseries_ids: + + manager.update( obj_id=dssid, unlink={"coverage_ids":dataset_ids}) diff -Nru eoxserver-0.4.0beta2/eoxserver/resources/coverages/management/commands/eoxs_synchronize.py eoxserver-0.3.2/eoxserver/resources/coverages/management/commands/eoxs_synchronize.py --- eoxserver-0.4.0beta2/eoxserver/resources/coverages/management/commands/eoxs_synchronize.py 1970-01-01 00:00:00.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/resources/coverages/management/commands/eoxs_synchronize.py 2013-12-10 14:57:00.000000000 +0000 @@ -0,0 +1,174 @@ +#------------------------------------------------------------------------------- +# $Id$ +# +# Project: EOxServer +# Authors: Stephan Krause +# Stephan Meissl +# Fabian Schindler +# +#------------------------------------------------------------------------------- +# Copyright (C) 2011 EOX IT Services GmbH +# +# 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 of this Software or works derived from this 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. +#------------------------------------------------------------------------------- + +import traceback + +from optparse import make_option + +from django.db import transaction +from django.core.management.base import BaseCommand, CommandError + +from eoxserver.core.system import System +from eoxserver.resources.coverages.management.commands import CommandOutputMixIn + +#------------------------------------------------------------------------------ + +from eoxserver.resources.coverages.managers import CoverageIdManager +from eoxserver.resources.coverages.managers import getRectifiedStitchedMosaicManager +from eoxserver.resources.coverages.managers import getDatasetSeriesManager + +#------------------------------------------------------------------------------ + +class Command(CommandOutputMixIn, BaseCommand): + + option_list = BaseCommand.option_list + ( + make_option('-a','--all', + action='store_true', + dest='synchronise_all', + default=False, + help=('Optional switch to enable the synchronization for all ' + 'registered containers.') + ), + ) + + args = '--all | [ ...]' + + help = ( + """ + Synchronizes all specified containers (RectifiedStitchedMosaic or + DatasetSeries) with the file system. + + Examples: + python manage.py %(name)s --all + + python manage.py %(name)s MER_FRS_1P_RGB_reduced \\ + MER_FRS_1P_reduced + """ % ({"name": __name__.split(".")[-1]}) + ) + + #-------------------------------------------------------------------------- + + def _error( self , entity , ds , msg ): + self.print_err( "Failed to synchronise %s '%s'!" + " Reason: %s"%( entity , ds, msg ) ) + + #-------------------------------------------------------------------------- + + def handle(self, *args, **options): + + # set up + + System.init() + + self.verbosity = int(options.get('verbosity', 1)) + + + #---------------------------------------------------------------------- + # prepare managers + + dsMngr = { + "RectifiedStitchedMosaic" : getRectifiedStitchedMosaicManager(), + "DatasetSeries" : getDatasetSeriesManager() } + + cidMngr = CoverageIdManager() + + #---------------------------------------------------------------------- + # parse arguments + + ids = [] + + if options.get("synchronise_all", False): + + # synchronise all container entities + for mngr in dsMngr.values() : + ids.extend( mngr.get_all_ids() ) + + else: + + # read ids from the commandline + ids.extend( args ) + + + #---------------------------------------------------------------------- + # synchronise objects + + success_count = 0 # success counter - counts finished syncs + + for id in ids : + + # get type of the entity + + dsType = cidMngr.getType( id ) + + # check the entity type + + if not dsMngr.has_key( dsType ) : + self.print_msg( "'%s' is neither mosaic nor series!"%id,2) + self._error( id , "Invalid identifier." ) + continue # continue by next entity + + self.print_msg( "Synchronising %s: '%s'" % ( dsType, id ) ) + + try: + + with transaction.commit_on_success(): + dsMngr[dsType].synchronize(id) + + except Exception as e : + + # print stack trace if required + if options.get("traceback", False): + self.print_msg(traceback.format_exc()) + + self._error( dsType, id, "%s: %s"%(type(e).__name__, str(e)) ) + + continue # continue by next dataset + + success_count += 1 #increment the success counter + self.print_msg( "%s successfully synchronised."%dsType,2) + + #---------------------------------------------------------------------- + # print the final info + + count = len(ids) + error_count = count - success_count + + if ( error_count > 0 ) : + self.print_msg( "Failed to synchronise %d objects." % ( + error_count ) , 1 ) + + if ( success_count > 0 ) : + self.print_msg( "Successfully synchronised %d of %s objects." % ( + success_count , count ) , 1 ) + else : + self.print_msg( "No object synchronised." ) + + if ( error_count > 0 ) : + raise CommandError("Not all objects could be synchronised.") diff -Nru eoxserver-0.4.0beta2/eoxserver/resources/coverages/management/commands/__init__.py eoxserver-0.3.2/eoxserver/resources/coverages/management/commands/__init__.py --- eoxserver-0.4.0beta2/eoxserver/resources/coverages/management/commands/__init__.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/resources/coverages/management/commands/__init__.py 2013-12-10 14:57:00.000000000 +0000 @@ -1,4 +1,5 @@ #------------------------------------------------------------------------------- +# $Id$ # # Project: EOxServer # Authors: Stephan Krause @@ -11,8 +12,8 @@ # 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 +# 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 @@ -35,9 +36,25 @@ from django.core.management.base import BaseCommand, CommandError from django.db import transaction +from eoxserver.core.system import System +from eoxserver.backends.local import LocalPath +from eoxserver.backends.ftp import RemotePath +from eoxserver.resources.coverages.models import LocalDataPackage,\ + RemoteDataPackage +from eoxserver.resources.coverages.exceptions import NoSuchCoverageException + +from django.contrib.gis.geos.geometry import GEOSGeometry + +#------------------------------------------------------------------------------- + +from eoxserver.resources.coverages.managers import CoverageIdManager +from eoxserver.resources.coverages.managers import getDatasetSeriesManager + +#------------------------------------------------------------------------------- logger = logging.getLogger(__name__) +#------------------------------------------------------------------------------- def _variable_args_cb(option, opt_str, value, parser): """ Helper function for optparse module. Allows @@ -74,38 +91,40 @@ del parser.rargs[:len(args)] break - try: + try : setattr(parser.values, option.dest, self.callback(" ".join(args))) - except ValueError, e: - raise OptionValueError(str(e)) + except ValueError as e : + raise OptionValueError( e.message ) class CommandOutputMixIn(object): - """ Helper mix-in class to ease the handling of user message reporting. - """ - def print_err(self, msg): - " Print an error message which is both logged and written to stderr. " + def print_err(self, msg ): + + # single level for all errors + # errors ALWAYS printed + # errors ALWAYS logged logger.error(msg) + self.stderr.write("ERROR: %s\n"%msg) - def print_wrn(self, msg): - """ Print a warning message, which is logged and posibly written to - stderr, depending on the set verbosity. - """ + def print_wrn(self, msg ): + + verbl = max(0,getattr(self, "verbosity", 1)) + + # single level for all warnings + # print of warnings suppressed in silent mode + # warnings ALWAYS logged logger.warning(msg) - if 0 < max(0, getattr(self, "verbosity", 1)): - self.stderr.write("WARNING: %s\n" % msg) + if 0 < verbl : + self.stderr.write("WARNING: %s\n"%msg) def print_msg(self, msg, level=1): - """ Print a basic message with a given level. The message is possibly - logged and/or written to stderr depending on the verbosity setting. - """ # three basic level of info messages # level == 0 - always printed even in the silent mode - not recommended @@ -113,49 +132,192 @@ # level >= 2 - debuging message (additional levels allowed) # messages ALWAYS logged (as either info or debug) - level = max(0, level) - verbosity = max(0, getattr(self, "verbosity", 1)) + level = max(0,level) + verbl = max(0,getattr(self, "verbosity", 1)) + + if level >= 2 : # everything with level 2 or higher is DEBUG - # everything with level 2 or higher is DEBUG - if level >= 2: prefix = "DEBUG" logger.debug(msg) - # levels 0 (silent) and 1 (default-verbose) - else: + else : # levels 0 (silent) and 1 (default-verbose) + prefix = "INFO" logger.info(msg) - if level <= verbosity: - self.stdout.write("%s: %s\n" % (prefix, msg)) + if ( level <= verbl ) : + self.stdout.write("%s: %s\n"%(prefix,msg)) - def print_traceback(self, e, kwargs): - """ Prints a traceback/stacktrace if the traceback option is set. - """ - if kwargs.get("traceback", False): - self.print_msg(traceback.format_exc()) +#------------------------------------------------------------------------------- +def get_dataset_ids_for_path(self, path): + "Return IDs of all datasets that are referencing the given path." + + url = urlparse(path, "file") + + if url.scheme == "file": + datapackages = LocalDataPackage.objects.filter( + data_location__path=path + ) + elif url.scheme == "ftp": + datapackages = RemoteDataPackage.objects.filter( + data_location__path=path, + data_location__storage__host=url.hostname, + data_location__storage__port=url.port, + data_location__storage__user=url.username, + data_location__storage__passwd=url.password + ) + elif url.scheme == "rasdaman": + raise NotImplementedError() + else: + raise CommandError("Unknown location type '%s'." % url.scheme) + + result = [] + + for record in datapackages: + datapackage = System.getRegistry().getFromFactory( + factory_id="resources.coverages.data.DataPackageFactory", + params={ + "record": record + } + ) + + result.extend([coverage.getCoverageId() for coverage in datapackage.getCoverages()]) + + if len(result) == 0: + raise CommandError("No dataset matching the given path found. PATH='%s'" % path) + + return result +#------------------------------------------------------------------------------- +# parsers - auxiliary subroutines -def nested_commit_on_success(func): - """Like commit_on_success, but doesn't commit existing transactions. +# footprint parser - This decorator is used to run a function within the scope of a - database transaction, committing the transaction on success and - rolling it back if an exception occurs. +def _footprint( src ) : + try: + return GEOSGeometry( src ) + except ValueError : + raise ValueError("Invalid 'footprint' specification '%s' !"%src ) + +# size parser - Unlike the standard transaction.commit_on_success decorator, this - version first checks whether a transaction is already active. If so - then it doesn't perform any commits or rollbacks, leaving that up to - whoever is managing the active transaction. +def _size( src ) : - From: https://djangosnippets.org/snippets/1343/ - """ + print "SRC-SIZE:" , src - commit_on_success = transaction.commit_on_success(func) - def _nested_commit_on_success(*args, **kwargs): - if transaction.is_managed(): - return func(*args, **kwargs) - else: - return commit_on_success(*args, **kwargs) - return transaction.wraps(func)(_nested_commit_on_success) + try: + tmp = tuple([ int(v) for v in src.split(",") ]) + print "TMP" , tmp , len(tmp) + if len(tmp) != 2 : raise ValueError + if ( tmp[0] < 0 ) or ( tmp[1] < 0 ) : raise ValueError + return tmp + except ValueError : + raise ValueError("Invalid 'size' specification '%s' !"%src) + + +# extent parser + +def _extent( src ) : + + try: + tmp = tuple([ float(v) for v in src.split(",") ]) + if len(tmp) != 4 : raise ValueError + return tmp + except ValueError : + raise ValueError("Invalid 'extent' specification '%s' !"%src) + +#------------------------------------------------------------------------------- + +class ManageDatasetSeriesCommand(BaseCommand, CommandOutputMixIn): + """Base class for dataset series content mangement commands.""" + + args = ("-d DS1 [DS2 [...]] -s DSS1 [DSS2 [...]]") + + option_list = BaseCommand.option_list + ( + make_option("-d","--dataset", "--datasets", dest="dataset_ids", + action="callback", callback=_variable_args_cb, default=None, + help=("Optional. One or more IDs of Datasets (either the Coverage " + "ID or the EO-ID).") + ), + make_option("-s","--dataset-series", dest="datasetseries_ids", + action="callback", callback=_variable_args_cb, default=None, + help=("Optional. One or more EO-IDs referencing Dataset Series.") + ), +# make_option("-m","--mode", dest="mode", default="id", +# choices=("id", "filename"), +# help=("Optional. This parameter defines how the datasets are " +# "identified.") +# ) + ) + + def handle(self, *args, **options): + + System.init() + + id_manager = CoverageIdManager() + dss_manager = getDatasetSeriesManager() + + self.verbosity = options["verbosity"] + #mode = options["mode"] + + if ((options["dataset_ids"] is None + or options["datasetseries_ids"] is None) and + len(args) < 2): + raise CommandError("Not enough arguments given.") + + # MP: WARNING Non-documented args semantics! + dataset_ids = options["dataset_ids"] or args[:-1] + datasetseries_ids = options["datasetseries_ids"] or args[-1:] + + # TODO: make arbitrary insertions possible, like data sources, etc. + + # MP: The filename to ID conversion should be done elsewehere + # do not create another egg-laying cow-monster! + # + #if mode == "filename": + # files = dataset_ids + # dataset_ids = [] + + # for path in files: + # dataset_ids.extend(self.get_dataset_ids_for_path(path)) + + # check the ids - report the non-existing ones! + + def check_ds_id( id ) : + if id_manager.getType(id) in ( "RectifiedDataset" , + "ReferenceableDataset" ) : + return True + else: + self.print_err( "Invalid dataset ID excluded from the input" + " list! ID='%s'" % ( id ) ) + return False + + def check_dss_id( id ) : + if id_manager.getType(id) == "DatasetSeries" : + return True + else: + self.print_err( "Invalid dataset series ID excluded from the input" + " list! ID='%s'" % ( id ) ) + return False + + dataset_ids = filter( check_ds_id , dataset_ids ) + datasetseries_ids = filter( check_dss_id , datasetseries_ids ) + + # stop if one of the lists is empty + if (len(dataset_ids)<1) or (len(datasetseries_ids)<1) : return + + # otherwise perform the action + with transaction.commit_on_success(): + try: + self.manage_series(dss_manager,dataset_ids,datasetseries_ids) + except NoSuchCoverageException, e: + raise CommandError("No coverage with ID '%s' registered" % e.msg) + + + def manage_series(self, manager, dataset_ids, datasetseries_ids): + """ Main method for dataset handling.""" + # to be implemented by the derived classes + raise NotImplementedError() + +#------------------------------------------------------------------------------- diff -Nru eoxserver-0.4.0beta2/eoxserver/resources/coverages/managers/base.py eoxserver-0.3.2/eoxserver/resources/coverages/managers/base.py --- eoxserver-0.4.0beta2/eoxserver/resources/coverages/managers/base.py 1970-01-01 00:00:00.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/resources/coverages/managers/base.py 2013-12-10 14:57:00.000000000 +0000 @@ -0,0 +1,393 @@ +#------------------------------------------------------------------------------- +# $Id$ +# +# Project: EOxServer +# Authors: Stephan Krause +# Stephan Meissl +# Martin Paces +# +#------------------------------------------------------------------------------- +# Copyright (C) 2011 EOX IT Services GmbH +# +# 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 of this Software or works derived from this 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. +#------------------------------------------------------------------------------- + +import os.path +import logging + +from uuid import uuid4 + +from eoxserver.core.system import System +from eoxserver.core.exceptions import InternalError +from eoxserver.resources.coverages.exceptions import ManagerError +from eoxserver.resources.coverages.exceptions import NoSuchCoverageException + +#------------------------------------------------------------------------------- + +from eoxserver.resources.coverages.managers.id_manager import CoverageIdManager + +#------------------------------------------------------------------------------- + +logger = logging.getLogger(__name__) + +#------------------------------------------------------------------------------- + +class BaseManager(object): + """ + """ + + def __init__(self): + self.id_factory = self._get_id_factory() + + self.location_factory = System.getRegistry().bind( + "backends.factories.LocationFactory" + ) + + self.data_package_factory = System.getRegistry().bind( + "resources.coverages.data.DataPackageFactory" + ) + + def create(self, obj_id=None, request_id=None, **kwargs): + """ + Creates a new instance of the underlying type and returns an according + wrapper object. The optional parameter ``obj_id`` is used as + CoverageID/EOID and a UUID is generated automatically when omitted. + + If the ID was previously reserved by a specific ``request_id`` this + parameter must be set. + + The other parameters depend on the actual coverage manager type and will + be documented there. + + If the given ID is already in use, an :exc:`~.CoverageIdInUseError` + exception is raised. If the ID is already reserved by another + ``request_id``, an :exc:`~.CoverageIdReservedError` is raised. + These exceptions are sub-classes of :exc:`~.CoverageIdError`. + + :param obj_id: the ID (CoverageID or EOID) of the object to be created + :type obj_id: string + :param request_id: an optional request ID for the acquisition of the + CoverageID/EOID. + :type request_id: string + :param kwargs: the arguments + :rtype: a wrapper of the created object + """ + + id_mgr = CoverageIdManager() + + if obj_id is None: + # generate a new ID + for _ in range(3): + new_id = self._generate_id() + if id_mgr.available(new_id): + obj_id = new_id + break + else: + raise InternalError("Could not generate a unique identifier.") + + id_mgr.reserve(obj_id, request_id) + + try: + coverage = self._create(obj_id, **kwargs) + + finally: + id_mgr.release(obj_id, fail=True) + + return coverage + + def update(self, obj_id, link=None, unlink=None, set=None): + """ + Updates the coverage/dataset series identified by ``obj_id``. The + ``link`` and ``unlink`` dicts are used to add or remove references to + other objects, whereas the ``set`` dict values are used to set + attributes of the objects. This can be either a set of values (like + ``geo_metadata`` or ``eo_metadata``) or single values as defined in the + ``FIELDS`` dict of the according wrapper. + + For all supported attributes please refer to the actually used manager. + + :param obj_id: the ID (CoverageID or EOID) of the object to be updated + :type obj_id: string + :param link: objects to be linked with + :type link: dict or None + :param unlink: objects to be unlinked + :type unlink: dict or None + :param set: attributes to be set + :type set: dict or None + :rtype: a wrapper of the altered object + """ + # get the three update dicts + link_kwargs = link if link is not None else {} + unlink_kwargs = unlink if unlink is not None else {} + set_kwargs = set if set is not None else {} + + # prepare the update dicts + self._prepare_update_dicts(link_kwargs, unlink_kwargs, set_kwargs) + + # get the correct wrapper + wrapper = self.id_factory.get(obj_id=obj_id) + + if wrapper is None: + raise NoSuchCoverageException(obj_id) + + wrapper.updateModel(link_kwargs, unlink_kwargs, set_kwargs) + + # update the fields + keys = wrapper.getAttrNames() + for key, value in set_kwargs.items(): + if key in keys: + wrapper.setAttrValue(key, value) + + wrapper.saveModel() + + return wrapper + + def _prepare_update_dicts(self, link_kwargs, unlink_kwargs, set_kwargs): + # Override this function to prepare the three dictionaries + # prior to the update of the model + pass + + def delete(self, obj_id): + raise InternalError("Not implemented.") + + def synchronize(self, obj_id): + raise InternalError("Not implemented.") + + def _get_id_factory(self): + raise InternalError("Not implemented.") + + def _generate_id(self): + return "%s_%s" % (self._get_id_prefix(), uuid4().hex) + + def _get_id_prefix(self): + raise InternalError("Not implemented.") + + def _create_data_package(self, location, metadata_location=None): + if location.getType() == "local": + data_package_type = "local" + elif location.getType() == "ftp": + data_package_type = "remote" + elif location.getType() == "rasdaman": + data_package_type = "rasdaman" + + return self.data_package_factory.getOrCreate( + type=data_package_type, + location=location, + metadata_location=metadata_location + ) + +#------------------------------------------------------------------------------- + +class BaseManagerContainerMixIn(object): + def __init__(self): + super(BaseManagerContainerMixIn, self).__init__() + + self.rect_dataset_mgr = System.getRegistry().bind( + "resources.coverages.managers.RectifiedDatasetManager" + ) + + def _get_data_sources(self, params): + data_sources = params.get("data_sources", []) + + for dir_dict in params.get("data_dirs", []): + location = self.location_factory.create( + **dir_dict + ) + + search_pattern = dir_dict.get("search_pattern") + + data_sources.append( + self.data_source_factory.getOrCreate( + type="data_source", + location=location, + search_pattern=search_pattern + ) + ) + + return data_sources + + def _get_coverages(self, params): + coverages = params.get("coverages", []) + + coverage_factory = System.getRegistry().bind( + "resources.coverages.wrappers.EOCoverageFactory" + ) + for cid in params.get("coverage_ids", []): + coverage = coverage_factory.get(obj_id=cid) + if not coverage: + raise NoSuchCoverageException(cid) + coverages.append(coverage) + + return coverages + + def _create_contained(self, container, data_sources): + # TODO: make this more efficient by using updateModel() + + new_datasets = [] + for data_source in data_sources: + locations = data_source.detect() + + logger.info("Detected locations: %s"%[location.getPath() for location in locations]) + + for location in locations: + md_location = self._guess_metadata_location(location) + + data_package = self._create_data_package(location, md_location) + + coverage_factory = System.getRegistry().bind( + "resources.coverages.wrappers.EOCoverageFactory" + ) + + filter_exprs = [System.getRegistry().getFromFactory( + "resources.coverages.filters.CoverageExpressionFactory", { + "op_name": "referenced_by", + "operands": (location,) + } + )] + + existing_coverages = coverage_factory.find( + impl_ids=["resources.coverages.wrappers.RectifiedDatasetWrapper", + "resources.coverages.wrappers.ReferenceableDatasetWrapper"], + filter_exprs=filter_exprs + ) + + if len(existing_coverages) == 1: + coverage = existing_coverages[0] + logger.info("Add %s (%s) to %s."%( + coverage.getCoverageId(), coverage.getType(), + container.getType() + ) + ) + container.addCoverage(existing_coverages[0]) + new_datasets.append(existing_coverages[0]) + + else: + eo_metadata = data_package.readEOMetadata() + + coverage_id_mgr = CoverageIdManager() + + coverage_id = coverage_id_mgr.reserve( + eo_metadata.getEOID() + ) + + try: + range_type_name = self._get_contained_range_type_name( + container, location + ) + + if container.getType() == "eo.rect_stitched_mosaic": + default_srid = container.getSRID() + else: + default_srid = None + + logger.info("Creating new coverage with ID %s." % coverage_id) + # TODO: implement creation of ReferenceableDatasets, + # RectifiedStitchedMosaics for DatasetSeriesManager + new_dataset = self.rect_dataset_mgr.create( + coverage_id, + location=location, + md_location=md_location, + range_type_name=range_type_name, + data_source=data_source, + container=container, + default_srid=default_srid + ) + + logger.info("Done creating new coverage with ID %s." % coverage_id) + + new_datasets.append(new_dataset) + + finally: + coverage_id_mgr.release(coverage_id) + + + return new_datasets + + def _synchronize(self, container, data_sources, datasets): + # TODO: make this more efficient by using updateModel() + + new_datasets = self._create_contained(container, data_sources) + + # if new datasets have been created the container metadata + # have already been updated + do_md_update = not len(new_datasets) + + # delete all datasets, which do not have a file + for dataset in datasets: + if dataset.getType() == "eo.rect_stitched_mosaic": + # do not delete the tile index from a stitched mosaic + continue + + if not dataset.getData().getLocation().exists(): + logger.info( + "Location %s does not exist. Deleting dangling dataset with ID %s"%( + dataset.getData().getLocation().getPath(), + dataset.getCoverageId() + ) + ) + + self.rect_dataset_mgr.delete(dataset.getCoverageId()) + + # force updating the metadata + do_md_update = True + + elif dataset.getAttrValue("automatic"): + # remove all automatic coverages from a mosaic/dataset series + # which are not contained in a data source. + contained = False + for data_source in container.getDataSources(): + if data_source.contains(dataset): + contained = True + break + + if not contained: + container.removeCoverage(dataset) + do_md_update = True + + # if no update has been done do it now + if do_md_update: + container.updateModel({}, {}, {}) + + def _guess_metadata_location(self, location): + if location.getType() == "local": + return self.location_factory.create( + type="local", + path="%s.xml" % os.path.splitext(location.getPath())[0] + ) + elif location.getType() == "ftp": + return self.location_factory.create( + type="ftp", + path="%s.xml" % os.path.splitext(location.getPath())[0], + host=location.getHost(), + port=location.getPort(), + user=location.getUser(), + passwd=location.getPassword() + ) + else: + return None + + def _get_contained_range_type_name(self, container, location=None): + raise InternalError("Not implemented.") + + def _prepare_update_dicts(self, link_kwargs, unlink_kwargs, set_kwargs): + super(BaseManagerContainerMixIn, self)._prepare_update_dicts(link_kwargs, unlink_kwargs, set_kwargs) + link_kwargs["coverages"] = self._get_coverages(link_kwargs) + unlink_kwargs["coverages"] = self._get_coverages(unlink_kwargs) + +#------------------------------------------------------------------------------- diff -Nru eoxserver-0.4.0beta2/eoxserver/resources/coverages/managers/coverage.py eoxserver-0.3.2/eoxserver/resources/coverages/managers/coverage.py --- eoxserver-0.4.0beta2/eoxserver/resources/coverages/managers/coverage.py 1970-01-01 00:00:00.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/resources/coverages/managers/coverage.py 2013-12-10 14:57:00.000000000 +0000 @@ -0,0 +1,258 @@ +#------------------------------------------------------------------------------- +# $Id$ +# +# Project: EOxServer +# Authors: Stephan Krause +# Stephan Meissl +# Martin Paces +# +#------------------------------------------------------------------------------- +# Copyright (C) 2011 EOX IT Services GmbH +# +# 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 of this Software or works derived from this 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. +#------------------------------------------------------------------------------- + +import os + +from eoxserver.core.system import System +from eoxserver.core.exceptions import InternalError +from eoxserver.resources.coverages.exceptions import MetadataException + +#------------------------------------------------------------------------------- + +from eoxserver.resources.coverages.managers.base import BaseManager + +#------------------------------------------------------------------------------- + +class CoverageManager(BaseManager): + def __init__(self): + super(CoverageManager, self).__init__() + + self.coverage_factory = self.id_factory + + def _get_id_factory(self): + return System.getRegistry().bind( + "resources.coverages.wrappers.EOCoverageFactory" + ) + + def _get_range_type_name(self, params): + if "range_type_name" in params: + return params["range_type_name"] + else: + raise InternalError( + "Mandatory 'range_type_name' parameter missing." + ) + + def _get_layer_metadata(self, params): + # TODO: extend this dictionary if necessary + KEY_MAPPING = { + "abstract": "ows_abstract", + "title": "ows_title", + "keywords": "ows_keywords" + } + + layer_metadata = {} + + for key in KEY_MAPPING: + if key in params: + layer_metadata[KEY_MAPPING[key]] = params[key] + + return layer_metadata + + def _get_existing_coverage(self, data_package): + existing_coverages = data_package.getCoverages() + + if len(existing_coverages) == 0: + return None + else: + return existing_coverages[0] + +#------------------------------------------------------------------------------- + +class CoverageManagerDatasetMixIn(object): + def _get_location(self, params, fail=True): + location = None + + if "location" in params: + location = params["location"] + + if "local_path" in params: + if location: + raise InternalError( + "Parameters defining the data location must be unambiguous." + ) + else: + location = self.location_factory.create( + type="local", + path=params["local_path"] + ) + + if "remote_path" in params: + if location: + raise InternalError( + "Parameters defining the data location must be unambiguous." + ) + elif "ftp_host" not in params: + raise InternalError( + "If specifying 'remote_path' the 'ftp_host' argument is required." + ) + else: + location = self.location_factory.create( + type="ftp", + path=params["remote_path"], + host=params["ftp_host"], + port=params.get("ftp_port"), + user=params.get("ftp_user"), + passwd=params.get("ftp_passwd") + ) + + if "collection" in params: + if location: + raise InternalError( + "Parameters defining the data location must be unambiguous." + ) + elif "ras_host" not in params: + raise InternalError( + "If specifying 'collection' the 'ras_host' argument is required." + ) + else: + location = self.location_factory.create( + type="rasdaman", + collection=params["collection"], + oid=params.get("oid"), + host=params["ras_host"], + port=params.get("ras_port"), + user=params.get("ras_user"), + passwd=params.get("ras_passwd"), + db_name=params.get("ras_db") + ) + elif "oid" in params: + raise InternalError( + "You must specify a 'collection' to specify a valid rasdaman array location." + ) + + if not location and fail: + raise InternalError( + "You must specify a data location to create a coverage." + ) + + return location + + def _get_geo_metadata(self, data_package, params): + geo_metadata = params.get("geo_metadata") + + default_srid = params.get("default_srid") + + if not geo_metadata and data_package: + geo_metadata = data_package.readGeospatialMetadata(default_srid) + if geo_metadata is None: + raise InternalError("Geospatial Metadata could not be read from " + "the dataset.") + + return geo_metadata + +#------------------------------------------------------------------------------- + +class CoverageManagerEOMixIn(object): + def _get_metadata_location(self, location, params, force=True): + md_location = None + + if "md_location" in params: + md_location = params["md_location"] + + if "md_local_path" in params: + if md_location: + raise InternalError( + "Metadata location must be unambiguous." + ) + else: + md_location = self.location_factory.create( + type="local", + path=params["md_local_path"] + ) + + if "md_remote_path" in params: + if md_location: + raise InternalError( + "Metadata location must be unambiguous." + ) + else: + md_location = self.location_factory.create( + type="ftp", + path=params["md_remote_path"], + host=location.getHost(), + port=location.getPort(), + user=location.getUser(), + passwd=location.getPassword() + ) + + if not md_location and force: + md_path = "%s.xml" % os.path.splitext(location.getPath())[0] + + md_location = self.location_factory.create( + type=location.getType(), + path=md_path, + + ) + + return md_location + + def _get_eo_metadata(self, data_package, params): + try: + if data_package: + return data_package.readEOMetadata() + except MetadataException: + if "eo_metadata" in params: + return params["eo_metadata"] + else: + raise + + if "eo_metadata" in params: + return params["eo_metadata"] + + raise MetadataException("Creating EO Coverages requires EO Metadata.") + + def _get_containers(self, params): + containers = params.get("container_ids", []) + wrappers = [] + + for obj_id in containers: + wrapper = System.getRegistry().getFromFactory( + "resources.coverages.wrappers.DatasetSeriesFactory", + {"obj_id": obj_id} + ) + + if not wrapper: + wrapper = System.getRegistry().getFromFactory( + "resources.coverages.wrappers.EOCoverageFactory", { + "impl_id":"resources.coverages.wrappers.RectifiedStitchedMosaicWrapper", + "obj_id": obj_id + } + ) + + if not wrapper: + raise InternalError( + "Dataset Series or Rectified Stitched Mosaic with ID %s not found." % obj_id + ) + + wrappers.append(wrapper) + + return wrappers + +#------------------------------------------------------------------------------- diff -Nru eoxserver-0.4.0beta2/eoxserver/resources/coverages/managers/cov_plain.py eoxserver-0.3.2/eoxserver/resources/coverages/managers/cov_plain.py --- eoxserver-0.4.0beta2/eoxserver/resources/coverages/managers/cov_plain.py 1970-01-01 00:00:00.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/resources/coverages/managers/cov_plain.py 2013-12-10 14:57:00.000000000 +0000 @@ -0,0 +1,102 @@ +#------------------------------------------------------------------------------- +# $Id$ +# +# Project: EOxServer +# Authors: Stephan Krause +# Stephan Meissl +# Martin Paces +# +#------------------------------------------------------------------------------- +# Copyright (C) 2011 EOX IT Services GmbH +# +# 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 of this Software or works derived from this 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. +#------------------------------------------------------------------------------- + +from eoxserver.core.exceptions import InternalError +from eoxserver.resources.coverages.exceptions import ManagerError +from eoxserver.resources.coverages.models import PlainCoverageRecord + +#------------------------------------------------------------------------------- + +from eoxserver.resources.coverages.managers.coverage import CoverageManager +from eoxserver.resources.coverages.managers.coverage import CoverageManagerDatasetMixIn + +#------------------------------------------------------------------------------- + +class PlainCoverageManager(CoverageManager, CoverageManagerDatasetMixIn): + + # TODO: implement PlainCoverageWrapper, CoverageFactory + + def _create(self, coverage_id, **kwargs): + location = self._get_location(kwargs) + + data_package = self._create_data_package(location) + + existing_coverage = self._get_existing_coverage(data_package) + + if existing_coverage: + + if self._validate_type(existing_coverage): + return existing_coverage + else: + raise ManagerError( + "Another coverage with different type, but the same data exists already." + ) + + else: + + geo_metadata = self._get_geo_metadata(data_package, kwargs) + + range_type_name = self._get_range_type_name(location, kwargs) + + layer_metadata = self._get_layer_metadata(kwargs) + + return self._create_coverage( + coverage_id, + data_package, + geo_metadata, + range_type_name, + layer_metadata + ) + + def _validate_type(self, coverage): + return coverage.getType() == "plain" + + def _create_coverage(self, coverage_id, data_package, geo_metadata, range_type_name, layer_metadata): + pass + + + def get_all_ids(self): + """ + Get IDs of all registered rectified datasets. + + :rtype: list of CoverageIDs (strings) + """ + raise InternalError("Not implemented.") + + + def check_id(self, obj_id): + """ + Check whether the ``obj_id`` identifies an existing record. + + :rtype: boolean + """ + raise InternalError("Not implemented.") + +#------------------------------------------------------------------------------- diff -Nru eoxserver-0.4.0beta2/eoxserver/resources/coverages/managers/eo_ds.py eoxserver-0.3.2/eoxserver/resources/coverages/managers/eo_ds.py --- eoxserver-0.4.0beta2/eoxserver/resources/coverages/managers/eo_ds.py 1970-01-01 00:00:00.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/resources/coverages/managers/eo_ds.py 2013-12-10 14:57:00.000000000 +0000 @@ -0,0 +1,98 @@ +#------------------------------------------------------------------------------- +# $Id$ +# +# Project: EOxServer +# Authors: Stephan Krause +# Stephan Meissl +# Martin Paces +# +#------------------------------------------------------------------------------- +# Copyright (C) 2011 EOX IT Services GmbH +# +# 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 of this Software or works derived from this 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. +#------------------------------------------------------------------------------- + +from eoxserver.resources.coverages.exceptions import ManagerError + +#------------------------------------------------------------------------------- + +from eoxserver.resources.coverages.managers.coverage import CoverageManager +from eoxserver.resources.coverages.managers.coverage import CoverageManagerDatasetMixIn +from eoxserver.resources.coverages.managers.coverage import CoverageManagerEOMixIn + +#------------------------------------------------------------------------------- + +class EODatasetManager(CoverageManager, CoverageManagerDatasetMixIn, CoverageManagerEOMixIn): + def _create(self, coverage_id, **kwargs): + location = self._get_location(kwargs) + + metadata_location = self._get_metadata_location(location, kwargs) + + data_package = self._create_data_package( + location, metadata_location + ) + + existing_coverage = self._get_existing_coverage(data_package) + + if existing_coverage: + + if self._validate_type(existing_coverage): + return existing_coverage + else: + raise ManagerError( + "Another coverage with different type, but the same data exists already." + ) + + else: + + geo_metadata = self._get_geo_metadata(data_package, kwargs) + + range_type_name = self._get_range_type_name(kwargs) + + layer_metadata = self._get_layer_metadata(kwargs) + + eo_metadata = self._get_eo_metadata(data_package, kwargs) + + data_source = kwargs.get("data_source") + + containers = self._get_containers(kwargs) + + visible = kwargs.get("visible", True) + + return self._create_coverage( + coverage_id, + data_package, + data_source, + geo_metadata, + range_type_name, + layer_metadata, + eo_metadata, + container=kwargs.get("container"), + containers=containers, + visible=visible + ) + + + def _prepare_update_dicts(self, link_kwargs, unlink_kwargs, set_kwargs): + super(EODatasetManager, self)._prepare_update_dicts(link_kwargs, unlink_kwargs, set_kwargs) + + link_kwargs["containers"] = self._get_containers(link_kwargs) + unlink_kwargs["containers"] = self._get_containers(unlink_kwargs) + +#------------------------------------------------------------------------------- diff -Nru eoxserver-0.4.0beta2/eoxserver/resources/coverages/managers/eo_ds_rect.py eoxserver-0.3.2/eoxserver/resources/coverages/managers/eo_ds_rect.py --- eoxserver-0.4.0beta2/eoxserver/resources/coverages/managers/eo_ds_rect.py 1970-01-01 00:00:00.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/resources/coverages/managers/eo_ds_rect.py 2013-12-10 14:57:00.000000000 +0000 @@ -0,0 +1,173 @@ +#------------------------------------------------------------------------------- +# $Id$ +# +# Project: EOxServer +# Authors: Stephan Krause +# Stephan Meissl +# Martin Paces +# +#------------------------------------------------------------------------------- +# Copyright (C) 2011 EOX IT Services GmbH +# +# 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 of this Software or works derived from this 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. +#------------------------------------------------------------------------------- + +from eoxserver.resources.coverages.exceptions import NoSuchCoverageException + +#------------------------------------------------------------------------------- + +from eoxserver.resources.coverages.managers.eo_ds import EODatasetManager + +#------------------------------------------------------------------------------- + +class RectifiedDatasetManager(EODatasetManager): + """ + Coverage Manager for `RectifiedDatasets`. The following parameters can be + used for the :meth:`~BaseManager.create` and :meth:`~BaseManager.update` + methods. + + To define the data and metadata location, the ``location`` and + ``md_location`` parameters can be used, where the value has to implement the + :class:`~.LocationInterface`. Alternatively ``local_path`` and + ``md_local_path`` can be used to define local locations. For when the data + and metadata is located on an FTP server, use ``remote_path`` and + ``md_remote_path`` instead, which also requires the ``ftp_host`` parameter + (``ftp_port``, ``ftp_user`` and ``ftp_passwd`` are optional). When the data + is located in a rasdaman database use the ``collection`` and ``ras_host`` + parameters. ``oid``, ``ras_port``, ``ras_user``, ``ras_passwd``, and + ``ras_db`` can be used to further specify the location. + Currently, these parameters can only be used within the + :meth:`~BaseManager.create` method and not within the + :meth:`~BaseManager.update` method + + To specify geospatial metadata use the ``geo_metadata`` parameter, + which has to be an instance of :class:`~.GeospatialMetadata`. Optionally + ``default_srid`` can be used to declare a default SRID. When updating, + it has to be placed within the ``set`` dict. + + To specify earth observation related metadata use the ``eo_metadata`` + parameter which has to be of the type :class:`~.EOMetadata`. When updating, + it has to be placed within the ``set`` dict. + + The mandatory parameter ``range_type_name`` states which range type + this coverage is using. + + If the created dataset shall be inserted into a `DatasetSeries` or + `RectifiedStitchedMosaic` a wrapper instance can be passed with the + ``container`` parameter. Alternatively you can use the ``container_ids`` + parameter, passing a list of IDs referencing either `DatasetSeries` or + `RectifiedStitchedMosaics`. When used in the context of an + :meth:`~BaseManager.update`, both parameters can be placed within the + ``link`` or the ``unlink`` dict, to either add or remove a reference to the + container. + + Additional metadata can be added with the ``abstract``, ``title``, + and ``keywords`` parameters. + + For additional ``set`` parameters for the :meth:`~BaseManager.update` method + please refer to the :attr:`~.RectifiedDatasetWrapper.FIELDS` attribute of + the according wrapper. + """ + + _wrapper = "resources.coverages.wrappers.RectifiedDatasetWrapper" + _type0 = "rect_dataset" + _type = "eo.%s"%_type0 + + REGISTRY_CONF = { + "name": "Rectified Dataset Manager", + "impl_id": "resources.coverages.managers.RectifiedDatasetManager", + "registry_values": { + "resources.coverages.interfaces.res_type": _type + } + } + + def delete(self, obj_id): + """ + Remove a rectified dataset identified by the ``obj_id`` parameter. + + :param obj_id: the ID (CoverageID or EOID) of the object to be deleted + :rtype: no output returned + """ + return self._get_wrapper( obj_id ).deleteModel() + + def _validate_type(self, coverage): + return coverage.getType() == self._type + + def _create_coverage(self, coverage_id, data_package, data_source, geo_metadata, range_type_name, layer_metadata, eo_metadata, container, containers, visible): + return self.coverage_factory.create( + impl_id=self._wrapper, + params={ + "coverage_id": coverage_id, + "data_package": data_package, + "data_source": data_source, + "geo_metadata": geo_metadata, + "range_type_name": range_type_name, + "layer_metadata": layer_metadata, + "eo_metadata": eo_metadata, + "container": container, + "containers": containers, + "visible": visible + } + ) + + def _get_id_prefix(self): + return self._type0 + + #-------------------------------------------------------------------------- + + def _get_wrapper( self, obj_id ) : + wrapper = self.coverage_factory.get( impl_id=self._wrapper, obj_id=obj_id ) + if not wrapper: raise NoSuchCoverageException(obj_id) + return wrapper + + + def get_all_ids(self): + """ + Get CoverageIDs of all registered rectified datasets. + + :rtype: list of CoverageIDs (strings) + """ + return [ obj.getCoverageId() for obj in self.coverage_factory.find( + impl_ids=[self._wrapper] ) ] + + + def check_id(self, obj_id): + """ + Check whether the ``obj_id`` identifies an existing rectified dataset. + + :rtype: boolean + """ + try: + self._get_wrapper( obj_id ) + return True + except NoSuchCoverageException : + return False + + + def is_automatic(self, obj_id): + """ + For the dataset identified by the ``obj_id`` parameter return value of + the ``automatic`` boolean flag. Returns + + :param obj_id: the ID (CoverageID or EOID) of the object to be deleted + :rtype: boolean value, ``True`` if the dataset is automatic + """ + return self._get_wrapper( obj_id ).isAutomatic() + +#------------------------------------------------------------------------------- diff -Nru eoxserver-0.4.0beta2/eoxserver/resources/coverages/managers/eo_ds_ref.py eoxserver-0.3.2/eoxserver/resources/coverages/managers/eo_ds_ref.py --- eoxserver-0.4.0beta2/eoxserver/resources/coverages/managers/eo_ds_ref.py 1970-01-01 00:00:00.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/resources/coverages/managers/eo_ds_ref.py 2013-12-10 14:57:00.000000000 +0000 @@ -0,0 +1,135 @@ +#------------------------------------------------------------------------------- +# $Id$ +# +# Project: EOxServer +# Authors: Stephan Krause +# Stephan Meissl +# Martin Paces +# +#------------------------------------------------------------------------------- +# Copyright (C) 2011 EOX IT Services GmbH +# +# 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 of this Software or works derived from this 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. +#------------------------------------------------------------------------------- + +from eoxserver.resources.coverages.exceptions import NoSuchCoverageException + +#------------------------------------------------------------------------------- + +from eoxserver.resources.coverages.managers.eo_ds import EODatasetManager + +#------------------------------------------------------------------------------- + +class ReferenceableDatasetManager(EODatasetManager): + # TODO documentation + """ + + Coverage Manager for `ReferenceableDatasets`. + + Sorry, but no one has bothered to document this class yet. + + """ + + _wrapper = "resources.coverages.wrappers.ReferenceableDatasetWrapper" + _type0 = "ref_dataset" + _type = "eo.%s"%_type0 + + REGISTRY_CONF = { + "name": "Referenceable Dataset Manager", + "impl_id": "resources.coverages.managers.ReferenceableDatasetManager", + "registry_values": { + "resources.coverages.interfaces.res_type": _type + } + } + + def delete(self, obj_id): + """ + Remove a referenceable dataset identified by the ``obj_id`` parameter. + + :param obj_id: the ID (CoverageID or EOID) of the object to be deleted + :rtype: no output returned + """ + return self._get_wrapper( obj_id ).deleteModel() + + def _validate_type(self, coverage): + return coverage.getType() == self._type + + def _create_coverage(self, coverage_id, data_package, data_source, geo_metadata, range_type_name, layer_metadata, eo_metadata, container, containers, visible): + return self.coverage_factory.create( + impl_id=self._wrapper, + params={ + "coverage_id": coverage_id, + "data_package": data_package, + "data_source": data_source, + "geo_metadata": geo_metadata, + "range_type_name": range_type_name, + "layer_metadata": layer_metadata, + "eo_metadata": eo_metadata, + "container": container, + "containers": containers, + "visible": visible + } + ) + + def _get_id_prefix(self): + return self._type0 + + #--------------------------------------------------------------------------- + + def _get_wrapper( self, obj_id ) : + wrapper = self.coverage_factory.get( impl_id=self._wrapper, obj_id=obj_id ) + if not wrapper: raise NoSuchCoverageException(obj_id) + return wrapper + + + def get_all_ids(self): + """ + Get CoverageIDs of all registered referenceable datasets. + + :rtype: list of CoverageIDs (strings) + """ + return [ obj.getCoverageId() for obj in self.coverage_factory.find( + impl_ids=[self._wrapper] ) ] + + + def check_id(self, obj_id): + """ + Check whether the ``obj_id`` identifies an existing referenceable + dataset. + + :rtype: boolean + """ + try: + self._get_wrapper( obj_id ) + return True + except NoSuchCoverageException : + return False + + + def is_automatic(self, obj_id): + """ + For the dataset identified by the ``obj_id`` parameter return value of + the ``automatic`` boolean flag. Returns + + :param obj_id: the ID (CoverageID or EOID) of the object to be deleted + :rtype: boolean value, ``True`` if the dataset is automatic + """ + return self._get_wrapper( obj_id ).isAutomatic() + +#------------------------------------------------------------------------------- diff -Nru eoxserver-0.4.0beta2/eoxserver/resources/coverages/managers/eo_ds_series.py eoxserver-0.3.2/eoxserver/resources/coverages/managers/eo_ds_series.py --- eoxserver-0.4.0beta2/eoxserver/resources/coverages/managers/eo_ds_series.py 1970-01-01 00:00:00.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/resources/coverages/managers/eo_ds_series.py 2013-12-10 14:57:00.000000000 +0000 @@ -0,0 +1,273 @@ +#------------------------------------------------------------------------------- +# $Id$ +# +# Project: EOxServer +# Authors: Stephan Krause +# Stephan Meissl +# Martin Paces +# +#------------------------------------------------------------------------------- +# Copyright (C) 2011 EOX IT Services GmbH +# +# 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 of this Software or works derived from this 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. +#------------------------------------------------------------------------------- + +import os.path +from ConfigParser import RawConfigParser +from eoxserver.core.system import System +from eoxserver.core.exceptions import InternalError +from eoxserver.resources.coverages.exceptions import NoSuchDatasetSeriesException + +#------------------------------------------------------------------------------- + +from eoxserver.resources.coverages.managers.base import BaseManager +from eoxserver.resources.coverages.managers.base import BaseManagerContainerMixIn + +#------------------------------------------------------------------------------- + +class DatasetSeriesManager(BaseManagerContainerMixIn, BaseManager): + """ + This manager handles interactions with ``DatasetSeries``. + + + If the ``obj_id`` argument is omitted a new object ID shall be generated + using the same mechanism as :meth:`acquireID`. If the provided object ID + is invalid or already in use, appropriate exceptions shall be raised. + + To add data sources to the ``DatasetSeries`` at the time it is created + the ``data_sources`` and ``data_dirs`` parameters can be used. The + ``data_sources`` parameter shall be a list of objects implementing the + :class:``~.DataSourceInterface``. Alternatively the ``data_dirs`` + parameter shall be a list of dictionaries consisting of the following + arguments: + + * ``search_pattern``: a regular expression to specify what files in the + directory are considered as data files. + + * ``path``: for local or FTP data sources, this parameter shall be a + path to a valid directory, containing the data files. + + * ``type``: defines the type of the location describing the data source. + This can either be `local` or `remote`. + + These parameters can also be used in the context of an + :meth:`~BaseManager.update` within the `link` or `unlink` dict. + + To specify earth observation related metadata use the ``eo_metadata`` + parameter which has to be of the type :class:`~.EOMetadata`. When updating, + it has to be placed within the ``set`` dict. + + For additional ``set`` parameters for the :meth:`~BaseManager.update` method + please refer to the :attr:`~.DatasetSeriesWrapper.FIELDS` attribute of the + according wrapper. + + .. method:: synchronize(obj_id) + + This method synchronizes a :class:`~.DatasetSeriesRecord` identified by + the ``obj_id`` with the file system. It does three tasks: + + * It scans through all directories specified by its data sources and + checks if data files exist which do not yet have an according record. + For each, a :class:`~.RectifiedDatasetRecord` is created and linked + with the :class:`~.DatasetSeriesRecord`. Also all existing, + but previously not contained datasets are linked to the `Dataset + Series`. + + * All contained instances of :class:`~.RectifiedDatasetRecord` are + checked if their data file still exists. If not, the according record + is unlinked from the `Dataset Series` and deleted. + + * All instances of :class:`~.RectifiedDatasetRecord` associated with the + :class:`~.DatasetSeriesRecord` which are not referenced by a + data source anymore are unlinked from the `Dataset Series`. + + .. method:: delete(obj_id) + + This deletes a `RectifiedDataset` record specified by its + ``obj_id``. If no coverage with this ID can be found, an + :exc:`~.NoSuchCoverage` exception will be raised. + """ + + _type0 = "dataset_series" + _type = "eo.%s"%_type0 + + REGISTRY_CONF = { + "name": "Dataset Series Manager", + "impl_id": "resources.coverages.managers.DatasetSeriesManager", + "registry_values": { + "resources.coverages.interfaces.res_type": _type + } + } + + def __init__(self): + super(DatasetSeriesManager, self).__init__() + + self.dataset_series_factory = self.id_factory + + self.data_source_factory = System.getRegistry().bind( + "resources.coverages.data.DataSourceFactory" + ) + + def synchronize(self, obj_id): + """ + Synchronise a dataset series identified by the ``obj_id`` + parameter. + + :param obj_id: the ID (EOID) of the object to be synchronised + :rtype: no output returned + """ + """ + This method synchronizes a :class:`~.DatasetSeriesRecord` + identified by the ``obj_id`` with the file system. It does three tasks: + + * It scans through all directories specified by its data sources and + checks if data files exist which do not yet have an according record. + For each, a :class:`~.RectifiedDatasetRecord` is created and linked + with the :class:`~.DatasetSeriesRecord`. Also all existing, + but previously not contained datasets are linked to the `Dataset + Series`. + + * All contained instances of :class:`~.RectifiedDatasetRecord` are + checked if their data file still exists. If not, the according record + is unlinked from the `Dataset Series` and deleted. + + * All instances of :class:`~.RectifiedDatasetRecord` associated with the + :class:`~.DatasetSeriesRecord` which are not referenced by a + data source anymore are unlinked from the `Dataset Series`. + + :param obj_id: the ID (CoverageID or EOID) of the object to be synchronised + :rtype: no output returned + """ + + container = self._get_wrapper( obj_id ) + + self._synchronize(container, container.getDataSources(), container.getEOCoverages()) + + def _prepare_update_dicts(self, link_kwargs, unlink_kwargs, set_kwargs): + super(DatasetSeriesManager, self)._prepare_update_dicts(link_kwargs, unlink_kwargs, set_kwargs) + link_kwargs["data_sources"] = self._get_data_sources(link_kwargs) + + def _get_id_factory(self): + return System.getRegistry().bind( + "resources.coverages.wrappers.DatasetSeriesFactory" + ) + + def _get_id_prefix(self): + return self._type0 + + def _create(self, eo_id, **kwargs): + #layer_metadata = self._get_layer_metadata(kwargs) + + eo_metadata = self._get_eo_metadata(kwargs) + + data_sources = self._get_data_sources(kwargs) + + coverages = self._get_coverages(kwargs) + + dataset_series = self.dataset_series_factory.create( + impl_id="resources.coverages.wrappers.DatasetSeriesWrapper", + params={ + "eo_id": eo_id, + #"layer_metadata": layer_metadata, + "eo_metadata": eo_metadata, + "data_sources": data_sources, + "coverages": coverages + } + ) + + self._create_contained(dataset_series, data_sources) + + return dataset_series + + def _get_eo_metadata(self, kwargs): + if "eo_metadata" in kwargs: + return kwargs["eo_metadata"] + else: + raise InternalError( + "Mandatory 'eo_metadata' keyword argument missing." + ) + + def _get_contained_range_type_name(self, container, location=None): + range_type_name = None + + if location.getType() == "local": + conf_path = "%s.conf" % os.path.splitext(location.getPath())[0] + + if os.path.exists(conf_path): + parser = RawConfigParser() + parser.read(conf_path) + if parser.has_option("range_type", "range_type_name"): + range_type_name = parser.get("range_type", "range_type_name") + else: + def_path = os.path.join( + os.path.dirname(location.getPath()), + "__default__.conf" + ) + + if os.path.exists(def_path): + parser = RawConfigParser() + parser.read(def_path) + if parser.has_option("range_type", "range_type_name"): + range_type_name = parser.get("range_type", "range_type_name") + + if range_type_name: + return range_type_name + else: + return "RGB" + + #--------------------------------------------------------------------------- + + def delete(self, obj_id): + """ + Remove a dataset series identified by the ``obj_id`` parameter. + + :param obj_id: the EOID of the object to be deleted + :rtype: no output returned + """ + return self._get_wrapper( obj_id ).deleteModel() + + + def _get_wrapper( self, obj_id ) : + wrapper = self.dataset_series_factory.get( obj_id=obj_id ) + if not wrapper: raise NoSuchDatasetSeriesException(obj_id) + return wrapper + + + def get_all_ids(self): + """ + Get EOIDs of all registered dataset series. + + :rtype: list of EOIDs (strings) + """ + return [ obj.getEOID() for obj in self.dataset_series_factory.find() ] + + + def check_id(self, obj_id): + """ + Check whether the ``obj_id`` identifies an existing dataset series. + + :rtype: boolean + """ + try: + self._get_wrapper( obj_id ) + return True + except NoSuchDatasetSeriesException : + return False + +#------------------------------------------------------------------------------- diff -Nru eoxserver-0.4.0beta2/eoxserver/resources/coverages/managers/eo_sm_rect.py eoxserver-0.3.2/eoxserver/resources/coverages/managers/eo_sm_rect.py --- eoxserver-0.4.0beta2/eoxserver/resources/coverages/managers/eo_sm_rect.py 1970-01-01 00:00:00.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/resources/coverages/managers/eo_sm_rect.py 2013-12-10 14:57:00.000000000 +0000 @@ -0,0 +1,329 @@ +#------------------------------------------------------------------------------- +# $Id$ +# +# Project: EOxServer +# Authors: Stephan Krause +# Stephan Meissl +# Martin Paces +# +#------------------------------------------------------------------------------- +# Copyright (C) 2011 EOX IT Services GmbH +# +# 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 of this Software or works derived from this 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. +#------------------------------------------------------------------------------- + +from eoxserver.core.system import System +from eoxserver.core.exceptions import InternalError +from eoxserver.resources.coverages.exceptions import NoSuchCoverageException + +from eoxserver.processing.mosaic import make_mosaic + +#------------------------------------------------------------------------------- + +from eoxserver.resources.coverages.managers.base import BaseManagerContainerMixIn +from eoxserver.resources.coverages.managers.coverage import CoverageManager + +#------------------------------------------------------------------------------- + +class RectifiedStitchedMosaicManager(BaseManagerContainerMixIn, CoverageManager): + """ + Coverage Manager for `RectifiedStitchedMosaics` + + To add data sources to the ``RectifiedStitchedMosaic`` at the time it is + created the ``data_sources`` and ``data_dirs`` parameters can be used. + The ``data_sources`` parameter shall be a list of objects implementing + the :class:`~.DataSourceInterface`. Alternatively the ``data_dirs`` + parameter shall be a list of dictionaries consisting of the following + arguments: + + * ``search_pattern``: a regular expression to specify what files in the + directory are considered as data files. + + * ``path``: for local or FTP data sources, this parameter shall be a + path to a valid directory, containing the data files. + + * ``type``: defines the type of the location describing the data source. + This can either be `local` or `remote`. + + These parameters can also be used in the context of an + :meth:`~BaseManager.update` within the `link` or `unlink` dict. + + To specify geospatial metadata use the ``geo_metadata`` parameter, + which has to be an instance of :class:`~.GeospatialMetadata`. Optionally + ``default_srid`` can be used to declare a default SRID. When updating, + it has to be placed within the ``set`` dict. + + To specify earth observation related metadata use the ``eo_metadata`` + parameter which has to be of the type :class:`~.EOMetadata`. When updating, + it has to be placed within the ``set`` dict. + + The mandatory parameter ``range_type_name`` states which range type + this coverage is using. + + If the created dataset shall be inserted into a `DatasetSeries` or + `RectifiedStitchedMosaic` a wrapper instance can be passed with the + ``container`` parameter. Alternatively you can use the ``container_ids`` + parameter, passing a list of IDs referencing either `DatasetSeries` + or `RectifiedStitchedMosaics`. These parameters can also be used in the + context of an :meth:`~BaseManager.update` within the `link` or `unlink` + dict. + + Additional metadata can be added with the ``abstract``, ``title``, + and ``keywords`` parameters. + + For additional ``set`` parameters for the :meth:`~BaseManager.update` method + please refer to the :attr:`~.RectifiedStitchedMosaicWrapper.FIELDS` + attribute of the according wrapper. + + .. method:: synchronize(obj_id) + + This method synchronizes a :class:`~.RectifiedStitchedMosaicRecord` + identified by the ``obj_id`` with the file system. It does three tasks: + + * It scans through all directories specified by its data sources and + checks if data files exist which do not yet have an according record. + For each, a :class:`~.RectifiedDatasetRecord` is created and linked + with the :class:`~.RectifiedStitchedMosaicRecord`. Also all existing, + but previously not contained datasets are linked to the `Rectified + Stitched Mosaic`. + + * All contained instances of :class:`~.RectifiedDatasetRecord` are + checked if their data file still exists. If not, the according record + is unlinked from the `Rectified Stitched Mosaic` and deleted. + + * All instances of :class:`~.RectifiedDatasetRecord` associated with the + :class:`~.RectifiedStitchedMosaicRecord` which are not referenced by a + data source anymore are unlinked from the `Rectified Stitched Mosaic`. + + .. method:: delete(obj_id) + + This deletes a `RectifiedDataset` record specified by its + ``obj_id``. If no coverage with this ID can be found, an + :exc:`~.NoSuchCoverage` exception will be raised. + + """ + + _wrapper = "resources.coverages.wrappers.RectifiedStitchedMosaicWrapper" + _type0 = "rect_stitched_mosaic" + _type = "eo.%s"%_type0 + + REGISTRY_CONF = { + "name": "Rectified Stitched Mosaic Manager", + "impl_id": "resources.coverages.managers.RectifiedStitchedMosaicManager", + "registry_values": { + "resources.coverages.interfaces.res_type": _type + } + } + + def delete(self, obj_id): + """ + Remove a referenceable dataset identified by the ``obj_id`` parameter. + + :param obj_id: the ID (CoverageID or EOID) of the object to be deleted + :rtype: no output returned + """ + return self._get_wrapper( obj_id ).deleteModel() + + def synchronize(self, obj_id): + """ + This method synchronizes a :class:`~.RectifiedStitchedMosaicRecord` + identified by the ``obj_id`` with the file system. It does three tasks: + + * It scans through all directories specified by its data sources and + checks if data files exist which do not yet have an according record. + For each, a :class:`~.RectifiedDatasetRecord` is created and linked + with the :class:`~.RectifiedStitchedMosaicRecord`. Also all existing, + but previously not contained datasets are linked to the `Rectified + Stitched Mosaic`. + + * All contained instances of :class:`~.RectifiedDatasetRecord` are + checked if their data file still exists. If not, the according record + is unlinked from the `Rectified Stitched Mosaic` and deleted. + + * All instances of :class:`~.RectifiedDatasetRecord` associated with the + :class:`~.RectifiedStitchedMosaicRecord` which are not referenced by a + data source anymore are unlinked from the `Rectified Stitched Mosaic`. + + :param obj_id: the ID (CoverageID or EOID) of the object to be synchronised + :rtype: no output returned + """ + + container = self._get_wrapper( obj_id ) + + self._synchronize(container, container.getDataSources(), container.getDatasets()) + self._make_mosaic(container) + + def __init__(self): + super(RectifiedStitchedMosaicManager, self).__init__() + + self.data_source_factory = System.getRegistry().bind( + "resources.coverages.data.DataSourceFactory" + ) + + self.tile_index_factory = System.getRegistry().bind( + "resources.coverages.data.TileIndexFactory" + ) + + def _create(self, coverage_id, **kwargs): + geo_metadata = self._get_geo_metadata(kwargs) + + range_type_name = self._get_range_type_name(kwargs) + + layer_metadata = self._get_layer_metadata(kwargs) + + eo_metadata = self._get_eo_metadata(kwargs) + + tile_index = self._get_tile_index(kwargs) + + data_sources = self._get_data_sources(kwargs) + + containers = self._get_containers(kwargs) + + coverages = self._get_coverages(kwargs) + + coverage = self._create_coverage( + coverage_id, + geo_metadata, + range_type_name, + layer_metadata, + eo_metadata, + tile_index, + data_sources, + kwargs.get("container"), + containers, + coverages + ) + + self._create_contained(coverage, data_sources) + + self._make_mosaic(coverage) + + return coverage + + def _create_coverage(self, coverage_id, geo_metadata, range_type_name, layer_metadata, eo_metadata, tile_index, data_sources, container=None, containers=None, coverages=None): + return self.coverage_factory.create( + impl_id=self._wrapper, + params={ + "coverage_id": coverage_id, + "geo_metadata": geo_metadata, + "range_type_name": range_type_name, + "layer_metadata": layer_metadata, + "eo_metadata": eo_metadata, + "tile_index": tile_index, + "data_sources": data_sources, + "container": container, + "containers": containers, + "coverages": coverages + } + ) + + def _get_geo_metadata(self, params): + if "geo_metadata" in params: + return params["geo_metadata"] + else: + raise InternalError( + "Mandatory 'geo_metadata' keyword argument missing." + ) + + def _get_eo_metadata(self, params): + if "eo_metadata" in params: + return params["eo_metadata"] + else: + raise InternalError( + "Mandatory 'eo_metadata' keyword argument missing." + ) + + def _get_tile_index(self, params): + if "storage_dir" in params: + return self.tile_index_factory.create( + type="index", storage_dir = params["storage_dir"] + ) + else: + raise InternalError( + "Mandatory 'storage_dir' keyword argument missing." + ) + + def _get_id_prefix(self): + return self._type0 + + def _get_contained_range_type_name(self, container, location=None): + return container.getRangeType().name + + def _get_containers(self, params): + containers = params.get("container_ids", []) + wrappers = [] + + for obj_id in containers: + wrapper = System.getRegistry().getFromFactory( + "resources.coverages.wrappers.DatasetSeriesFactory", + {"obj_id": obj_id} + ) + + if not wrapper: + raise InternalError( + "Dataset Series or ID %s not found." % obj_id + ) + + wrappers.append(wrapper) + + return wrappers + + def _make_mosaic(self, coverage): + make_mosaic(coverage) + + def _prepare_update_dicts(self, link_kwargs, unlink_kwargs, set_kwargs): + super(RectifiedStitchedMosaicManager, self)._prepare_update_dicts(link_kwargs, unlink_kwargs, set_kwargs) + link_kwargs["data_sources"] = self._get_data_sources(link_kwargs) + + #--------------------------------------------------------------------------- + + def _validate_type(self, coverage): + return coverage.getType() == self._type + + + def _get_wrapper( self, obj_id ) : + wrapper = self.coverage_factory.get( impl_id=self._wrapper, obj_id=obj_id ) + if not wrapper: raise NoSuchCoverageException(obj_id) + return wrapper + + + def get_all_ids(self): + """ + Get CoverageIDs of all registered rectified stitched mosaics. + + :rtype: list of CoverageIDs (strings) + """ + return [ obj.getCoverageId() for obj in self.coverage_factory.find( + impl_ids=[self._wrapper] ) ] + + + def check_id(self, obj_id): + """ + Check whether the ``obj_id`` identifies an existing rectified stitched + mosaic. + + :rtype: boolean + """ + try: + self._get_wrapper( obj_id ) + return True + except NoSuchCoverageException : + return False + +#------------------------------------------------------------------------------- diff -Nru eoxserver-0.4.0beta2/eoxserver/resources/coverages/managers/id_manager.py eoxserver-0.3.2/eoxserver/resources/coverages/managers/id_manager.py --- eoxserver-0.4.0beta2/eoxserver/resources/coverages/managers/id_manager.py 1970-01-01 00:00:00.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/resources/coverages/managers/id_manager.py 2013-12-10 14:57:00.000000000 +0000 @@ -0,0 +1,316 @@ +#------------------------------------------------------------------------------- +# $Id$ +# +# Project: EOxServer +# Authors: Stephan Krause +# Stephan Meissl +# Martin Paces +# +#------------------------------------------------------------------------------- +# Copyright (C) 2011 EOX IT Services GmbH +# +# 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 of this Software or works derived from this 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. +#------------------------------------------------------------------------------- + +from django.db.models import Q + +from eoxserver.core.system import System +from eoxserver.resources.coverages.exceptions import ( + ManagerError, CoverageIdReservedError, + CoverageIdInUseError, CoverageIdReleaseError ) +from eoxserver.resources.coverages.models import ( + PlainCoverageRecord, RectifiedDatasetRecord, + ReferenceableDatasetRecord, RectifiedStitchedMosaicRecord, + ReservedCoverageIdRecord, CoverageRecord, DatasetSeriesRecord, +) + +try: + from django.utils import timezone +except ImportError: + from datetime import datetime as timezone +from datetime import timedelta + +#------------------------------------------------------------------------------- + +#COVERAGE_TYPES = { +# "PlainCoverage" : PlainCoverageRecord , +# "RectifiedDataset" : RectifiedDatasetRecord , +# "ReferenceableDataset" : ReferenceableDatasetRecord , +# "RectifiedStitchedMosaic" : RectifiedStitchedMosaicRecord , +#} + +COVERAGE_EO_TYPES = { + "RectifiedDataset" : RectifiedDatasetRecord , + "ReferenceableDataset" : ReferenceableDatasetRecord , + "RectifiedStitchedMosaic" : RectifiedStitchedMosaicRecord , +} + +#EO_TYPES = { +# "RectifiedDataset" : RectifiedDatasetRecord , +# "ReferenceableDataset" : ReferenceableDatasetRecord , +# "RectifiedStitchedMosaic" : RectifiedStitchedMosaicRecord , +# "DatasetSeries" : DatasetSeriesRecord , +#} + +#------------------------------------------------------------------------------- + +class CoverageIdManager(object): + """ + Manager for Coverage IDs. The purpose of this manager class is to help: + + * During registration of a new EO-entities/coverage when the uniqueness + of the ID must be guarantied. Further, the manager provies means for + time limitted reservation (booking) of IDs preventing any parallel + process to ``steal`` the ID while the new coverage is being + registered. + + * During inspection of an existing ID. The manager determines wehther + the inspected ID belongs to an existing EO-entity/coverage or is + reserved for a new one. Further, it helps to determine type of the + EO-entity/coverage associated to it so that a proper specific manager + class can be selected for further action. + + .. note:: + + EOIDs of DatasetSeries are now included. The name ``CoverageIdManager`` + is therefore misleading as the EO-IDs are involved in the checks. + + """ + + def reserve(self, coverage_id, request_id=None, until=None): + """ + Tries to reserve a ``coverage_id`` until a given datetime. If ``until`` + is omitted, the configuration value + ``resources.coverages.coverage_id.reservation_time`` is used. + + If the ID is already reserved and the ``resource_id`` is not equal to + the reserved ``resource_id``, a :class:`~.CoverageIdReservedError` is + raised. If the ID is already taken by an existing coverage a + :class:`~.CoverageIdInUseError` is raised. + These exceptions are sub-classes of :exc:`~.CoverageIdError`. + """ + + obj, _ = ReservedCoverageIdRecord.objects.get_or_create( + coverage_id=coverage_id, + defaults={ + "until": timezone.now() + } + ) + + if not until: + values = System.getConfig().getConfigValue( + "resources.coverages.coverage_id", "reservation_time" + ).split(":") + + for _ in xrange(len(values[:4]) - 4): + values.insert(0, 0) + + dt = timedelta(days=int(values[0]), hours=int(values[1]), + minutes=int(values[2]), seconds=int(values[3])) + until = timezone.now() + dt + + if timezone.now() < obj.until: + if not (obj.request_id == request_id and obj.request_id is not None): + raise CoverageIdReservedError( + "Coverage ID '%s' is reserved until %s" % (coverage_id, obj.until) + ) + elif CoverageRecord.objects.filter(coverage_id=coverage_id).count() > 0: + raise CoverageIdInUseError("Coverage ID '%s' is already in use." + % coverage_id + ) + + obj.request_id = request_id + obj.until = until + obj.save() + + + def release(self, coverage_id, fail=False): + """ + Releases a previously reserved ``coverage_id``. + + If ``fail`` is set to ``True``, an exception is raised when the ID was + not previously reserved. + """ + + try: + obj = ReservedCoverageIdRecord.objects.get(coverage_id=coverage_id) + obj.delete() + + except ReservedCoverageIdRecord.DoesNotExist: + if fail: + raise CoverageIdReleaseError( + "Coverage ID '%s' was not reserved" % coverage_id + ) + + + def check( self , coverage_id ): + """ + + .. warning:: + + This method has been deprecated. Use :meth:`isUsed` instead. + + """ + return self.isUsed( coverage_id ) + + + def isUsed( self , coverage_id ) : + """ + Returns a boolean value, indicating if the ``coverage_id`` is identifier + of an existing entity (coverage, eo-dataset, rs-mosaic or ds-series). + + .. note:: + + The check also involves EO-IDs! + + """ + + # TODO unify the coverage and eo IDs!!! + count = CoverageRecord.objects.filter(coverage_id=coverage_id).count() + count += RectifiedDatasetRecord.objects.filter(eo_id=coverage_id).count() + count += ReferenceableDatasetRecord.objects.filter(eo_id=coverage_id).count() + count += RectifiedStitchedMosaicRecord.objects.filter(eo_id=coverage_id).count() + count += DatasetSeriesRecord.objects.filter(eo_id=coverage_id).count() + + return ( count > 0 ) + + + def isReserved( self , coverage_id ) : + """ + Returns a boolean value, indicating if the ``coverage_id`` is reserved + for an entity being currently created. + """ + + return ( ReservedCoverageIdRecord.objects.filter(coverage_id=\ + coverage_id,until__gte=timezone.now()).count() > 0 ) + + + def getCoverageType( self , coverage_id ): + """ + + .. warning:: + + This method has been deprecated. Use :meth:`getType` instead. + + """ + return self.getType( coverage_id ) + + + def getType( self , coverage_id ): + """ + Returns string, type name of the entity identified by the given ID. + In case there is no entity corresponding to the given ID None is + returned. + + Possible return values are: + None, 'PlainCoverage', 'RectifiedDataset', 'ReferenceableDataset', + 'RectifiedStitchedMosaic', 'DatasetSeries', and 'Reserved' + """ + + #TODO: There should be a fast and easy method how to get the coverage/series type! + # If possible in a SINGLE DB query. + + for ct in COVERAGE_EO_TYPES : + if COVERAGE_EO_TYPES[ct].objects.filter( + Q(coverage_id=coverage_id) | Q(coverage_id=coverage_id) + ).count() > 0 : return ct + + if DatasetSeriesRecord.objects.filter(eo_id=coverage_id).count() > 0 : + return "DatasetSeries" + + if PlainCoverageRecord.objects.filter(coverage_id=coverage_id).count() > 0 : + return "PlainCoverage" + + if self.isReserved( coverage_id ) : + return "Reserved" + + return None + + + def available(self, coverage_id): # TODO available for a specific request_id + """ + + .. warning:: + + This method has been deprecated. Use :meth:`isAvailable` instead. + + """ + return self.isAvailable(coverage_id) + + + def isAvailable(self, coverage_id): # TODO available for a specific request_id + """ + Returns a boolean value, indicating if the ``coverage_id`` is identifier + of an existing entity (coverage, eo-dataset, rs-mosaic or ds-series) + or it is a reserved ID. + + .. note:: + + The check also involves EO-IDs! + + """ + return not ( self.isReserved(coverage_id) or self.isUsed(coverage_id) ) + + + def getRequestId(self, coverage_id): + """ + Returns the request ID associated with a + :class:`~.ReservedCoverageIdRecord` or `None` if the no record with that + ID is available. + """ + + try: + obj = ReservedCoverageIdRecord.objects.get(coverage_id=coverage_id) + return obj.request_id + except ReservedCoverageIdRecord.DoesNotExist: + return None + + + def getCoverageIds(self, request_id): + """ + + .. warning:: + + This method has been deprecated. Use :meth:`getReservedIds` + instead. + + """ + return self.getReservedIds(request_id) + + + def getReservedIds(self, request_id): + """ + Returns a list of all reserved IDs associated to a specific request ID. + """ + return [ obj.coverage_id for obj in ReservedCoverageIdRecord.\ + objects.filter(request_id=request_id) ] + + def getAllReservedIds(self): + """ + Returns a list of all reserved IDs associated to a specific request ID. + """ + return [ obj.coverage_id for obj in ReservedCoverageIdRecord.\ + objects.all() ] + + + #def _get_id_factory(self): + # # Unused, but would raise an exception. + # return None + +#------------------------------------------------------------------------------- diff -Nru eoxserver-0.4.0beta2/eoxserver/resources/coverages/managers/__init__.py eoxserver-0.3.2/eoxserver/resources/coverages/managers/__init__.py --- eoxserver-0.4.0beta2/eoxserver/resources/coverages/managers/__init__.py 1970-01-01 00:00:00.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/resources/coverages/managers/__init__.py 2013-12-10 14:57:00.000000000 +0000 @@ -0,0 +1,117 @@ +#------------------------------------------------------------------------------- +# $Id$ +# +# Project: EOxServer +# Authors: Stephan Krause +# Stephan Meissl +# Martin Paces +# +#------------------------------------------------------------------------------- +# Copyright (C) 2011 EOX IT Services GmbH +# +# 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 of this Software or works derived from this 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. +#------------------------------------------------------------------------------- + +""" +This module implements variaous managers providing API for operation over +the stored datasets. For details of the provided functionality see the +documentation of the individual manager classes. +""" + +from eoxserver.core.system import System +from eoxserver.resources.coverages.interfaces import ManagerInterface + +#------------------------------------------------------------------------------- +# manifest of the exported objects + +__all__ = [ + "CoverageIdManager", + "BaseManager", + "BaseManagerContainerMixIn", + "CoverageManager", + "CoverageManagerDatasetMixIn", + "CoverageManagerEOMixIn", + "EODatasetManager", + "RectifiedDatasetManager", + "ReferenceableDatasetManager", + "RectifiedStitchedMosaicManager", + "DatasetSeriesManager", + "RectifiedDatasetManagerImplementation", + "ReferenceableDatasetManagerImplementation", + "RectifiedStitchedMosaicManagerImplementation", + "DatasetSeriesManagerImplementation", +] + +#------------------------------------------------------------------------------- +# load managers' classes + +from id_manager import CoverageIdManager +from base import BaseManager +from base import BaseManagerContainerMixIn +from coverage import CoverageManager +from coverage import CoverageManagerDatasetMixIn +from coverage import CoverageManagerEOMixIn +from eo_ds import EODatasetManager +from eo_ds_rect import RectifiedDatasetManager +from eo_ds_ref import ReferenceableDatasetManager +from eo_sm_rect import RectifiedStitchedMosaicManager +from eo_ds_series import DatasetSeriesManager + +#------------------------------------------------------------------------------- +# create managers' implementations + +RectifiedDatasetManagerImplementation = \ + ManagerInterface.implement(RectifiedDatasetManager) + +ReferenceableDatasetManagerImplementation = \ + ManagerInterface.implement(ReferenceableDatasetManager) + +RectifiedStitchedMosaicManagerImplementation = \ + ManagerInterface.implement(RectifiedStitchedMosaicManager) + +DatasetSeriesManagerImplementation = \ + ManagerInterface.implement(DatasetSeriesManager) + +#------------------------------------------------------------------------------- +# helper functions returning the managers' instances + +def __get_manager( class_obj ) : + + return System.getRegistry().findAndBind( + intf_id="resources.coverages.interfaces.Manager", + params={ "resources.coverages.interfaces.res_type":class_obj._type}) + + +def getRectifiedDatasetManager(): + """ get instance/implementation of ``RectifiedDatasetManager`` """ + return __get_manager( RectifiedDatasetManager ) + +def getReferenceableDatasetManager(): + """ get instance/implementation of ``ReferenceableDatasetManager`` """ + return __get_manager( ReferenceableDatasetManager ) + +def getRectifiedStitchedMosaicManager(): + """ get instance/implementation of ``RectifiedStitchedMosaicManager`` """ + return __get_manager( RectifiedStitchedMosaicManager ) + +def getDatasetSeriesManager(): + """ get instance/implementation of ``DatasetSeriesManager`` """ + return __get_manager( DatasetSeriesManager ) + +#------------------------------------------------------------------------------- diff -Nru eoxserver-0.4.0beta2/eoxserver/resources/coverages/metadata/component.py eoxserver-0.3.2/eoxserver/resources/coverages/metadata/component.py --- eoxserver-0.4.0beta2/eoxserver/resources/coverages/metadata/component.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/resources/coverages/metadata/component.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,54 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -from eoxserver.core import env, Component, ExtensionPoint -from eoxserver.resources.coverages.metadata.interfaces import * - -class MetadataComponent(Component): - metadata_readers = ExtensionPoint(MetadataReaderInterface) - metadata_writers = ExtensionPoint(MetadataWriterInterface) - - - def get_reader_by_test(self, obj): - for reader in self.metadata_readers: - if reader.test(obj): - return reader - return None - - - def get_reader_by_format(self, format): - for reader in self.metadata_readers: - if format in reader.formats: - return reader - return None - - - def get_writer_by_format(self, format): - for writer in self.metadata_writers: - if format in writer.formats: - return writer - return None diff -Nru eoxserver-0.4.0beta2/eoxserver/resources/coverages/metadata/formats/dimap_general.py eoxserver-0.3.2/eoxserver/resources/coverages/metadata/formats/dimap_general.py --- eoxserver-0.4.0beta2/eoxserver/resources/coverages/metadata/formats/dimap_general.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/resources/coverages/metadata/formats/dimap_general.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,155 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -from eoxserver.core.util.timetools import parse_iso8601 -from django.contrib.gis.geos import Polygon, MultiPolygon - -from eoxserver.core import Component, implements -from eoxserver.core.decoders import xml -from eoxserver.core.util.xmltools import parse -from eoxserver.resources.coverages.metadata.interfaces import ( - MetadataReaderInterface -) - - -class DimapGeneralFormatReader(Component): - implements(MetadataReaderInterface) - - def test(self, obj): - tree = parse(obj) - return tree is not None and tree.tag == "Dimap_Document" - - def get_format_name(self, obj): - return "dimap" - - def read(self, obj): - tree = parse(obj) - if self.test(tree): - version = tree.xpath( - "Metadadata_Id/METADATA_FORMAT/" - "|Metadata_Identification/METADATA_FORMAT" - )[0].get("version", "1.1") - if version.startswith("1.1"): - decoder = DimapGeneralFormat11Decoder(tree) - elif version.startswith("2.0"): - decoder = DimapGeneralFormat20Decoder(tree) - else: - raise Exception( - "DIMAP version '%s' is not supported." % version - ) - - values = { - "identifier": decoder.identifier, - "format": "dimap" - } - - # in Dimap, pretty much everything is optional - def cond_set(dct, key, value): - if value is not None: - dct[key] = value - - # decode - begin_time = decoder.begin_time - end_time = decoder.end_time - footprint = decoder.footprint - projection = decoder.projection - size = decoder.size - gt = decoder.geotransform - - # calculate extent - extent = None - if size and gt: - extent = ( - gt[0], gt[3], gt[0] + gt[1] * size[0], gt[3] + gt[5] * size[1] - ) - - # set values - cond_set(values, "begin_time", begin_time) - cond_set(values, "end_time", begin_time) - cond_set(values, "footprint", begin_time) - cond_set(values, "size", begin_time) - cond_set(values, "extent", begin_time) - cond_set(values, "projection", begin_time) - - return values - - raise Exception("Could not parse from obj '%s'." % repr(obj)) - - -def parse_date_or_datetime_11(string): - value = parse_iso8601(string) - if not value: - raise Exception("Could not parse date or datetime from '%s'." % string) - return value - - -def parse_footprint_11(elem): - points = [] - - for vertex in elem.findall("Vertex"): - points.append(( - float(vertex.findtext("FRAME_LON")), - float(vertex.findtext("FRAME_LAT")) - )) - - if points[0] != points[-1]: - points.append(points[0]) - - return MultiPolygon(Polygon(points)) - - -def parse_size_11(elem): - return (int(elem.findtext("NCOLS")), int(elem.findtext("NROWS"))) - - -def parse_geotransform_11(elem): - values = ( - elem.findtext("ULXMAP"), elem.findtext("XDIM"), 0, - elem.findtext("ULYMAP"), 0, elem.findtext("YDIM") - ) - return map(float, values) - - -def parse_projection_11(elem): - pass - # TODO - - -class DimapGeneralFormat11Decoder(xml.Decoder): - identifier = xml.Parameter("Dataset_Id/DATASET_NAME/text()") - begin_time = xml.Parameter("Production/DATASET_PRODUCTION_DATE/text()", type=parse_date_or_datetime_11, num="?") - end_time = xml.Parameter("Production/DATASET_PRODUCTION_DATE/text()", type=parse_date_or_datetime_11, num="?") - footprint = xml.Parameter("Dataset_Frame", type=parse_footprint_11, num="?") - size = xml.Parameter("Raster_Dimensions", type=parse_size_11, num="?") - geotransform = xml.Parameter("Geoposition/Geoposition_Insert", type=parse_geotransform_11, num="?") - projection = xml.Parameter("Coordinate_Reference_System/Horizontal_CS", type=parse_projection_11, num="?") # TODO - - -class DimapGeneralFormat20Decoder(xml.Decoder): - identifier = xml.Parameter("Dataset_Identification/DATASET_ID/text()") - # TODO implement further parameters diff -Nru eoxserver-0.4.0beta2/eoxserver/resources/coverages/metadata/formats/eoom.py eoxserver-0.3.2/eoxserver/resources/coverages/metadata/formats/eoom.py --- eoxserver-0.4.0beta2/eoxserver/resources/coverages/metadata/formats/eoom.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/resources/coverages/metadata/formats/eoom.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,85 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -from django.contrib.gis.geos import Polygon, MultiPolygon - -from eoxserver.core.util.timetools import parse_iso8601 -from eoxserver.core.util.xmltools import parse, NameSpace, NameSpaceMap -from eoxserver.core.util.iteratortools import pairwise -from eoxserver.core import Component, implements -from eoxserver.core.decoders import xml -from eoxserver.resources.coverages.metadata.interfaces import ( - MetadataReaderInterface -) - - -NS_EOP = NameSpace("http://www.opengis.net/eop/2.0", "eop") -NS_OM = NameSpace("http://www.opengis.net/om/2.0", "om") -NS_GML = NameSpace("http://www.opengis.net/gml/3.2", "gml") -nsmap = NameSpaceMap(NS_EOP, NS_OM, NS_GML) - - -class EOOMFormatReader(Component): - implements(MetadataReaderInterface) - - def test(self, obj): - tree = parse(obj) - return tree is not None and tree.tag == NS_EOP("EarthObservation") - - def read(self, obj): - tree = parse(obj) - if tree is not None: - decoder = EOOMFormatDecoder(tree) - return { - "identifier": decoder.identifier, - "begin_time": decoder.begin_time, - "end_time": decoder.end_time, - "footprint": MultiPolygon(*decoder.polygons), - "format": "eogml" - } - raise Exception("Could not parse from obj '%s'." % repr(obj)) - - -def parse_polygon_xml(elem): - return Polygon( - parse_ring(elem.xpath("gml:exterior/gml:LinearRing/gml:posList", namespaces=nsmap)[0].text), - *map(lambda e: parse_ring(e.text), elem.xpath("gml:interior/gml:LinearRing/gml:posList", namespaces=nsmap)) - ) - -def parse_ring(string): - points = [] - raw_coords = map(float, string.split(" ")) - return [(lon, lat) for lat, lon in pairwise(raw_coords)] - - -class EOOMFormatDecoder(xml.Decoder): - identifier = xml.Parameter("eop:metaDataProperty/eop:EarthObservationMetaData/eop:identifier/text()", type=str, num=1) - begin_time = xml.Parameter("om:phenomenonTime/gml:TimePeriod/gml:beginPosition/text()", type=parse_iso8601, num=1) - end_time = xml.Parameter("om:phenomenonTime/gml:TimePeriod/gml:endPosition/text()", type=parse_iso8601, num=1) - polygons = xml.Parameter("om:featureOfInterest/eop:Footprint/eop:multiExtentOf/gml:MultiSurface/gml:surfaceMember/gml:Polygon", type=parse_polygon_xml, num="+") - - namespaces = nsmap diff -Nru eoxserver-0.4.0beta2/eoxserver/resources/coverages/metadata/formats/gdal_dataset_envisat.py eoxserver-0.3.2/eoxserver/resources/coverages/metadata/formats/gdal_dataset_envisat.py --- eoxserver-0.4.0beta2/eoxserver/resources/coverages/metadata/formats/gdal_dataset_envisat.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/resources/coverages/metadata/formats/gdal_dataset_envisat.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,100 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -import re -from datetime import datetime -from os.path import splitext - -from django.utils.timezone import utc - -from eoxserver.core import Component, implements -from eoxserver.resources.coverages.metadata.interfaces import ( - GDALDatasetMetadataReaderInterface -) - - -class GDALDatasetEnvisatMetadataFormatReader(Component): - """ Metadata format reader for specific ENVISAT products. - """ - implements(GDALDatasetMetadataReaderInterface) - - def test_ds(self, ds): - """ Check whether or not the dataset seems to be an ENVISAT image and - has the correct metadata tags. - """ - md_dict = ds.GetMetadata_Dict() - for key in ("MPH_PRODUCT", "MPH_SENSING_START", "MPH_SENSING_STOP"): - if key not in md_dict: - return False - if ds.GetGCPCount() == 0: - return False - - return True - - def read_ds(self, ds): - """ Return the ENVISAT specific metadata items. - """ - return { - "identifier": splitext(ds.GetMetadataItem("MPH_PRODUCT"))[0], - "begin_time": parse_datetime(ds.GetMetadataItem("MPH_SENSING_START")), - "end_time": parse_datetime(ds.GetMetadataItem("MPH_SENSING_STOP")) - } - - -MONTHS = { - "JAN": 1, - "FEB": 2, - "MAR": 3, - "APR": 4, - "MAY": 5, - "JUN": 6, - "JUL": 7, - "AUG": 8, - "SEP": 9, - "OCT": 10, - "NOV": 11, - "DEC": 12 -} - - -def parse_datetime(timestamp): - """ Datetime parsing function for special Envisat datetime format. - """ - - - match = re.match( - r"(\d{2})-([A-Z]{3})-(\d{4}) (\d{2}):(\d{2}):(\d{2}).*", timestamp - ) - day = int(match.group(1)) - month = MONTHS[match.group(2)] - year = int(match.group(3)) - hour = int(match.group(4)) - minute = int(match.group(5)) - second = int(match.group(6)) - - return datetime(year, month, day, hour, minute, second, tzinfo=utc) diff -Nru eoxserver-0.4.0beta2/eoxserver/resources/coverages/metadata/formats/gdal_dataset.py eoxserver-0.3.2/eoxserver/resources/coverages/metadata/formats/gdal_dataset.py --- eoxserver-0.4.0beta2/eoxserver/resources/coverages/metadata/formats/gdal_dataset.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/resources/coverages/metadata/formats/gdal_dataset.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,146 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -from django.contrib.gis.geos import GEOSGeometry, Polygon, MultiPolygon - -from eoxserver.core import Component, ExtensionPoint, implements -from eoxserver.contrib import gdal -from eoxserver.resources.coverages.metadata.interfaces import ( - MetadataReaderInterface, GDALDatasetMetadataReaderInterface -) -from eoxserver.processing.gdal import reftools as rt -from eoxserver.contrib import osr - - -def open_gdal(obj): - if isinstance(obj, gdal.Dataset): - return obj - try: - return gdal.Open(obj) - except RuntimeError: - return None - - -class GDALDatasetMetadataReader(Component): - implements(MetadataReaderInterface) - - additional_readers = ExtensionPoint(GDALDatasetMetadataReaderInterface) - - def test(self, obj): - return open_gdal(obj) is not None - - def get_format_name(self, obj): - ds = open_gdal(obj) - if not ds: - return None - - driver = ds.GetDriver() - return "GDAL/" + driver.ShortName - - def read(self, obj): - ds = open_gdal(obj) - if ds is None: - raise Exception("Could not parse from obj '%s'." % repr(obj)) - - driver = ds.GetDriver() - size = (ds.RasterXSize, ds.RasterYSize) - values = {"size": size} - - # --= rectified datasets =-- - # NOTE: If the projection is a non-zero string then - # the geocoding is given by the Geo-Trasnformation - # matrix - not matter what are the values. - if ds.GetProjection(): - values["coverage_type"] = "RectifiedDataset" - values["projection"] = (ds.GetProjection(), "WKT") - - # get coordinates of all four image corners - gt = ds.GetGeoTransform() - def gtrans(x, y): - return gt[0] + x*gt[1] + y*gt[2], gt[3] + x*gt[4] + y*gt[5] - vpix = [(0, 0), (0, size[1]), (size[0], 0), (size[0], size[1])] - vx, vy = zip(*(gtrans(x, y) for x, y in vpix)) - - # find the extent - values["extent"] = (min(vx), min(vy), max(vx), max(vy)) - - # --= tie-point encoded referenceable datasets =-- - # NOTE: If the GCP projection is a non-zero string and - # there are GCPs we are dealing with a tie-point geocoded - # referenceable dataset. The extent is given by the image - # footprint. The fooprint must not be wrapped arround - # the date-line! - elif ds.GetGCPProjection() and ds.GetGCPCount() > 0: - values["coverage_type"] = "ReferenceableDataset" - projection = ds.GetGCPProjection() - values["projection"] = (projection, "WKT") - - # parse the spatial reference to get the EPSG code - sr = osr.SpatialReference(projection, "WKT") - - # NOTE: GeosGeometry can't handle non-EPSG geometry projections. - if sr.GetAuthorityName(None) == "EPSG": - srid = int(sr.GetAuthorityCode(None)) - - # get the footprint - rt_prm = rt.suggest_transformer(ds) - fp_wkt = rt.get_footprint_wkt(ds, **rt_prm) - footprint = GEOSGeometry(fp_wkt, srid) - - if isinstance(footprint, Polygon): - footprint = MultiPolygon(footprint) - elif not isinstance(footprint, MultiPolygon): - raise TypeError( - "Got invalid geometry %s" % type(footprint).__name__ - ) - - values["footprint"] = footprint - values["extent"] = footprint.extent - - # --= dataset with no geocoding =-- - # TODO: Handling of other types of GDAL geocoding (e.g, RPC). - else: - pass - - reader = self._find_additional_reader(ds) - if reader: - additional_values = reader.read_ds(ds) - for key, value in additional_values.items(): - values.setdefault(key, value) - - driver_metadata = driver.GetMetadata() - frmt = driver_metadata.get("DMD_MIMETYPE") - if frmt: - values["format"] = frmt - - return values - - def _find_additional_reader(self, ds): - for reader in self.additional_readers: - if reader.test_ds(ds): - return reader - return None diff -Nru eoxserver-0.4.0beta2/eoxserver/resources/coverages/metadata/formats/inspire.py eoxserver-0.3.2/eoxserver/resources/coverages/metadata/formats/inspire.py --- eoxserver-0.4.0beta2/eoxserver/resources/coverages/metadata/formats/inspire.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/resources/coverages/metadata/formats/inspire.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,80 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -from django.contrib.gis.geos import Polygon, MultiPolygon - -from eoxserver.core.util.xmltools import parse, NameSpace, NameSpaceMap -from eoxserver.core.util.timetools import parse_iso8601 -from eoxserver.core.util.iteratortools import pairwise -from eoxserver.core import Component, implements -from eoxserver.core.decoders import xml -from eoxserver.resources.coverages.metadata.interfaces import ( - MetadataReaderInterface -) - -NS_GMD = NameSpace("http://www.isotc211.org/2005/gmd", "gmd") -NS_GML = NameSpace("http://www.opengis.net/gml", "gml") -NS_GCO = NameSpace("http://www.isotc211.org/2005/gco", "gco") - -nsmap = NameSpaceMap(NS_GMD, NS_GCO, NS_GML) - - -class InspireFormatReader(Component): - implements(MetadataReaderInterface) - - def test(self, obj): - tree = parse(obj) - return tree is not None and tree.tag == NS_GMD("MD_Metadata") - - def read(self, obj): - tree = parse(obj) - if tree is not None: - decoder = InspireFormatDecoder(tree) - return { - "identifier": decoder.identifier, - "begin_time": decoder.begin_time, - "end_time": decoder.end_time, - "footprint": decoder.footprint, - "format": "inspire" - } - raise Exception("Could not parse from obj '%s'." % repr(obj)) - - -def parse_line_string(string): - raw_coords = map(float, string.strip().split()) - return MultiPolygon( - Polygon([(lon, lat) for lat, lon in pairwise(raw_coords)]) - ) - - -class InspireFormatDecoder(xml.Decoder): - identifier = xml.Parameter("gmd:fileIdentifier/gco:CharacterString/text()", type=str, num=1) - begin_time = xml.Parameter("gmd:dataQualityInfo/gmd:DQ_DataQuality/gmd:lineage/gmd:LI_Lineage/gmd:source/gmd:LI_Source/gmd:sourceExtent/gmd:EX_Extent/gmd:temporalElement/gmd:EX_TemporalExtent/gmd:extent/gml:TimePeriod/gml:beginPosition/text()", type=parse_iso8601, num=1) - end_time = xml.Parameter("gmd:dataQualityInfo/gmd:DQ_DataQuality/gmd:lineage/gmd:LI_Lineage/gmd:source/gmd:LI_Source/gmd:sourceExtent/gmd:EX_Extent/gmd:temporalElement/gmd:EX_TemporalExtent/gmd:extent/gml:TimePeriod/gml:endPosition/text()", type=parse_iso8601, num=1) - footprint = xml.Parameter("gmd:identificationInfo/gmd:MD_DataIdentification/gmd:extent/gmd:EX_Extent/gmd:geographicElement/gmd:EX_BoundingPolygon/gmd:polygon/gml:LineString/gml:posList/text()", type=parse_line_string, num=1) - - namespaces = nsmap diff -Nru eoxserver-0.4.0beta2/eoxserver/resources/coverages/metadata/formats/native.py eoxserver-0.3.2/eoxserver/resources/coverages/metadata/formats/native.py --- eoxserver-0.4.0beta2/eoxserver/resources/coverages/metadata/formats/native.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/resources/coverages/metadata/formats/native.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,122 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -from lxml import etree -from lxml.builder import E - -from django.contrib.gis.geos import Polygon, MultiPolygon - -from eoxserver.core.util.xmltools import parse -from eoxserver.core.util.timetools import isoformat -from eoxserver.core.util.iteratortools import pairwise -from eoxserver.core.util.timetools import parse_iso8601 -from eoxserver.core import Component, implements -from eoxserver.core.decoders import xml -from eoxserver.resources.coverages.metadata.interfaces import ( - MetadataReaderInterface, MetadataWriterInterface -) - - -class NativeFormat(Component): - implements(MetadataReaderInterface) - implements(MetadataWriterInterface) - - formats = ("native", ) - - def test(self, obj): - xml = parse(obj) - return xml is not None and xml.tag == "Metadata" - - def get_format_name(self, obj): - return "native" - - def read(self, obj): - tree = parse(obj) - if tree is not None: - decoder = NativeFormatDecoder(tree) - return { - "identifier": decoder.identifier, - "begin_time": decoder.begin_time, - "end_time": decoder.end_time, - "footprint": MultiPolygon(*decoder.polygons), - "format": "native" - } - raise Exception("Could not parse from obj '%s'." % repr(obj)) - - def write(self, values, file_obj, format=None, encoding=None, pretty=False): - def flip(point): - return point[1], point[0] - - # ignore format - tree = E.Metadata( - E.EOID(values["identifier"]), - E.BeginTime(isoformat(values["begin_time"])), - E.EndTime(isoformat(values["end_time"])), - E.Footprint( - *map(lambda polygon: - E.Polygon( - E.Exterior( - " ".join([ - "%f %f" % flip(point) - for point in polygon.exterior_ring - ]) - ), - *[E.Interior( - " ".join([ - "%f %f" % flip(point) - for point in interior - ]) - ) for interior in polygon[1:]] - ), - values["footprint"] - ) - ) - ) - - file_obj.write( - etree.tostring(tree, pretty_print=pretty, encoding=encoding) - ) - - -def parse_polygon_xml(elem): - return Polygon( - parse_ring(elem.findtext("Exterior")), - *map(lambda e: parse_ring(e.text), elem.findall("Interior")) - ) - - -def parse_ring(string): - raw_coords = map(float, string.split(" ")) - return [(lon, lat) for lat, lon in pairwise(raw_coords)] - - -class NativeFormatDecoder(xml.Decoder): - identifier = xml.Parameter("EOID/text()") - begin_time = xml.Parameter("BeginTime/text()", type=parse_iso8601) - end_time = xml.Parameter("EndTime/text()", type=parse_iso8601) - polygons = xml.Parameter("Footprint/Polygon", type=parse_polygon_xml, num="+") diff -Nru eoxserver-0.4.0beta2/eoxserver/resources/coverages/metadata/interfaces.py eoxserver-0.3.2/eoxserver/resources/coverages/metadata/interfaces.py --- eoxserver-0.4.0beta2/eoxserver/resources/coverages/metadata/interfaces.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/resources/coverages/metadata/interfaces.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,119 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -class MetadataReaderInterface(object): - """ Interface for metadata readers. - """ - - def test(self, obj): - """ Return a boolean value, whether or not metadata can be extracted - from the given object. - """ - pass - - - def format(self, obj): - """ Returns a format specifier for the given object. Can be ignored, - when the reader only supports one format. - """ - pass - - - def read(self, obj): - """ Returns a dict with any of the following keys: - - identifier (string) - - extent (a four tuple of floats) - - size (a two-tuple of ints) - - projection (an integer or two-tuple of two strings (definition and format)) - - footprint (a django.contrib.gis.geos.MultiPolygon) - - begin_time (a python datetime.datetime) - - end_time (a python datetime.datetime) - - The argument obj is of an arbitrary type, the reader needs to - determine whether or not the type is supported and an exception - shall be raised if not. - """ - pass - - -class MetadataWriterInterface(object): - """ Interface for metadata writers. - """ - - @property - def formats(self): - pass - - - def write(self, values, file_obj, format=None): - """ Write the given values (a dict) to the file-like object `file_obj`. - The dict contains all of the following entries: - - identifier (string) - - extent (a four tuple of floats) - - size (a two-tuple of ints) - - projection (an integer or two-tuple of two strings (definition and format)) - - footprint (a django.contrib.gis.geos.MultiPolygon) - - begin_time (a python datetime.datetime) - - end_time (a python datetime.datetime) - - The writer may ignore non-applicable parameters. - """ - pass - - -class GDALDatasetMetadataReaderInterface(object): - """ Interface for GDAL dataset metadata readers. - """ - - def test_ds(self, obj): - """ Return a boolean value, whether or not metadata can be extracted - from the given object. - """ - pass - - - def format(self, obj): - """ Returns a format specifier for the given object. Can be ignored, - when the reader only supports one format. - """ - pass - - - def read_ds(self, ds): - """ Returns a dict with any of the following keys: - - identifier (string) - - extent (a four tuple of floats) - - size (a two-tuple of ints) - - projection (an integer or two-tuple of two strings (definition and format)) - - footprint (a django.contrib.gis.geos.MultiPolygon) - - begin_time (a python datetime.datetime) - - end_time (a python datetime.datetime) - - The argument `ds` is a gdal.Dataset. - """ - pass diff -Nru eoxserver-0.4.0beta2/eoxserver/resources/coverages/metadata.py eoxserver-0.3.2/eoxserver/resources/coverages/metadata.py --- eoxserver-0.4.0beta2/eoxserver/resources/coverages/metadata.py 1970-01-01 00:00:00.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/resources/coverages/metadata.py 2013-12-10 14:57:00.000000000 +0000 @@ -0,0 +1,697 @@ +#------------------------------------------------------------------------------- +# $Id$ +# +# Project: EOxServer +# Authors: Stephan Krause +# Stephan Meissl +# +#------------------------------------------------------------------------------- +# Copyright (C) 2011 EOX IT Services GmbH +# +# 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 of this Software or works derived from this 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. +#------------------------------------------------------------------------------- + +""" +This module contains the implementation of basic XML EO metadata formats and +EO metadata objects. +""" + +import re +import datetime +from os.path import splitext + +from django.contrib.gis.geos import GEOSGeometry + +from eoxserver.contrib import gdal +from eoxserver.core.system import System +from eoxserver.core.exceptions import ( + InternalError, ImplementationAmbiguous, ImplementationNotFound +) +from eoxserver.core.util.xmltools import XMLDecoder, XMLEncoder, etree +from eoxserver.core.util.timetools import getDateTime +from eoxserver.resources.coverages import crss +from eoxserver.resources.coverages.interfaces import ( + GenericEOMetadataInterface, EOMetadataFormatInterface, + EOMetadataReaderInterface +) +from eoxserver.resources.coverages.exceptions import MetadataException +from eoxserver.processing.gdal import reftools as rt + +#------------------------------------------------------------------------------- +# defining namespaces + +NS_EOP="http://www.opengis.net/eop/2.0" +NS_OMD="http://www.opengis.net/om/2.0" +NS_GML="http://www.opengis.net/gml/3.2" + +#------------------------------------------------------------------------------- + +class MetadataFormat(object): + """ + Abstract base class for metada formats. A blueprint for implementing + :class:`~.MetadataFormatInterface`. + """ + def test(self, test_params): + """ + Not implemented. Raises :exc:`~.InternalError`. + """ + + raise InternalError("Not implemented.") + + def getName(self): + """ + Not implemented. Raises :exc:`~.InternalError`. + """ + + raise InternalError("Not implemented.") + + def getMetadataKeys(self): + """ + Not implemented. Raises :exc:`~.InternalError`. + """ + + raise InternalError("Not implemented.") + + def getMetadataValues(self, keys, raw_metadata): + """ + Not implemented. Raises :exc:`~.InternalError`. + """ + + raise InternalError("Not implemented.") + +class XMLMetadataFormat(MetadataFormat): + """ + This is a base class for XML based metadata formats. It inherits from + :class:`MetadataFormat`. + """ + + # concrete formats must define an XML decoding schema as defined by + # core.util.xmltools.XMLDecoder here + #PARAM_SCHEMA = {} + + def getMetadataKeys(self): + """ + Returns the keys for key-value-pair metadata access. + """ + + return self.PARAM_SCHEMA.keys() + + def getMetadataValues(self, keys, raw_metadata): + """ + Returns metadata key-value-pairs for the given ``keys``. The argument + ``raw_metadata`` is expected to be a string containing valid XML. This + method raises :exc:`~.InternalError` if ``raw_metadata`` is not a + string or :exc:`~.MetadataException` if it cannot be parsed as valid + XML. + """ + + if not isinstance(raw_metadata, (str, unicode)): + raise InternalError( + "XML metadata formats cannot decode raw metadata of type '%s'." %\ + raw_metadata.__class__.__name__ + ) + + try: + decoder = XMLDecoder(raw_metadata, self.PARAM_SCHEMA) + except Exception, e: + raise MetadataException( + "Invalid XML input to XMLMetadataFormat.getMetadataValues(). Error message: '%s'" %\ + str(e) + ) + + ret_dict = {} + + for key in keys: + if key not in self.PARAM_SCHEMA: + ret_dict[key] = None + else: + ret_dict[key] = decoder.getValue(key) + + return ret_dict + + +class EnvisatDatasetMetadataFormat(MetadataFormat): + """ Metadata format for ENVISAT datasets. """ + + REGISTRY_CONF = { + "name": "ENVISAT Dataset Metadata Format", + "impl_id": "resources.coverages.metadata.EnvisatDatasetMetadataFormat" + } + + METADATA_TAGS = ("MPH_PRODUCT", "MPH_SENSING_START", "MPH_SENSING_STOP") + + def test(self, test_params): + """ + This metadata format is applicable, if all metadata tags (``MPH_PRODUCT``, + ``MPH_SENSING_START``, ``MPH_SENSING_STOP``) are found within the + metadata entries of the dataset and the dataset contains at least one GCP. + """ + try: + ds = test_params["dataset"] + md_dict = ds.GetMetadata_Dict() + if (all([(md in md_dict) for md in self.METADATA_TAGS]) + and len(ds.GetGCPs()) > 0): + return True + except (KeyError, AttributeError): + pass + return False + + def getName(self): + return "envisat-dataset" + + def getEOMetadata(self, raw_metadata): + if not isinstance(raw_metadata, gdal.Dataset): + raise InternalError( + "ENVISAT Dataset metadata formats cannot decode raw metadata of type '%s'." %\ + raw_metadata.__class__.__name__ + ) + + eoid = splitext(raw_metadata.GetMetadataItem("MPH_PRODUCT"))[0] + rt_prm = rt.suggest_transformer(raw_metadata) + fp_wkt = rt.get_footprint_wkt(raw_metadata,**rt_prm) + + return EOMetadata( + eo_id=eoid, + begin_time=self._parse_timestamp(raw_metadata.GetMetadataItem("MPH_SENSING_START")), + end_time=self._parse_timestamp(raw_metadata.GetMetadataItem("MPH_SENSING_STOP")), + footprint= GEOSGeometry( fp_wkt ), + md_format=self, + ) + + def _parse_timestamp(self, timestamp): + MONTHS = { + "JAN": 1, + "FEB": 2, + "MAR": 3, + "APR": 4, + "MAY": 5, + "JUN": 6, + "JUL": 7, + "AUG": 8, + "SEP": 9, + "OCT": 10, + "NOV": 11, + "DEC": 12 + } + + m = re.match(r"(\d{2})-([A-Z]{3})-(\d{4}) (\d{2}):(\d{2}):(\d{2}).*", timestamp) + day = int(m.group(1)) + month = MONTHS[m.group(2)] + year = int(m.group(3)) + hour = int(m.group(4)) + minute = int(m.group(5)) + second = int(m.group(6)) + + return datetime.datetime(year, month, day, hour, minute, second) + + def getMetadataKeys(self): + """ + Returns the keys for key-value-pair metadata access. + """ + + return self.METADATA_TAGS + +EnvisatDatasetMetadataFormatImplementation = \ +EOMetadataFormatInterface.implement(EnvisatDatasetMetadataFormat) + +class XMLEOMetadataFormat(XMLMetadataFormat): + """ + This is the base class for XML EO Metadata formats implementing + :class:`~.EOMetadataFormatInterface`. It adds :meth:`getEOMetadata` to + the :class:`XMLMetadataFormat` implementation it inherits from. + """ + + def getEOMetadata(self, raw_metadata): + """ + This method decodes the raw XML metadata passed to it and returns + an :class:`EOMetadata` instance. The method raises + :exc:`~.InternalError` if ``raw_metadata`` is not a string or + :exc:`~.MetadataException` if it cannot be parsed as valid XML. + """ + if not isinstance(raw_metadata, str) or isinstance(raw_metadata, unicode): + raise InternalError( + "XML EO metadata formats cannot decode raw metadata of type '%s'." %\ + raw_metadata.__class__.__name__ + ) + + try: + decoder = XMLDecoder(raw_metadata, self.PARAM_SCHEMA) + except Exception, e: + raise MetadataException( + "Invalid XML input to XMLMetadataFormat.getEOMetadata(). Error message: '%s'" %\ + str(e) + ) + + return EOMetadata( + eo_id = self._get_eo_id(decoder), + begin_time = self._get_begin_time(decoder), + end_time = self._get_end_time(decoder), + footprint = self._get_footprint(decoder), + md_format = self, + raw_metadata = raw_metadata + ) + + def _get_eo_id(self, decoder): + return decoder.getValue("eoid") + + def _get_begin_time(self, decoder): + return getDateTime(decoder.getValue("begintime")) + + def _get_end_time(self, decoder): + return getDateTime(decoder.getValue("endtime")) + + def _get_footprint(self, decoder): + + #footprint SRID + srid = 4326 + + # float format + frm = "%.10g %.10g" + + #axes swapper + swap = crss.getAxesSwapper( srid ) + + # WKT polygon packing closure + def posListToWkt( pl ): + tmp = [ frm%swap(pl[i],pl[i+1]) for i in xrange(0,len(pl),2) ] + return ",".join( tmp ) + + polygon_dicts = decoder.getValue("footprint") + + polygon_wkts = [] + for polygon_dict in polygon_dicts: + exterior_wkt = "(%s)" % posListToWkt(polygon_dict["exterior_ring"]) + interior_wkts = ["(%s)" % posListToWkt(interior_ring) for interior_ring in polygon_dict["interior_rings"]] + + if len(interior_wkts) == 0: + polygon_wkts.append("(%s)" % exterior_wkt) + else: + polygon_wkts.append("(%s,%s)" % (exterior_wkt, ",".join(interior_wkts))) + + if len(polygon_wkts) == 1: + wkt = "POLYGON%s" % polygon_wkts[0] + elif len(polygon_wkts) == 0: + wkt = "" + else: + wkt = "MULTIPOLYGON(%s)" % ",".join(polygon_wkts) + + return GEOSGeometry(wkt) + +class NativeMetadataFormat(XMLEOMetadataFormat): + """ + This is an implementation of an EOxServer native metadata format. This + format was designed to be as simple as possible and is intended for use in + a testing environment. A template XML snippet looks like:: + + + some_unique_eoid + YYYY-MM-DDTHH:MM:SSZ + YYYY-MM-DDTHH:MM:SSZ + + + Mandatory - some_pos_list as all-space-delimited Lat Lon pairs (closed polygon i.e. 5 coordinate pairs for a rectangle) in EPSG:4326 + [ + Optional - some_pos_list as all-space-delimited Lat Lon pairs (closed polygon) in EPSG:4326 + ... + ] + + + + """ + REGISTRY_CONF = { + "name": "EOxServer Native EO Metadata Format", + "impl_id": "resources.coverages.metadata.NativeMetadataFormat" + } + + PARAM_SCHEMA = { + "eoid": {"xml_location": "/EOID", "xml_type": "string"}, + "begintime": {"xml_location": "/BeginTime", "xml_type": "string"}, + "endtime": {"xml_location": "/EndTime", "xml_type": "string"}, + "footprint": {"xml_location": "/Footprint/Polygon", "xml_type": "dict[1:]", "xml_dict_elements": { + "exterior_ring": {"xml_location": "Exterior", "xml_type": "floatlist"}, + "interior_rings": {"xml_location": "Interior", "xml_type": "floatlist[]"} + }} + } + + def test(self, test_params): + """ + This method is required by the :class:`~.Registry`. It tests whether + XML input can be interpreted as EOxServer native XML. It expects one + dictionary entry ``root_name`` in the ``test_params`` dictionary. It + will raise :exc:`~.InternalError` if it is missing. + + The method will return ``True`` if the ``root_name`` is ``"Metadata"``, + ``False`` otherwise. + """ + if "root_name" in test_params: + return test_params["root_name"] == "Metadata" + else: + raise InternalError( + "Missing mandatory 'root_name' test parameter for metadata format detection." + ) + + def getName(self): + """ + Returns ``"native"``. + """ + + return "native" + +NativeMetadataFormatImplementation = \ +EOMetadataFormatInterface.implement(NativeMetadataFormat) + +class EOOMFormat(XMLEOMetadataFormat): + """ + This is a basic implementation of the OGC (and ESA HMA) EO O&M metadata + format. + """ + + REGISTRY_CONF = { + "name": "EO O&M Metadata Format", + "impl_id": "resources.coverages.metadata.EOOMFormat" + } + + PARAM_SCHEMA = { + "eoid": {"xml_location": "/{%s}metaDataProperty/{%s}EarthObservationMetaData/{%s}identifier"%(NS_EOP,NS_EOP,NS_EOP), "xml_type": "string"}, + "begintime": {"xml_location": "/{%s}phenomenonTime/{%s}TimePeriod/{%s}beginPosition"%(NS_OMD,NS_GML,NS_GML), "xml_type": "string"}, + "endtime": {"xml_location": "/{%s}phenomenonTime/{%s}TimePeriod/{%s}endPosition"%(NS_OMD,NS_GML,NS_GML), "xml_type": "string"}, + "footprint": {"xml_location": "/{%s}featureOfInterest/{%s}Footprint/{%s}multiExtentOf/{%s}MultiSurface/{%s}surfaceMember/{%s}Polygon"%(NS_OMD,NS_EOP,NS_EOP,NS_GML,NS_GML,NS_GML), "xml_type": "dict[1:]", "xml_dict_elements": { + "exterior_ring": {"xml_location": "{%s}exterior/{%s}LinearRing/{%s}posList"%(NS_GML,NS_GML,NS_GML), "xml_type": "floatlist"}, + "interior_rings": {"xml_location": "{%s}interior/{%s}LinearRing/{%s}posList"%(NS_GML,NS_GML,NS_GML), "xml_type": "floatlist[]"} + }} + } + + def test(self, test_params): + """ + This method is required by the :class:`~.Registry`. It tests whether + XML input can be interpreted as EOxServer native XML. It expects one + dictionary entry ``root_name`` in the ``test_params`` dictionary. It + will raise :exc:`~.InternalError` if it is missing. + + The method will return ``True`` if the ``root_name`` is ``"Metadata"``, + ``False`` otherwise. + """ + if "root_name" in test_params: + return test_params["root_name"] == "EarthObservation" + else: + raise InternalError( + "Missing mandatory 'root_name' test parameter for metadata format detection." + ) + + def getName(self): + """ + Returns ``"eogml"``. + """ + + return "eogml" + +EOOMFormatImplementation = EOMetadataFormatInterface.implement(EOOMFormat) + +class DIMAPFormat(MetadataFormat): + # NOT YET IMPLEMENTED + + def test(self, test_params): + return False + + def getName(self): + return "dimap" + +class EOMetadata(object): + """ + This is an implementation of :class:`~.GenericEOMetadataInterface`. It + is an object containing the basic set of EO Metadata required by EOxServer. + Additional metadata is available using the generic metadata access methods. + + Instances of this object are returned by metadata format implementations. + """ + + REGISTRY_CONF = { + "name": "EO Metadata object", + "impl_id": "resources.coverages.metadata.EOMetadata" + } + + def __init__(self, eo_id, begin_time, end_time, footprint, md_format=None, raw_metadata=None): + self.eo_id = eo_id + self.begin_time = begin_time + self.end_time = end_time + self.footprint = footprint + + self.md_format = md_format + self.raw_metadata = raw_metadata + + def getEOID(self): + """ + Returns the EO ID of the object. + """ + return self.eo_id + + def getBeginTime(self): + """ + Returns the acquisition begin time as :class:`datetime.datetime` + object. + """ + return self.begin_time + + def getEndTime(self): + """ + Returns the acquisition end time as :class:`datetime.datetime` + object. + """ + return self.end_time + + def getFootprint(self): + """ + Returns the acquisition footprint as + :class:`django.contrib.gis.geos.GEOSGeometry` object. + """ + return self.footprint + + def getMetadataFormat(self): + """ + Returns the metadata format object, i.e. an implementation of + :class:`~.EOMetadataFormatInterface` if one was defined when creating + the object, ``None`` otherwise. + """ + return self.md_format + + def getMetadataKeys(self): + """ + Returns the keys of the metadata key-value-pairs that can be retrieved + from this instance or an empty list if no metadata format has been + specified that can decode the raw metadata. + """ + if self.md_format: + return self.md_format.getMetadataKeys() + else: + return [] + + def getMetadataValues(self, keys): + """ + Returns a dictionary of metadata key-value-pairs for the given keys. + If there is no metadata format and/or no raw metadata object defined + for the instance a dictionary mapping the keys to ``None`` is returned. + """ + + if self.md_format and self.raw_metadata: + return self.md_format.getMetadataValues(keys, self.raw_metadata) + else: + return dict.fromkeys(keys) + + def getRawMetadata(self): + return self.raw_metadata + +EOMetadataImplementation = GenericEOMetadataInterface.implement(EOMetadata) + +class XMLEOMetadataFileReader(object): + """ + This is an implementation of :class:`~.EOMetadataReaderInterface` for + local XML files, i.e. ``resources.coverages.interfaces.location_type`` + is ``local`` and ``resources.coverages.interfaces.encoding_type`` is + ``xml``. + """ + REGISTRY_CONF = { + "name": "XML EO Metadata File Reader", + "impl_id": "resources.coverages.metadata.XMLEOMetadataFileReader", + "registry_values": { + "resources.coverages.interfaces.location_type": "local", + "resources.coverages.interfaces.encoding_type": "xml" + } + } + + def test(self, test_params): + try: + with open(test_params["location"].getPath()) as f: + etree.parse(f) + return True + except: + return False + + def readEOMetadata(self, location): + """ + Returns an :class:`EOMetadata` object for the XML file at the given + local path. Raises :exc:`~.InternalError` if the location is not a + path on the local file system or :exc:`~.DataAccessError` if it cannot + be opened. :exc:`~.MetadataException` is raised if the file content + is not valid XML or if the XML metadata format is unknown. + """ + + if location.getType() == "local": + md_file = location.open() + else: + raise InternalError( + "Attempt to read metadata from non-local location." + ) + + xml = md_file.read() + + md_file.close() + + DETECTION_SCHEMA = { + "root_name": {"xml_location": "/", "xml_type": "localName"} + } + + try: + decoder = XMLDecoder(xml, DETECTION_SCHEMA) + except Exception, e: + raise MetadataException( + "File at '%s' is not valid XML. Error message: '%s'" %\ + location.getPath(), str(e) + ) + + root_name = decoder.getValue("root_name") + + try: + md_format = System.getRegistry().findAndBind( + intf_id = "resources.coverages.interfaces.EOMetadataFormat", + params = {"root_name": root_name} + ) + except ImplementationNotFound: + raise MetadataException( + "Unknown XML EO Metadata format. XML root element name not '%s' not recognized." %\ + root_name + ) + except ImplementationAmbiguous: + raise InternalError( + "Multiple XML EO Metadata Formats for XML root element name '%s'." %\ + root_name + ) + + return md_format.getEOMetadata(xml) + +XMLEOMetadataFileReaderImplementation = \ +EOMetadataReaderInterface.implement(XMLEOMetadataFileReader) + +class DatasetMetadataFileReader(object): + """ + This is an implementation of :class:`~.EOMetadataReaderInterface` for + local dataset files, i.e. ``resources.coverages.interfaces.location_type`` + is ``local`` and ``resources.coverages.interfaces.encoding_type`` is + ``dataset``. + """ + + REGISTRY_CONF = { + "name": "Dataset Metadata File Reader", + "impl_id": "resources.coverages.metadata.DatasetMetadataFileReader", + "registry_values": { + "resources.coverages.interfaces.location_type": "local", + "resources.coverages.interfaces.encoding_type": "dataset" + } + } + + def test(self, test_params): + try: + gdal.Open(test_params["location"].getPath()) + return True + except: + return False + + def readEOMetadata(self, location): + """ + Returns an :class:`EOMetadata` object for the dataset file at the given + local path. Raises :exc:`~.InternalError` if the location is not a + path on the local file system or :exc:`~.DataAccessError` if it cannot + be opened. :exc:`~.MetadataException` is raised if the file content + is not valid or if the metadata format is unknown. + """ + + if location.getType() != "local": + raise InternalError( + "Attempt to read metadata from non-local location." + ) + + try: + ds = gdal.Open(location.getPath()) + driver = ds.GetDriver() + except Exception, e: #TODO here! fill in exception class for failure in opening GDAL datasets + raise MetadataException( + "File at '%s' is not a valid GDAL dataset. Error message: '%s'" %\ + location.getPath(), str(e) + ) + + try: + md_format = System.getRegistry().findAndBind( + intf_id = "resources.coverages.interfaces.EOMetadataFormat", + params={ + "root_name": "", + "dataset": ds, + "driver": driver + } + ) + except ImplementationNotFound: + raise MetadataException( + "Unknown Metadata format. Driver '%s' not recognized." %\ + driver.ShortName + ) + except ImplementationAmbiguous: + raise InternalError( + "Multiple EO Metadata Formats for driver '%s'." %\ + driver.ShortName + ) + + return md_format.getEOMetadata(ds) + + +DatasetMetadataFileReaderImplementation = \ +EOMetadataReaderInterface.implement(DatasetMetadataFileReader) + + +class NativeMetadataFormatEncoder(XMLEncoder): + """ + Encodes EO Coverage metadata + """ + + def encodeMetadata(self, eoid, begin_time, end_time, polygon): + return self._makeElement("", "Metadata", [ + ("", "EOID", eoid), + ("", "BeginTime", begin_time), + ("", "EndTime", end_time), + ("", "Footprint", [ + ("", "Polygon", [ + ("", "Exterior", self._posListToString(polygon[0])) + ] + [ + tuple("", "Interior", self._posListToString(interior)) + for interior in polygon[1:] + ]) + ]) + ]) + + + def _posListToString(self, ring): + return " ".join(map(str, ring)) + diff -Nru eoxserver-0.4.0beta2/eoxserver/resources/coverages/migrations/0001_initial.py eoxserver-0.3.2/eoxserver/resources/coverages/migrations/0001_initial.py --- eoxserver-0.4.0beta2/eoxserver/resources/coverages/migrations/0001_initial.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/resources/coverages/migrations/0001_initial.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,323 +0,0 @@ -# -*- coding: utf-8 -*- -from south.utils import datetime_utils as datetime -from south.db import db -from south.v2 import SchemaMigration -from django.db import models - - -class Migration(SchemaMigration): - - def forwards(self, orm): - # Adding model 'Projection' - db.create_table(u'coverages_projection', ( - (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('name', self.gf('django.db.models.fields.CharField')(unique=True, max_length=64)), - ('format', self.gf('django.db.models.fields.CharField')(max_length=16)), - ('definition', self.gf('django.db.models.fields.TextField')()), - )) - db.send_create_signal(u'coverages', ['Projection']) - - # Adding model 'DataSource' - db.create_table(u'coverages_datasource', ( - (u'dataset_ptr', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['backends.Dataset'], unique=True, primary_key=True)), - ('pattern', self.gf('django.db.models.fields.CharField')(max_length=32)), - ('collection', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['coverages.Collection'])), - )) - db.send_create_signal(u'coverages', ['DataSource']) - - # Adding model 'EOObject' - db.create_table(u'coverages_eoobject', ( - (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('begin_time', self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True)), - ('end_time', self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True)), - ('footprint', self.gf('django.contrib.gis.db.models.fields.MultiPolygonField')(null=True, blank=True)), - ('identifier', self.gf('django.db.models.fields.CharField')(unique=True, max_length=256)), - ('real_content_type', self.gf('django.db.models.fields.PositiveSmallIntegerField')()), - )) - db.send_create_signal(u'coverages', ['EOObject']) - - # Adding model 'ReservedID' - db.create_table(u'coverages_reservedid', ( - (u'eoobject_ptr', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['coverages.EOObject'], unique=True, primary_key=True)), - ('until', self.gf('django.db.models.fields.DateTimeField')(null=True)), - ('request_id', self.gf('django.db.models.fields.CharField')(max_length=256, null=True)), - )) - db.send_create_signal(u'coverages', ['ReservedID']) - - # Adding model 'NilValueSet' - db.create_table(u'coverages_nilvalueset', ( - (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('name', self.gf('django.db.models.fields.CharField')(max_length=512)), - ('data_type', self.gf('django.db.models.fields.PositiveIntegerField')()), - )) - db.send_create_signal(u'coverages', ['NilValueSet']) - - # Adding model 'NilValue' - db.create_table(u'coverages_nilvalue', ( - (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('raw_value', self.gf('django.db.models.fields.CharField')(max_length=512)), - ('reason', self.gf('django.db.models.fields.CharField')(max_length=512)), - ('nil_value_set', self.gf('django.db.models.fields.related.ForeignKey')(related_name='nil_values', to=orm['coverages.NilValueSet'])), - )) - db.send_create_signal(u'coverages', ['NilValue']) - - # Adding model 'RangeType' - db.create_table(u'coverages_rangetype', ( - (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('name', self.gf('django.db.models.fields.CharField')(unique=True, max_length=512)), - )) - db.send_create_signal(u'coverages', ['RangeType']) - - # Adding model 'Band' - db.create_table(u'coverages_band', ( - (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('index', self.gf('django.db.models.fields.PositiveSmallIntegerField')()), - ('name', self.gf('django.db.models.fields.CharField')(max_length=512)), - ('identifier', self.gf('django.db.models.fields.CharField')(max_length=512)), - ('description', self.gf('django.db.models.fields.TextField')(null=True, blank=True)), - ('definition', self.gf('django.db.models.fields.CharField')(max_length=512, null=True, blank=True)), - ('uom', self.gf('django.db.models.fields.CharField')(max_length=64)), - ('data_type', self.gf('django.db.models.fields.PositiveIntegerField')()), - ('color_interpretation', self.gf('django.db.models.fields.PositiveIntegerField')(null=True, blank=True)), - ('raw_value_min', self.gf('django.db.models.fields.CharField')(max_length=512, null=True, blank=True)), - ('raw_value_max', self.gf('django.db.models.fields.CharField')(max_length=512, null=True, blank=True)), - ('range_type', self.gf('django.db.models.fields.related.ForeignKey')(related_name='bands', to=orm['coverages.RangeType'])), - ('nil_value_set', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['coverages.NilValueSet'], null=True, blank=True)), - )) - db.send_create_signal(u'coverages', ['Band']) - - # Adding unique constraint on 'Band', fields ['index', 'range_type'] - db.create_unique(u'coverages_band', ['index', 'range_type_id']) - - # Adding unique constraint on 'Band', fields ['identifier', 'range_type'] - db.create_unique(u'coverages_band', ['identifier', 'range_type_id']) - - # Adding model 'Coverage' - db.create_table(u'coverages_coverage', ( - (u'dataset_ptr', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['backends.Dataset'], unique=True)), - (u'eoobject_ptr', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['coverages.EOObject'], unique=True, primary_key=True)), - ('min_x', self.gf('django.db.models.fields.FloatField')()), - ('min_y', self.gf('django.db.models.fields.FloatField')()), - ('max_x', self.gf('django.db.models.fields.FloatField')()), - ('max_y', self.gf('django.db.models.fields.FloatField')()), - ('srid', self.gf('django.db.models.fields.PositiveIntegerField')(null=True, blank=True)), - ('projection', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['coverages.Projection'], null=True, blank=True)), - ('size_x', self.gf('django.db.models.fields.PositiveIntegerField')()), - ('size_y', self.gf('django.db.models.fields.PositiveIntegerField')()), - ('range_type', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['coverages.RangeType'])), - ('visible', self.gf('django.db.models.fields.BooleanField')(default=False)), - )) - db.send_create_signal(u'coverages', ['Coverage']) - - # Adding model 'Collection' - db.create_table(u'coverages_collection', ( - (u'eoobject_ptr', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['coverages.EOObject'], unique=True, primary_key=True)), - )) - db.send_create_signal(u'coverages', ['Collection']) - - # Adding model 'EOObjectToCollectionThrough' - db.create_table(u'coverages_eoobjecttocollectionthrough', ( - (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('eo_object', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['coverages.EOObject'])), - ('collection', self.gf('django.db.models.fields.related.ForeignKey')(related_name='coverages_set', to=orm['coverages.Collection'])), - )) - db.send_create_signal(u'coverages', ['EOObjectToCollectionThrough']) - - # Adding unique constraint on 'EOObjectToCollectionThrough', fields ['eo_object', 'collection'] - db.create_unique(u'coverages_eoobjecttocollectionthrough', ['eo_object_id', 'collection_id']) - - # Adding model 'RectifiedDataset' - db.create_table(u'coverages_rectifieddataset', ( - (u'coverage_ptr', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['coverages.Coverage'], unique=True, primary_key=True)), - )) - db.send_create_signal(u'coverages', ['RectifiedDataset']) - - # Adding model 'ReferenceableDataset' - db.create_table(u'coverages_referenceabledataset', ( - (u'coverage_ptr', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['coverages.Coverage'], unique=True, primary_key=True)), - )) - db.send_create_signal(u'coverages', ['ReferenceableDataset']) - - # Adding model 'RectifiedStitchedMosaic' - db.create_table(u'coverages_rectifiedstitchedmosaic', ( - (u'collection_ptr', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['coverages.Collection'], unique=True)), - (u'coverage_ptr', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['coverages.Coverage'], unique=True, primary_key=True)), - )) - db.send_create_signal(u'coverages', ['RectifiedStitchedMosaic']) - - # Adding model 'DatasetSeries' - db.create_table(u'coverages_datasetseries', ( - (u'collection_ptr', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['coverages.Collection'], unique=True, primary_key=True)), - )) - db.send_create_signal(u'coverages', ['DatasetSeries']) - - - def backwards(self, orm): - # Removing unique constraint on 'EOObjectToCollectionThrough', fields ['eo_object', 'collection'] - db.delete_unique(u'coverages_eoobjecttocollectionthrough', ['eo_object_id', 'collection_id']) - - # Removing unique constraint on 'Band', fields ['identifier', 'range_type'] - db.delete_unique(u'coverages_band', ['identifier', 'range_type_id']) - - # Removing unique constraint on 'Band', fields ['index', 'range_type'] - db.delete_unique(u'coverages_band', ['index', 'range_type_id']) - - # Deleting model 'Projection' - db.delete_table(u'coverages_projection') - - # Deleting model 'DataSource' - db.delete_table(u'coverages_datasource') - - # Deleting model 'EOObject' - db.delete_table(u'coverages_eoobject') - - # Deleting model 'ReservedID' - db.delete_table(u'coverages_reservedid') - - # Deleting model 'NilValueSet' - db.delete_table(u'coverages_nilvalueset') - - # Deleting model 'NilValue' - db.delete_table(u'coverages_nilvalue') - - # Deleting model 'RangeType' - db.delete_table(u'coverages_rangetype') - - # Deleting model 'Band' - db.delete_table(u'coverages_band') - - # Deleting model 'Coverage' - db.delete_table(u'coverages_coverage') - - # Deleting model 'Collection' - db.delete_table(u'coverages_collection') - - # Deleting model 'EOObjectToCollectionThrough' - db.delete_table(u'coverages_eoobjecttocollectionthrough') - - # Deleting model 'RectifiedDataset' - db.delete_table(u'coverages_rectifieddataset') - - # Deleting model 'ReferenceableDataset' - db.delete_table(u'coverages_referenceabledataset') - - # Deleting model 'RectifiedStitchedMosaic' - db.delete_table(u'coverages_rectifiedstitchedmosaic') - - # Deleting model 'DatasetSeries' - db.delete_table(u'coverages_datasetseries') - - - models = { - u'backends.dataset': { - 'Meta': {'object_name': 'Dataset'}, - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) - }, - u'coverages.band': { - 'Meta': {'ordering': "('index',)", 'unique_together': "(('index', 'range_type'), ('identifier', 'range_type'))", 'object_name': 'Band'}, - 'color_interpretation': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}), - 'data_type': ('django.db.models.fields.PositiveIntegerField', [], {}), - 'definition': ('django.db.models.fields.CharField', [], {'max_length': '512', 'null': 'True', 'blank': 'True'}), - 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'identifier': ('django.db.models.fields.CharField', [], {'max_length': '512'}), - 'index': ('django.db.models.fields.PositiveSmallIntegerField', [], {}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}), - 'nil_value_set': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['coverages.NilValueSet']", 'null': 'True', 'blank': 'True'}), - 'range_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'bands'", 'to': u"orm['coverages.RangeType']"}), - 'raw_value_max': ('django.db.models.fields.CharField', [], {'max_length': '512', 'null': 'True', 'blank': 'True'}), - 'raw_value_min': ('django.db.models.fields.CharField', [], {'max_length': '512', 'null': 'True', 'blank': 'True'}), - 'uom': ('django.db.models.fields.CharField', [], {'max_length': '64'}) - }, - u'coverages.collection': { - 'Meta': {'object_name': 'Collection', '_ormbases': [u'coverages.EOObject']}, - 'eo_objects': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'collections'", 'symmetrical': 'False', 'through': u"orm['coverages.EOObjectToCollectionThrough']", 'to': u"orm['coverages.EOObject']"}), - u'eoobject_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['coverages.EOObject']", 'unique': 'True', 'primary_key': 'True'}) - }, - u'coverages.coverage': { - 'Meta': {'object_name': 'Coverage', '_ormbases': [u'coverages.EOObject', u'backends.Dataset']}, - u'dataset_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['backends.Dataset']", 'unique': 'True'}), - u'eoobject_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['coverages.EOObject']", 'unique': 'True', 'primary_key': 'True'}), - 'max_x': ('django.db.models.fields.FloatField', [], {}), - 'max_y': ('django.db.models.fields.FloatField', [], {}), - 'min_x': ('django.db.models.fields.FloatField', [], {}), - 'min_y': ('django.db.models.fields.FloatField', [], {}), - 'projection': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['coverages.Projection']", 'null': 'True', 'blank': 'True'}), - 'range_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['coverages.RangeType']"}), - 'size_x': ('django.db.models.fields.PositiveIntegerField', [], {}), - 'size_y': ('django.db.models.fields.PositiveIntegerField', [], {}), - 'srid': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}), - 'visible': ('django.db.models.fields.BooleanField', [], {'default': 'False'}) - }, - u'coverages.datasetseries': { - 'Meta': {'object_name': 'DatasetSeries', '_ormbases': [u'coverages.Collection']}, - u'collection_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['coverages.Collection']", 'unique': 'True', 'primary_key': 'True'}) - }, - u'coverages.datasource': { - 'Meta': {'object_name': 'DataSource', '_ormbases': [u'backends.Dataset']}, - 'collection': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['coverages.Collection']"}), - u'dataset_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['backends.Dataset']", 'unique': 'True', 'primary_key': 'True'}), - 'pattern': ('django.db.models.fields.CharField', [], {'max_length': '32'}) - }, - u'coverages.eoobject': { - 'Meta': {'object_name': 'EOObject'}, - 'begin_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), - 'end_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), - 'footprint': ('django.contrib.gis.db.models.fields.MultiPolygonField', [], {'null': 'True', 'blank': 'True'}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'identifier': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '256'}), - 'real_content_type': ('django.db.models.fields.PositiveSmallIntegerField', [], {}) - }, - u'coverages.eoobjecttocollectionthrough': { - 'Meta': {'unique_together': "(('eo_object', 'collection'),)", 'object_name': 'EOObjectToCollectionThrough'}, - 'collection': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'coverages_set'", 'to': u"orm['coverages.Collection']"}), - 'eo_object': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['coverages.EOObject']"}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) - }, - u'coverages.nilvalue': { - 'Meta': {'object_name': 'NilValue'}, - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'nil_value_set': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'nil_values'", 'to': u"orm['coverages.NilValueSet']"}), - 'raw_value': ('django.db.models.fields.CharField', [], {'max_length': '512'}), - 'reason': ('django.db.models.fields.CharField', [], {'max_length': '512'}) - }, - u'coverages.nilvalueset': { - 'Meta': {'object_name': 'NilValueSet'}, - 'data_type': ('django.db.models.fields.PositiveIntegerField', [], {}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}) - }, - u'coverages.projection': { - 'Meta': {'object_name': 'Projection'}, - 'definition': ('django.db.models.fields.TextField', [], {}), - 'format': ('django.db.models.fields.CharField', [], {'max_length': '16'}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '64'}) - }, - u'coverages.rangetype': { - 'Meta': {'object_name': 'RangeType'}, - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '512'}) - }, - u'coverages.rectifieddataset': { - 'Meta': {'object_name': 'RectifiedDataset', '_ormbases': [u'coverages.Coverage']}, - u'coverage_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['coverages.Coverage']", 'unique': 'True', 'primary_key': 'True'}) - }, - u'coverages.rectifiedstitchedmosaic': { - 'Meta': {'object_name': 'RectifiedStitchedMosaic', '_ormbases': [u'coverages.Coverage', u'coverages.Collection']}, - u'collection_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['coverages.Collection']", 'unique': 'True'}), - u'coverage_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['coverages.Coverage']", 'unique': 'True', 'primary_key': 'True'}) - }, - u'coverages.referenceabledataset': { - 'Meta': {'object_name': 'ReferenceableDataset', '_ormbases': [u'coverages.Coverage']}, - u'coverage_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['coverages.Coverage']", 'unique': 'True', 'primary_key': 'True'}) - }, - u'coverages.reservedid': { - 'Meta': {'object_name': 'ReservedID', '_ormbases': [u'coverages.EOObject']}, - u'eoobject_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['coverages.EOObject']", 'unique': 'True', 'primary_key': 'True'}), - 'request_id': ('django.db.models.fields.CharField', [], {'max_length': '256', 'null': 'True'}), - 'until': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}) - } - } - - complete_apps = ['coverages'] \ No newline at end of file diff -Nru eoxserver-0.4.0beta2/eoxserver/resources/coverages/models.py eoxserver-0.3.2/eoxserver/resources/coverages/models.py --- eoxserver-0.4.0beta2/eoxserver/resources/coverages/models.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/resources/coverages/models.py 2013-12-10 14:57:00.000000000 +0000 @@ -1,9 +1,10 @@ #------------------------------------------------------------------------------- +# $Id$ # # Project: EOxServer -# Authors: Fabian Schindler +# Authors: Stephan Krause # Stephan Meissl -# Stephan Krause +# Martin Paces # #------------------------------------------------------------------------------- # Copyright (C) 2011 EOX IT Services GmbH @@ -11,8 +12,8 @@ # 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 +# 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 @@ -27,738 +28,482 @@ # THE SOFTWARE. #------------------------------------------------------------------------------- +import re import logging -from itertools import chain +from django.core.validators import RegexValidator from django.core.exceptions import ValidationError + from django.contrib.gis.db import models -from django.utils.timezone import now +from django.contrib.gis.geos import Point, LinearRing, Polygon, MultiPolygon +from django.contrib.gis.geos.error import GEOSException -from eoxserver.core import models as base -from eoxserver.contrib import gdal, osr -from eoxserver.backends import models as backends -from eoxserver.resources.coverages.util import ( - detect_circular_reference, collect_eo_metadata, is_same_grid, - parse_raw_value +from eoxserver.contrib import gdal +from eoxserver.core.models import Resource +from eoxserver.backends.models import ( + Location, LocalPath, RemotePath, + RasdamanLocation, CacheFile +) +from eoxserver.resources.coverages.validators import ( + validateEOOM, validateCoverageIDnotInEOOM ) +from eoxserver.resources.coverages.formats import _gerexValMime as regexMIMEType -logger = logging.getLogger(__name__) - - -#=============================================================================== -# Helpers -#=============================================================================== -def iscoverage(eo_object): - """ Helper to check whether an EOObject is a coverage. """ - return issubclass(eo_object.real_type, Coverage) +logger = logging.getLogger(__name__) +MIMETypeValidator = RegexValidator( regexMIMEType , message="The field must contain a valid MIME Type!" ) +NCNameValidator = RegexValidator(re.compile(r'^[a-zA-z_][a-zA-Z0-9_.-]*$'), message="This field must contain a valid NCName.") -def iscollection(eo_object): - """ Helper to check whether an EOObject is a collection. """ - return issubclass(eo_object.real_type, Collection) +class NilValueRecord(models.Model): + reason = models.CharField(max_length=128, default="http://www.opengis.net/def/nil/OGC/0/unknown", + choices=( + ("http://www.opengis.net/def/nil/OGC/0/inapplicable", "Inapplicable (There is no value)"), + ("http://www.opengis.net/def/nil/OGC/0/missing", "Missing"), + ("http://www.opengis.net/def/nil/OGC/0/template", "Template (The value will be available later)"), + ("http://www.opengis.net/def/nil/OGC/0/unknown", "Unknown"), + ("http://www.opengis.net/def/nil/OGC/0/withheld", "Withheld (The value is not divulged)"), + ("http://www.opengis.net/def/nil/OGC/0/AboveDetectionRange", "Above detection range"), + ("http://www.opengis.net/def/nil/OGC/0/BelowDetectionRange", "Below detection range") + ) + ) + value = models.IntegerField() + def __unicode__(self): + return self.reason+" "+str(self.value) -#=============================================================================== -# Metadata classes -#=============================================================================== + class Meta: + verbose_name = "Nil Value" -class Projection(models.Model): - """ Model for elaborate projection definitions. The `definition` is valid - for a given `format`. The `spatial_reference` property returns an - osr.SpatialReference for this Projection. - """ +class BandRecord(models.Model): + name = models.CharField(max_length=256) + identifier = models.CharField(max_length=256) + description = models.TextField() + definition = models.CharField(max_length=256) + uom = models.CharField("UOM", max_length=16) + nil_values = models.ManyToManyField(NilValueRecord, null=True, blank=True, verbose_name="Nil Value") + gdal_interpretation = models.IntegerField("GDAL Interpretation", default=gdal.GCI_Undefined, + choices=gdal.GCI_TO_NAME.items() + ) - name = models.CharField(max_length=64, unique=True) - format = models.CharField(max_length=16) + def __unicode__(self): + return self.name - definition = models.TextField() + class Meta: + verbose_name = "Band" - @property - def spatial_reference(self): - sr = osr.SpatialReference() - # TODO: parse definition - - return sr +class RangeTypeRecord(models.Model): + name = models.CharField(max_length=256) + data_type = models.IntegerField(choices=gdal.GDT_TO_NAME.items()) + bands = models.ManyToManyField(BandRecord, through="RangeType2Band") def __unicode__(self): return self.name - -class Extent(models.Model): - """ Model mix-in for spatial objects which have a 2D Bounding Box expressed - in a projection given either by a SRID or a whole `Projection` object. - """ - - min_x = models.FloatField() - min_y = models.FloatField() - max_x = models.FloatField() - max_y = models.FloatField() - srid = models.PositiveIntegerField(blank=True, null=True) - projection = models.ForeignKey(Projection, blank=True, null=True) - - @property - def spatial_reference(self): - if self.srid is not None: - sr = osr.SpatialReference() - sr.ImportFromEPSG(self.srid) - return sr - else: - return self.projection.spatial_reference + class Meta: + verbose_name = "Range Type" - @property - def extent(self): - """ Returns the extent as a 4-tuple. """ - return self.min_x, self.min_y, self.max_x, self.max_y - - @extent.setter - def extent(self, value): - """ Set the extent as a tuple. """ - self.min_x, self.min_y, self.max_x, self.max_y = value - - def clean(self): - # make sure that neither both nor none of SRID or projections is set - if self.projection is None and self.srid is None: - raise ValidationError("No projection or srid given.") - elif self.projection is not None and self.srid is not None: - raise ValidationError( - "Fields 'projection' and 'srid' are mutually exclusive." - ) +class RangeType2Band(models.Model): + band = models.ForeignKey(BandRecord) + range_type = models.ForeignKey(RangeTypeRecord) + no = models.PositiveIntegerField() + + class Meta: + verbose_name = "Band in Range type" + +class ExtentRecord(models.Model): + srid = models.IntegerField("SRID") + size_x = models.IntegerField() + size_y = models.IntegerField() + minx = models.FloatField() + miny = models.FloatField() + maxx = models.FloatField() + maxy = models.FloatField() + def __unicode__(self): + return "Extent (SRID=%d; %f, %f, %f, %f; Size: %d x %d)" % ( + self.srid, self.minx, self.miny, self.maxx, self.maxy, self.size_x, self.size_y + ) + class Meta: - abstract = True + verbose_name = "Extent" +class LayerMetadataRecord(models.Model): + key = models.CharField(max_length=256) + value = models.TextField() -class EOMetadata(models.Model): - """ Model mix-in for objects that have EO metadata (timespan and footprint) - associated. - """ - - begin_time = models.DateTimeField(null=True, blank=True) - end_time = models.DateTimeField(null=True, blank=True) - footprint = models.MultiPolygonField(null=True, blank=True) - - #objects = models.GeoManager() - - @property - def extent_wgs84(self): - if self.footprint is None: - return None - return self.footprint.extent - - @property - def time_extent(self): - return self.begin_time, self.end_time + def __unicode__(self): + return self.key class Meta: - abstract = True + verbose_name = "Layer Metadata" + verbose_name_plural = "Layer Metadata" +class LineageRecord(models.Model): -class DataSource(backends.Dataset): - pattern = models.CharField(max_length=32, null=False, blank=False) - collection = models.ForeignKey("Collection") - - -#=============================================================================== -# Base class EOObject -#=============================================================================== - - -# registry to map the integer type IDs to the model types and vice-versa. -EO_OBJECT_TYPE_REGISTRY = {} - - -class EOObject(base.Castable, EOMetadata): - """ Base class for EO objects. All EO objects share a pool of unique - `identifiers`. - """ - - identifier = models.CharField(max_length=256, unique=True, null=False, blank=False) - - # this field is required to be named 'real_content_type' - real_content_type = models.PositiveSmallIntegerField() - type_registry = EO_OBJECT_TYPE_REGISTRY - + class Meta: + verbose_name = "Lineage Entry" + verbose_name_plural = "Lineage Entries" +class EOMetadataRecord(models.Model): + timestamp_begin = models.DateTimeField("Begin of acquisition") + timestamp_end = models.DateTimeField("End of acquisition") + footprint = models.MultiPolygonField(srid=4326) + eo_gml = models.TextField("EO O&M", blank=True, validators=[validateEOOM]) # validate against schema objects = models.GeoManager() - - def __init__(self, *args, **kwargs): - # TODO: encapsulate the change-tracking - super(EOObject, self).__init__(*args, **kwargs) - self._original_begin_time = self.begin_time - self._original_end_time = self.end_time - self._original_footprint = self.footprint - - - def save(self, *args, **kwargs): - super(EOObject, self).save(*args, **kwargs) - - # propagate changes of the EO Metadata up in the collection hierarchy - if (self._original_begin_time != self.begin_time - or self._original_end_time != self.end_time - or self._original_footprint != self.footprint): - - for collection in self.collections.all(): - collection.update_eo_metadata() - - # set the new values for subsequent calls to `save()` - self._original_begin_time = self.begin_time - self._original_end_time = self.end_time - self._original_footprint = self.footprint - - - def __unicode__(self): - return "%s (%s)" % (self.identifier, self.real_type._meta.verbose_name) - - class Meta: - verbose_name = "EO Object" - verbose_name_plural = "EO Objects" - -#=============================================================================== -# Identifier reservation -#=============================================================================== - -class ReservedIDManager(models.Manager): - """ Model manager for `ReservedID` models for easier handling. Returns only - `QuerySets` that contain valid reservations. - """ - def get_original_queryset(self): - return super(ReservedIDManager, self).get_queryset() - - def get_queryset(self): - Q = models.Q - self.get_original_queryset().filter( - Q(until__isnull=True) | Q(until__gt=now()) - ) - - def cleanup_reservations(self): - Q = models.Q - self.get_original_queryset().filter( - Q(until__isnull=False) | Q(until__lte=now()) - ).delete() - - def remove_reservation(self, identifier=None, request_id=None): - if not identifier and not request_id: - raise ValueError("Either identifier or request ID required") - - if identifier: - model = self.get_original_queryset().get(identifier=identifier) - if request_id and model.request_id != request_id: - raise ValueError( - "Given request ID does not match the reservation." - ) - else: - model = self.get_original_queryset().get(request_id=request_id) - model.delete() + verbose_name = "EO Metadata Entry" + verbose_name_plural = "EO Metadata Entries" + def __unicode__(self): + return ("BeginTime: %s" % self.timestamp_begin) + + + def save(self, *args, **kwargs): + from eoxserver.core.util.timetools import UTCOffsetTimeZoneInfo + if self.timestamp_begin.tzinfo is None: + dt = self.timestamp_begin.replace(tzinfo=UTCOffsetTimeZoneInfo()) + self.timestamp_begin = dt.astimezone(UTCOffsetTimeZoneInfo()) + + if self.timestamp_end.tzinfo is None: + dt = self.timestamp_end.replace(tzinfo=UTCOffsetTimeZoneInfo()) + self.timestamp_end = dt.astimezone(UTCOffsetTimeZoneInfo()) + models.Model.save(self, *args, **kwargs) + + def clean(self): + pass -class ReservedID(EOObject): - """ Model to reserve a specific ID. The field `until` can be used to - specify the end of the reservation. +class DataSource(models.Model): # Maybe make two sub models for local and remote storages. """ - until = models.DateTimeField(null=True) - request_id = models.CharField(max_length=256, null=True) - - objects = ReservedIDManager() - - -EO_OBJECT_TYPE_REGISTRY[0] = ReservedID - -#=============================================================================== -# RangeType structure -#=============================================================================== - - -class NilValueSet(models.Model): - """ Collection model for nil values. + """ + location = models.ForeignKey(Location, related_name="data_sources") + search_pattern = models.CharField(max_length=1024, null=True) + + class Meta: + unique_together = ('location', 'search_pattern') + + def __unicode__(self): + return "%s: %s" % (str(self.location), self.search_pattern) - name = models.CharField(max_length=512) - data_type = models.PositiveIntegerField() - - def __init__(self, *args, **kwargs): - super(NilValueSet, self).__init__(*args, **kwargs) - self._cached_nil_values = None - - @property - def values(self): - return [nil_value.value for nil_value in self] - +class DataPackage(models.Model): + data_package_type = models.CharField(max_length=32, editable=False) + metadata_format_name = models.CharField(max_length=128, null=True, blank=True) + def __unicode__(self): - return "%s (%s)" % (self.name, gdal.GetDataTypeName(self.data_type)) + if self.data_package_type == "local": + return "Local Data Package: %s / %s" % ( + self.localdatapackage.data_location, + self.localdatapackage.metadata_location, + ) + + elif self.data_package_type == "remote": + return "Remote Data Package: %s / %s" % ( + self.remotedatapackage.data_location, + self.remotedatapackage.metadata_location, + ) + elif self.data_package_type == "rasdaman": + return "Rasdaman Data Package: %s / %s" % ( + self.rasdamandatapackage.data_location, + self.rasdamandatapackage.metadata_location, + ) + else: + return "Unknown location type" - @property - def cached_nil_values(self): - if self._cached_nil_values is None: - self._cached_nil_values = list(self.nil_values.all()) - return self._cached_nil_values + +class LocalDataPackage(DataPackage): + DATA_PACKAGE_TYPE = "local" + + data_location = models.ForeignKey(LocalPath, related_name="data_file_packages") + metadata_location = models.ForeignKey(LocalPath, related_name="metadata_file_packages", null=True) + source_format = models.CharField( max_length=64, null=False, blank=False, validators = [ MIMETypeValidator ] ) - def __iter__(self): - return iter(self.cached_nil_values) +class RemoteDataPackage(DataPackage): + DATA_PACKAGE_TYPE = "remote" + + data_location = models.ForeignKey(RemotePath, related_name="data_file_packages") + metadata_location = models.ForeignKey(RemotePath, related_name="metadata_file_packages", null=True) + source_format = models.CharField( max_length=64, null=False, blank=False, validators = [ MIMETypeValidator ] ) - def __len__(self): - return len(self.cached_nil_values) + cache_file = models.ForeignKey(CacheFile, related_name="remote_data_packages", null=True) + + def delete(self): + cache_file = self.cache_file + super(RemoteDataPackage, self).delete() + cache_file.delete() - def __getitem__(self, index): - return self.cached_nil_values[index] +class RasdamanDataPackage(DataPackage): + DATA_PACKAGE_TYPE = "rasdaman" + + data_location = models.ForeignKey(RasdamanLocation, related_name="data_packages") + metadata_location = models.ForeignKey(LocalPath, related_name="rasdaman_metadata_file_packages", null=True) +class TileIndex(models.Model): + storage_dir = models.CharField(max_length=1024) + class Meta: - verbose_name = "Nil Value Set" + verbose_name = "Tile Index" + verbose_name_plural = "Tile Indices" +class ReservedCoverageIdRecord(models.Model): + until = models.DateTimeField() + request_id = models.CharField(max_length=256, null=True) + coverage_id = models.CharField("Coverage ID", max_length=256, unique=True, validators=[NCNameValidator]) -NIL_VALUE_CHOICES = ( - ("http://www.opengis.net/def/nil/OGC/0/inapplicable", "Inapplicable (There is no value)"), - ("http://www.opengis.net/def/nil/OGC/0/missing", "Missing"), - ("http://www.opengis.net/def/nil/OGC/0/template", "Template (The value will be available later)"), - ("http://www.opengis.net/def/nil/OGC/0/unknown", "Unknown"), - ("http://www.opengis.net/def/nil/OGC/0/withheld", "Withheld (The value is not divulged)"), - ("http://www.opengis.net/def/nil/OGC/0/AboveDetectionRange", "Above detection range"), - ("http://www.opengis.net/def/nil/OGC/0/BelowDetectionRange", "Below detection range") -) +class CoverageRecord(Resource): + coverage_id = models.CharField("Coverage ID", max_length=256, unique=True, validators=[NCNameValidator]) -class NilValue(models.Model): - """ Single nil value contributing to a nil value set. - """ + range_type = models.ForeignKey(RangeTypeRecord, on_delete=models.PROTECT) + layer_metadata = models.ManyToManyField(LayerMetadataRecord, null=True, blank=True) + automatic = models.BooleanField(default=False) # True means that the dataset was automatically generated from a dataset series's data dir + data_source = models.ForeignKey(DataSource, related_name="%(class)s_set", null=True, blank=True, on_delete=models.SET_NULL) # Has to be set if automatic is true. - raw_value = models.CharField(max_length=512, help_text="The string representation of the nil value.") - reason = models.CharField(max_length=512, null=False, blank=False, choices=NIL_VALUE_CHOICES, help_text="A string identifier (commonly a URI or URL) for the reason of this nil value.") + def clean(self): + super(CoverageRecord, self).clean() + if self.automatic and self.data_source is None: + raise ValidationError('DataSource has to be set if automatic is true.') - nil_value_set = models.ForeignKey(NilValueSet, related_name="nil_values") - def __unicode__(self): - return "%s (%s)" % (self.reason, self.raw_value) - - @property - def value(self): - """ Get the parsed python value from the saved value string. - """ - return parse_raw_value(self.raw_value, self.nil_value_set.data_type) + return self.coverage_id - def clean(self): - """ Check that the value can be parsed. - """ - try: - _ = self.value - except Exception, e: - raise ValidationError(str(e)) +class PlainCoverageRecord(CoverageRecord): + extent = models.ForeignKey(ExtentRecord, related_name = "single_file_coverages") + data_package = models.ForeignKey(DataPackage, related_name="plain_coverages") class Meta: - verbose_name = "Nil Value" - - -class RangeType(models.Model): - """ Collection model for bands. - """ - - name = models.CharField(max_length=512, null=False, blank=False, unique=True) - - - def __init__(self, *args, **kwargs): - super(RangeType, self).__init__(*args, **kwargs) - self._cached_bands = None - - def __unicode__(self): - return self.name - - @property - def cached_bands(self): - if self._cached_bands is None: - self._cached_bands = list(self.bands.all()) - return self._cached_bands - - def __iter__(self): - return iter(self.cached_bands) - - def __len__(self): - return len(self.cached_bands) - - def __getitem__(self, index): - return self.cached_bands[index] - + verbose_name = "Single File Coverage" + verbose_name_plural = "Single File Coverages" + + def delete(self): + extent = self.extent + super(PlainCoverageRecord, self).delete() + extent.delete() + +class EOCoverageMixIn(models.Model): + eo_id = models.CharField("EO ID", max_length=256, unique=True, validators=[NCNameValidator]) + eo_metadata = models.OneToOneField(EOMetadataRecord, + related_name="%(class)s_set", + verbose_name="EO Metadata Entry") + lineage = models.OneToOneField(LineageRecord, related_name="%(class)s_set") + class Meta: - verbose_name = "Range Type" - - -class Band(models.Model): - """ Model for storing band related metadata. - """ - - index = models.PositiveSmallIntegerField() - name = models.CharField(max_length=512, null=False, blank=False) - identifier = models.CharField(max_length=512, null=False, blank=False) - description = models.TextField(null=True, blank=True) - definition = models.CharField(max_length=512, null=True, blank=True) - uom = models.CharField(max_length=64, null=False, blank=False) - - # GDAL specific - data_type = models.PositiveIntegerField() - color_interpretation = models.PositiveIntegerField(null=True, blank=True) - - raw_value_min = models.CharField(max_length=512, null=True, blank=True, help_text="The string representation of the minimum value.") - raw_value_max = models.CharField(max_length=512, null=True, blank=True, help_text="The string representation of the maximum value.") - - range_type = models.ForeignKey(RangeType, related_name="bands", null=False, blank=False) - nil_value_set = models.ForeignKey(NilValueSet, null=True, blank=True) - - def clean(self): - nil_value_set = self.nil_value_set - if nil_value_set and nil_value_set.data_type != self.data_type: - raise ValidationError( - "The data type of the band is not equal to the data type of " - "its nil value set." - ) - - min_ = parse_raw_value(self.raw_value_min, self.data_type) - max_ = parse_raw_value(self.raw_value_min, self.data_type) - - if min_ is not None and max_ is not None and min_ > max_: - raise ValidationError("Minimum value larger than maximum value") + abstract = True - class Meta: - ordering = ('index',) - unique_together = (('index', 'range_type'), ('identifier', 'range_type')) + def delete(self): + eo_metadata = self.eo_metadata + lineage = self.lineage + super(EOCoverageMixIn, self).delete() + eo_metadata.delete() + lineage.delete() +class EODatasetMixIn(EOCoverageMixIn): + data_package = models.ForeignKey(DataPackage, related_name="%(class)s_set") + visible = models.BooleanField(default=False) # True means that the dataset is visible in the GetCapabilities response def __unicode__(self): - return "%s (%s)" % (self.name, gdal.GetDataTypeName(self.data_type)) - - @property - def allowed_values(self): - dt = self.data_type - min_ = parse_raw_value(self.raw_value_min, dt) - max_ = parse_raw_value(self.raw_value_max, dt) - limits = gdal.GDT_NUMERIC_LIMITS[dt] - - return ( - min_ if min_ is not None else limits[0], - max_ if max_ is not None else limits[1], - ) - - @property - def significant_figures(self): - return gdal.GDT_SIGNIFICANT_FIGURES[self.data_type] - + return self.eo_id -#=============================================================================== -# Base classes for Coverages and Collections -#=============================================================================== - - -class Coverage(EOObject, Extent, backends.Dataset): - """ Common base model for all coverage types. - """ - - size_x = models.PositiveIntegerField() - size_y = models.PositiveIntegerField() + class Meta: + abstract=True - range_type = models.ForeignKey(RangeType) - - visible = models.BooleanField(default=False) # True means that the dataset is visible in the GetCapabilities response - - @property - def size(self): - return self.size_x, self.size_y - - @size.setter - def size(self, value): - self.size_x, self.size_y = value - - @property - def resolution_x(self): - return (self.max_x - self.min_x) / float(self.size_x) - - @property - def resolution_y(self): - return (self.max_y - self.min_y) / float(self.size_y) - - @property - def resolution(self): - return (self.resolution_x, self.resolution_y) - - objects = models.GeoManager() + def delete(self): + data_package = self.data_package + super(EODatasetMixIn, self).delete() + data_package.delete() + +def _checkFootprint(footprint, extent): + """ Check footprint to match the extent. """ - -class Collection(EOObject): - """ Base model for all collections. - """ - - eo_objects = models.ManyToManyField(EOObject, through="EOObjectToCollectionThrough", related_name="collections") - - objects = models.GeoManager() - - def insert(self, eo_object, through=None): - # TODO: a collection shall not contain itself! - if self.pk == eo_object.pk: - raise ValidationError("A collection cannot contain itself.") - - if through is None: - # was not invoked by the through model, so create it first. - # insert will be invoked again in the `through.save()` method. - logger.debug("Creating relation model for %s and %s." % (self, eo_object)) - through = EOObjectToCollectionThrough(eo_object=eo_object, collection=self) - through.full_clean() - through.save() - return - - logger.debug("Inserting %s into %s." % (eo_object, self)) - - # cast self to actual collection type - self.cast().perform_insertion(eo_object, through) - - - def perform_insertion(self, eo_object, through=None): - """Interface method for collection insertions. If the insertion is not - possible, raise an exception. - EO metadata collection needs to be done here as-well! - """ - - raise ValidationError("Collection %s cannot insert %s" % (str(self), str(eo_object))) - - - def remove(self, eo_object, through=None): - if through is None: - EOObjectToCollectionThrough.objects.get(eo_object=eo_object, collection=self).delete() - return - - logger.debug("Removing %s from %s." % (eo_object, self)) + #TODO: Make the rtol value configurable. + # allow footprint to exceed extent by given % of smaller extent size + rtol = 0.005 # .5% + difx = abs(extent.maxx - extent.minx) + dify = abs(extent.maxy - extent.miny) + atol = rtol * min(difx, dify) + + try: + bbox = Polygon.from_bbox((extent.minx, extent.miny, extent.maxx, extent.maxy)) + bbox.srid = int(extent.srid) - # call actual remove method on actual collection type - self.cast().perform_removal(eo_object) - - - def perform_removal(self, eo_object): - """ Interface method for collection removals. Update of EO-metadata needs - to be performed here. Abortion of removal is not possible (atm). - """ - raise NotImplementedError - - - def update_eo_metadata(self): - logger.debug("Updating EO Metadata for %s." % self) - self.begin_time, self.end_time, self.footprint = collect_eo_metadata(self.eo_objects.all()) - self.full_clean() - self.save() - - # containment methods - - def contains(self, eo_object, recursive=False): - """ Check if an EO object is contained in a collection or subcollection, - if `recursive` is set to `True`. - """ - - if not isinstance(eo_object, EOObject): - raise ValueError("Expected EOObject.") - - if self.eo_objects.filter(pk=eo_object.pk).exists(): - return True - - if recursive: - for collection in self.eo_objects.filter(collection__isnull=False): - collection = collection.cast() - if collection.contains(eo_object, recursive): - return True - - return False - - - def __contains__(self, eo_object): - """ Shorthand for non-recursive `contains()` method. """ - return self.contains(eo_object) - - def __iter__(self): - return iter(self.eo_objects.all()) - - def iter_cast(self, recursive=False): - for eo_object in self.eo_objects.all(): - eo_object = eo_object.cast() - yield eo_object - if recursive and iscollection(eo_object): - for item in eo_object.iter_cast(recursive): - yield item - - def __len__(self): - if self.id == None: - return 0 - return self.eo_objects.count() - - -class EOObjectToCollectionThrough(models.Model): - """Relation of objects to collections. - Warning: do *not* use bulk methods of query sets of this collection, as it - will not invoke the correct `insert` and `remove` methods on the collection. - """ - - eo_object = models.ForeignKey(EOObject) - collection = models.ForeignKey(Collection, related_name="coverages_set") - - objects = models.GeoManager() - - - def __init__(self, *args, **kwargs): - super(EOObjectToCollectionThrough, self).__init__(*args, **kwargs) - try: - self._original_eo_object = self.eo_object - except: - self._original_eo_object = None - - try: - self._original_collection = self.collection - except: - self._original_collection = None - - - def save(self, *args, **kwargs): - if (self._original_eo_object is not None - and self._original_collection is not None - and (self._original_eo_object != self.eo_object - or self._original_collection != self.collection)): - logger.debug("Relation has been altered!") - self._original_collection.remove(self._original_eo_object, self) - - def getter(eo_object): - return eo_object.collections.all() - - if detect_circular_reference(self.eo_object, self.collection, getter): - raise ValidationError("Circular reference detected.") - - # perform the insertion - # TODO: this is a bit buggy, as the insertion cannot be aborted this way - # but if the insertion is *before* the save, then EO metadata collecting - # still handles previously removed ones. - self.collection.insert(self.eo_object, self) - - super(EOObjectToCollectionThrough, self).save(*args, **kwargs) - - self._original_eo_object = self.eo_object - self._original_collection = self.collection - - - def delete(self, *args, **kwargs): - # TODO: pre-remove method? (maybe to cancel remove?) - logger.debug("Deleting relation model between for %s and %s." % (self.collection, self.eo_object)) - result = super(EOObjectToCollectionThrough, self).delete(*args, **kwargs) - self.collection.remove(self.eo_object, self) - return result - - - class Meta: - unique_together = (("eo_object", "collection"),) - verbose_name = "EO Object to Collection Relation" - verbose_name_plural = "EO Object to Collection Relations" - - -#=============================================================================== -# Actual Coverage and Collections -#=============================================================================== + bbox_ll = bbox.transform(footprint.srs, clone=True) + + normalized_space = Polygon.from_bbox((-180, -90, 180, 90)) + non_normalized_space = Polygon.from_bbox((180, -90, 360, 90)) + + normalized_space.srid = int(extent.srid) + non_normalized_space.srid = int(extent.srid) + + if not normalized_space.contains(bbox_ll): + # create 2 bboxes for each side of the date line + bbox_ll1 = bbox_ll.intersection(normalized_space) + bbox_ll2 = bbox_ll.intersection(non_normalized_space) + + bbox_ll2 = Polygon(LinearRing([Point(x - 360, y) for x, y in bbox_ll2.exterior_ring]), srid=bbox_ll2.srid) + + bbox_ll1.transform(extent.srid) + bbox_ll2.transform(extent.srid) + + e1 = bbox_ll1.extent + e2 = bbox_ll2.extent + + bbox = MultiPolygon( + Polygon.from_bbox((e1[0] - atol, e2[1] - atol, e1[2] + atol, e1[3] + atol)), + Polygon.from_bbox((e2[0] - atol, e2[1] - atol, e2[2] + atol, e2[3] + atol)), + srid=extent.srid + ) + else: + # just use the tolerance for a slightly larger bbox + bbox = Polygon.from_bbox((extent.minx - atol, extent.miny - atol, + extent.maxx + atol, extent.maxy + atol)) + bbox.srid = int(extent.srid) + + if footprint.srid != bbox.srid: + footprint_bboxsrs = footprint.transform(bbox.srs, clone=True) + else: + footprint_bboxsrs = footprint + + logger.debug("Extent: %s" % bbox.wkt) + logger.debug("Footprint: %s" % footprint_bboxsrs.wkt) + + if not bbox.contains(footprint_bboxsrs): + raise ValidationError("The datasets's extent does not surround its" + " footprint. Extent: '%s' Footprint: '%s'." + % (bbox.wkt, footprint_bboxsrs.wkt) + ) + + except GEOSException: + pass -class RectifiedDataset(Coverage): - """ Coverage type using a rectified grid. - """ - - objects = models.GeoManager() +class RectifiedDatasetRecord(CoverageRecord, EODatasetMixIn): + extent = models.ForeignKey(ExtentRecord, related_name="rect_datasets") class Meta: verbose_name = "Rectified Dataset" verbose_name_plural = "Rectified Datasets" - -EO_OBJECT_TYPE_REGISTRY[10] = RectifiedDataset - - -class ReferenceableDataset(Coverage): - """ Coverage type using a referenceable grid. - """ - objects = models.GeoManager() + def clean(self): + super(RectifiedDatasetRecord, self).clean() + + # TODO: this does not work in the admins changelist.save method + # A wrong WKB is inside the eo_metadata.footprint entry + _checkFootprint( self.eo_metadata.footprint , self.extent ) + + validateCoverageIDnotInEOOM(self.coverage_id, self.eo_metadata.eo_gml) + + def delete(self): + extent = self.extent + super(RectifiedDatasetRecord, self).delete() + extent.delete() + +class ReferenceableDatasetRecord(CoverageRecord, EODatasetMixIn): + extent = models.ForeignKey(ExtentRecord, related_name="refa_datasets") class Meta: verbose_name = "Referenceable Dataset" verbose_name_plural = "Referenceable Datasets" + + def clean(self): + super(ReferenceableDatasetRecord, self).clean() + + # TODO: taken from Rectified DS, check if applicable to Referenceable DS too + # TODO: this does not work in the admins changelist.save method + # A wrong WKB is inside the eo_metadata.footprint entry + _checkFootprint( self.eo_metadata.footprint , self.extent ) + + validateCoverageIDnotInEOOM(self.coverage_id, self.eo_metadata.eo_gml) + + def delete(self): + extent = self.extent + super(EOCoverageMixIn, self).delete() + extent.delete() + +class RectifiedStitchedMosaicRecord(CoverageRecord, EOCoverageMixIn): + extent = models.ForeignKey(ExtentRecord, related_name="rect_stitched_mosaics") + data_sources = models.ManyToManyField(DataSource, + null=True, blank=True, + related_name="rect_stitched_mosaics") + tile_index = models.ForeignKey(TileIndex, related_name="rect_stitched_mosaics") + rect_datasets = models.ManyToManyField(RectifiedDatasetRecord, + null=True, blank=True, + related_name="rect_stitched_mosaics", + verbose_name="Rectified Dataset(s)") -EO_OBJECT_TYPE_REGISTRY[11] = ReferenceableDataset - + def __unicode__(self): + return self.eo_id -class RectifiedStitchedMosaic(Coverage, Collection): - """ Collection type which can entail rectified datasets that share a common - range type and are on the same grid. - """ - - objects = models.GeoManager() - class Meta: - verbose_name = "Rectified Stitched Mosaic" - verbose_name_plural = "Rectified Stitched Mosaics" - - def perform_insertion(self, eo_object, through=None): - if eo_object.real_type != RectifiedDataset: - raise ValidationError("In a %s only %s can be inserted." % ( - RectifiedStitchedMosaic._meta.verbose_name, - RectifiedDataset._meta.verbose_name_plural - )) - - rectified_dataset = eo_object.cast() - if self.range_type != rectified_dataset.range_type: - raise ValidationError( - "Dataset '%s' has a different Range Type as the Rectified " - "Stitched Mosaic '%s'." % (rectified_dataset, self.identifier) - ) - - if not is_same_grid((self, rectified_dataset)): + verbose_name = "Stitched Mosaic" + verbose_name_plural = "Stitched Mosaics" + + def clean(self): + super(RectifiedStitchedMosaicRecord, self).clean() + + footprint = self.eo_metadata.footprint + bbox = Polygon.from_bbox((self.extent.minx, self.extent.miny, + self.extent.maxx, self.extent.maxy)) + bbox.set_srid(int(self.extent.srid)) + + if footprint.srid != bbox.srid: + footprint.transform(bbox.srs) + + if not bbox.contains(footprint): raise ValidationError( - "Dataset '%s' has not the same base grid as the Rectified " - "Stitched Mosaic '%s'." % (rectified_dataset, self.identifier) + "Extent does not surround footprint. Extent: '%s' Footprint: " + "'%s'" % (str(bbox), str(footprint)) ) + + validateCoverageIDnotInEOOM(self.coverage_id, self.eo_metadata.eo_gml) - self.begin_time, self.end_time, self.footprint = collect_eo_metadata( - self.eo_objects.all(), insert=[eo_object] - ) - # TODO: recalculate size and extent! - self.full_clean() - self.save() - return - - def perform_removal(self, eo_object): - self.begin_time, self.end_time, self.footprint = collect_eo_metadata( - self.eo_objects.all(), exclude=[eo_object] - ) - # TODO: recalculate size and extent! - self.full_clean() - self.save() - return - -EO_OBJECT_TYPE_REGISTRY[20] = RectifiedStitchedMosaic - - -class DatasetSeries(Collection): - """ Collection type that can entail any type of EO object, even other - collections. - """ + def delete(self): + tile_index = self.tile_index + # TODO maybe delete only automatic datasets? + for dataset in self.rect_datasets.all(): + dataset.delete() + super(RectifiedStitchedMosaicRecord, self).delete() + tile_index.delete() + +class DatasetSeriesRecord(Resource): + eo_id = models.CharField("EO ID", max_length=256, unique=True, validators=[NCNameValidator]) + eo_metadata = models.OneToOneField(EOMetadataRecord, + related_name="dataset_series_set", + verbose_name="EO Metadata Entry") + data_sources = models.ManyToManyField(DataSource, + null=True, blank=True, + related_name="dataset_series_set") + rect_stitched_mosaics = models.ManyToManyField(RectifiedStitchedMosaicRecord, + blank=True, null=True, + related_name="dataset_series_set", + verbose_name="Stitched Mosaic(s)") + rect_datasets = models.ManyToManyField(RectifiedDatasetRecord, + blank=True, null=True, + related_name="dataset_series_set", + verbose_name="Rectified Dataset(s)") + ref_datasets = models.ManyToManyField(ReferenceableDatasetRecord, + blank=True, null=True, + related_name="dataset_series_set", + verbose_name="Referenceable Dataset(s)") + + layer_metadata = models.ManyToManyField(LayerMetadataRecord, null=True, blank=True) - objects = models.GeoManager() + def __unicode__(self): + return self.eo_id class Meta: verbose_name = "Dataset Series" verbose_name_plural = "Dataset Series" - - def perform_insertion(self, eo_object, through=None): - self.begin_time, self.end_time, self.footprint = collect_eo_metadata( - self.eo_objects.all(), insert=[eo_object], bbox=True - ) - self.full_clean() - self.save() - return - - def perform_removal(self, eo_object): - self.begin_time, self.end_time, self.footprint = collect_eo_metadata( - self.eo_objects.all(), exclude=[eo_object], bbox=True - ) - self.full_clean() - self.save() - return - -EO_OBJECT_TYPE_REGISTRY[30] = DatasetSeries + def delete(self): + eo_metadata = self.eo_metadata + for dataset in self.rect_datasets.filter(automatic=True): + if dataset.dataset_series_set.count() == 1 and \ + dataset.rect_stitched_mosaics.count() == 0: + dataset.delete() + for dataset in self.ref_datasets.filter(automatic=True): + if dataset.dataset_series_set.count() == 1: + dataset.delete() + super(DatasetSeriesRecord, self).delete() + eo_metadata.delete() diff -Nru eoxserver-0.4.0beta2/eoxserver/resources/coverages/rangetype.py eoxserver-0.3.2/eoxserver/resources/coverages/rangetype.py --- eoxserver-0.4.0beta2/eoxserver/resources/coverages/rangetype.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/resources/coverages/rangetype.py 2013-12-10 14:57:00.000000000 +0000 @@ -1,4 +1,5 @@ #------------------------------------------------------------------------------- +# $Id$ # # Project: EOxServer # Authors: Stephan Krause @@ -11,8 +12,8 @@ # 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 +# 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 @@ -30,266 +31,268 @@ from django.db import transaction from eoxserver.contrib import gdal -#TODO: get rid of the W* wrapper classes -#class WRangeType(object): -# """ -# RangeType contains range type information of a coverage. The -# constructor accepts the mandatory ``name`` and ``data_type`` -# parameters as well as an optional ``bands`` parameter. If no bands -# are specified they shall be added with :meth:`addBands`. -# """ -# -# def __init__(self, name, bands=[] ): -# self.name = name -# self.bands = bands -# -# def __eq__(self, other): -# if (self.name != other.name -# or self.bands != other.bands): -# return False -# return True -# -# def addBand(self, band): -# "Append a new band to the band list." -# self.bands.append(band) -# -# def asDict( self ): -# """ return object as a tupe to be passed to JSON serializer """ - -# bands = [ band.asDict() for band in self.bands ] - -# return { -# "name" : self.name, -# "bands" : bands -# } - - -#class WBand(object): -# """\ -# Band represents a band configuration. -# -# The ``data_type`` parameter may be set to one of the following -# constants defined in :mod:`osgeo.gdalconst`: -# -# * ``GDT_Byte`` -# * ``GDT_UInt16`` -# * ``GDT_Int16`` -# * ``GDT_UInt32`` -# * ``GDT_Int32`` -# * ``GDT_Float32`` -# * ``GDT_Float64`` -# * ``GDT_CInt16`` -# * ``GDT_CInt32`` -# * ``GDT_CFloat32`` -# * ``GDT_CFloat64`` -# -# The ``color_interpretation`` parameter contains the GDAL -# BandInterpretation value which may be assigned to a band. It may -# be set to one of the following constants defined in -# :mod:`osgeo.gdalconst`: -# -# * ``GCI_Undefined`` -# * ``GCI_GrayIndex`` -# * ``GCI_PaletteIndex`` -# * ``GCI_RedBand`` -# * ``GCI_GreenBand`` -# * ``GCI_BlueBand`` -# * ``GCI_AlphaBand`` -# * ``GCI_HueBand`` -# * ``GCI_SaturationBand`` -# * ``GCI_LightnessBand`` -# * ``GCI_CyanBand`` -# * ``GCI_MagentaBand`` -# * ``GCI_YellowBand`` -# * ``GCI_BlackBand`` -# -# It defaults to ``GCI_Undefined``. -# """ - -# def __init__(self, -# name, -# data_type, -# identifier='', -# description='', -# definition='http://opengis.net/def/property/OGC/0/Radiance', -# nil_values=[], -# uom='W.m-2.sr-1.nm-1', -# color_interpretation=gdal.GCI_Undefined -# ): -# self.name = name -# self.data_type = data_type -# self.identifier = identifier -# self.description = description -# self.definition = definition -# self.nil_values = nil_values -# self.uom = uom -# self.color_interpretation = color_interpretation - -# def __eq__(self, other): -# if (self.name != other.name -# or self.data_type != data_type -# or self.identifier != other.identifier -# or self.description != other.description -# or self.definition != other.definition -# or self.nil_values != other.nil_values -# or self.uom != other.uom -# or self.color_interpretation != other.color_interpretation): -# return False -# return True - -# def addNilValue(self, nil_value ): -# "Append a new nil-value to the band list." -# self.nil_values.append(nil_value) - -# def asDict( self ): -# """ -# Return object's data as a dictionary to be passed to a JSON serializer. -# """ - -# return { -# "name" : self.name, -# "data_type" : self.getDataTypeAsString(), -# "identifier" : self.identifier, -# "description" : self.description, -# "definition" : self.definition, -# "uom" : self.uom, -# "nil_values" : [ nv.asDict() for nv in self.nil_values ], -# "color_interpretation" : self.getColorInterpretationAsString() -# } -# -# def getDataTypeAsString( self ) : -# "Return string representation of the ``data_type``." -# return gdal.GDT_TO_NAME.get( self.data_type, "Invalid" ) -# -# def getColorInterpretationAsString( self ) : -# "Return string representation of the ``color_interpretation``." -# return gdal.GCI_TO_NAME.get( self.color_interpretation , "Invalid" ) - -# def getSignificantFigures(self): -# "Get significant figures of the currently used type." -# dt = self.data_type -# if dt == gdal.GDT_Byte: -# return 3 -# elif dt in ( gdal.GDT_UInt16 , gdal.GDT_Int16 , gdal.GDT_CInt16 ) : -# return 5 -# elif dt in ( gdal.GDT_UInt32 , gdal.GDT_Int32 , gdal.GDT_CInt32 ) : -# return 10 -# elif dt in ( gdal.GDT_Float32 , gdal.GDT_CFloat32 ) : -# return 38 -# elif dt in ( gdal.GDT_Float64 , gdal.GDT_CFloat64 ) : -# return 308 -# else: -# raise NotImplementedError() -# -# def getAllowedValues(self): -# "Get interval bounds of the currently used type." -# dt = self.data_type -# if dt == gdal.GDT_Byte: -# return (0, 255) -# elif dt == gdal.GDT_UInt16: -# return (0, 65535) -# elif dt in ( gdal.GDT_Int16 , gdal.GDT_CInt32 ) : -# return (-32768, 32767) -# elif dt == gdal.GDT_UInt32: -# return (0, 4294967295) -# elif dt in ( gdal.GDT_Int32 , gdal.GDT_CInt32 ) : -# return (-2147483648, 2147483647) -# elif dt in ( gdal.GDT_Float32 , gdal.GDT_CFloat32 ) : -# return (-3.40282e+38, 3.40282e+38) -# elif dt in ( gdal.GDT_Float64 , gdal.GDT_CFloat64 ) : -# return (-1.7976931348623157e+308, 1.7976931348623157e+308) -# else: -# raise NotImplementedError() - - -#class WNilValue(object): -# """ -# This class represents nil values of a coverage band. -# -# The constructor accepts the nil value itself and a reason. The -# reason shall be one of: -# -# * ``http://www.opengis.net/def/nil/OGC/0/inapplicable`` -# * ``http://www.opengis.net/def/nil/OGC/0/missing`` -# * ``http://www.opengis.net/def/nil/OGC/0/template`` -# * ``http://www.opengis.net/def/nil/OGC/0/unknown`` -# * ``http://www.opengis.net/def/nil/OGC/0/withheld`` -# * ``http://www.opengis.net/def/nil/OGC/0/AboveDetectionRange`` -# * ``http://www.opengis.net/def/nil/OGC/0/BelowDetectionRange`` -# -# See http://www.opengis.net/def/nil/ for the official description -# of the meanings of these values. - -# Note: the type of the nill value is assumed to be the same -# as the one of the associated band. -# """ -# -# def __init__(self, reason, value): -# self.reason = reason -# self.value = value -# -# def __eq__(self, other): -# if self.reason != other.reason or self.value != other.value: -# return False -# return True - -# def asDict( self ): -# """ -# Return object's data as a dictionary to be passed to a JSON serializer. -# """ - -# return { "reason" : self.reason , "value" : self.value } - - -## TODO: rewrite this function according to new RangeType definition -#def getRangeTypeFromFile(filename): -# """Get range type from the file given by the ``filename``.""" -# ds = gdal.Open(str(filename)) -# -# range_type = RangeType("", ds.GetRasterBand(1).DataType) -# -# for i in range(1, ds.RasterCount + 1): -# band = ds.GetRasterBand(i) -# color_intp = band.GetRasterColorInterpretation() -# if color_intp == gdal.GCI_RedBand: -# name = "red" -# description = "Red Band" -# elif color_intp == gdal.GCI_GreenBand: -# name = "green" -# description = "Green Band" -# elif color_intp == gdal.GCI_BlueBand: -# name = "blue" -# description = "Blue Band" -# else: -# name = "unknown_band_%d" % i -# description = "Unknown Band" - -# range_type.addBand(Band( -# name, name, description, -# nil_values=[ -# NilValue( -# value=band.GetNoDataValue(), -# reason="http://www.opengis.net/def/nil/OGC/1.0/unknown" -# ) -# ], -# color_interpretation = color_intp -# ) -# ) -# -# return range_type -#============================================================================== +class Band(object): + """\ + Band represents a band configuration. + + The ``gdal_interpretation`` parameter contains the GDAL + BandInterpretation value which may be assigned to a band. It may + be set to one of the following constants defined in + :mod:`osgeo.gdalconst`: + + * ``GCI_Undefined`` + * ``GCI_GrayIndex`` + * ``GCI_PaletteIndex`` + * ``GCI_RedBand`` + * ``GCI_GreenBand`` + * ``GCI_BlueBand`` + * ``GCI_AlphaBand`` + * ``GCI_HueBand`` + * ``GCI_SaturationBand`` + * ``GCI_LightnessBand`` + * ``GCI_CyanBand`` + * ``GCI_MagentaBand`` + * ``GCI_YellowBand`` + * ``GCI_BlackBand`` + + It defaults to ``GCI_Undefined``. + """ -from eoxserver.resources.coverages.models import RangeType -from eoxserver.resources.coverages.models import Band -from eoxserver.resources.coverages.models import NilValueSet -from eoxserver.resources.coverages.models import NilValue + def __init__(self, + name, + identifier='', + description='', + definition='http://opengis.net/def/property/OGC/0/Radiance', + nil_values=None, + uom='W.m-2.sr-1.nm-1', + gdal_interpretation=gdal.GCI_Undefined + ): + self.name = name + self.identifier = identifier + self.description = description + self.definition = definition + if nil_values is None: + self.nil_values = [] + else: + self.nil_values = nil_values + self.uom = uom + self.gdal_interpretation = gdal_interpretation + + def getGDALInterpretationAsString( self ) : + "Return string representation of the ``gdal_interpretation``." + return gdal.GCI_TO_NAME.get( self.gdal_interpretation , "Invalid" ) + + def __eq__(self, other): + if (self.name != other.name + or self.identifier != other.identifier + or self.description != other.description + or self.definition != other.definition + or self.nil_values != other.nil_values + or self.uom != other.uom + or self.gdal_interpretation != other.gdal_interpretation): + return False + return True + + def asDict( self ): + """ + Return object's data as a dictionary to be passed to a JSON serializer. + """ + + nils = [ nil.asDict() for nil in self.nil_values ] + + return { + "name" : self.name, + "identifier" : self.identifier, + "description" : self.description, + "definition" : self.definition, + "uom" : self.uom, + "nil_values" : nils, + "gdal_interpretation" : self.getGDALInterpretationAsString() + } +class NilValue(object): + """ + This class represents nil values of a coverage band. + + The constructor accepts the nil value itself and a reason. The + reason shall be one of: + + * ``http://www.opengis.net/def/nil/OGC/0/inapplicable`` + * ``http://www.opengis.net/def/nil/OGC/0/missing`` + * ``http://www.opengis.net/def/nil/OGC/0/template`` + * ``http://www.opengis.net/def/nil/OGC/0/unknown`` + * ``http://www.opengis.net/def/nil/OGC/0/withheld`` + * ``http://www.opengis.net/def/nil/OGC/0/AboveDetectionRange`` + * ``http://www.opengis.net/def/nil/OGC/0/BelowDetectionRange`` + + See http://www.opengis.net/def/nil/ for the official description + of the meanings of these values. + """ + + def __init__(self, reason, value): + self.reason = reason + self.value = value + + def __eq__(self, other): + if self.reason != other.reason or self.value != other.value: + return False + return True + + def asDict( self ): + """ + Return object's data as a dictionary to be passed to a JSON serializer. + """ + + return { "reason" : self.reason , "value" : self.value } + +class RangeType(object): + """ + RangeType contains range type information of a coverage. The + constructor accepts the mandatory ``name`` and ``data_type`` + parameters as well as an optional ``bands`` parameter. If no bands + are specified they shall be added with :meth:`addBands`. + + The ``data_type`` parameter may be set to one of the following + constants defined in :mod:`osgeo.gdalconst`: + + * ``GDT_Byte`` + * ``GDT_UInt16`` + * ``GDT_Int16`` + * ``GDT_UInt32`` + * ``GDT_Int32`` + * ``GDT_Float32`` + * ``GDT_Float64`` + * ``GDT_CInt16`` + * ``GDT_CInt32`` + * ``GDT_CFloat32`` + * ``GDT_CFloat64`` + """ + + def __init__(self, name, data_type, bands=None): + self.name = name + self.data_type = data_type + if bands is None: + self.bands = [] + else: + self.bands = bands + + def __eq__(self, other): + if (self.name != other.name + or self.data_type != other.data_type + or self.bands != other.bands): + return False + return True + + def __ne__(self, other): + return not (self == other) + + def getDataTypeAsString( self ) : + "Return string representation of the ``data_type``." + return gdal.GDT_TO_NAME.get( self.data_type, "Invalid" ) + + def addBand(self, band): + "Append a new band to the band list." + self.bands.append(band) + + def getSignificantFigures(self): + "Get significant figures of the currently used type." + dt = self.data_type + if dt == gdal.GDT_Byte: + return 3 + elif dt in ( gdal.GDT_UInt16 , gdal.GDT_Int16 , gdal.GDT_CInt16 ) : + return 5 + elif dt in ( gdal.GDT_UInt32 , gdal.GDT_Int32 , gdal.GDT_CInt32 ) : + return 10 + elif dt in ( gdal.GDT_Float32 , gdal.GDT_CFloat32 ) : + return 38 + #TODO 64-bit float and complex + #elif dt in ( gdal.GDT_Float64 , gdal.GDT_CFloat64 ) : + # return ?? + else: + raise NotImplementedError() + + def getAllowedValues(self): + "Get interval bounds of the currently used type." + dt = self.data_type + if dt == gdal.GDT_Byte: + return (0, 255) + elif dt == gdal.GDT_UInt16: + return (0, 65535) + elif dt in ( gdal.GDT_Int16 , gdal.GDT_CInt32 ) : + return (-32768, 32767) + elif dt == gdal.GDT_UInt32: + return (0, 4294967295) + elif dt in ( gdal.GDT_Int32 , gdal.GDT_CInt32 ) : + return (-2147483648, 2147483647) + elif dt in ( gdal.GDT_Float32 , gdal.GDT_CFloat32 ) : + return (-3.40282e+38, 3.40282e+38) + #TODO 64-bit float and complex + #elif dt in ( gdal.GDT_Float64 , gdal.GDT_CFloat64 ) : + # return ?? + else: + raise NotImplementedError() + + def asDict( self ): + """ return object as a tupe to be passed to JSON serializer """ + + bands = [ band.asDict() for band in self.bands ] + + return { + "name" : self.name, + "data_type" : self.getDataTypeAsString(), + "bands" : bands + } + + +# TODO: rewrite this function according to new RangeType definition +def getRangeTypeFromFile(filename): + """Get range type from the file given by the ``filename``.""" + ds = gdal.Open(str(filename)) + + range_type = RangeType("", ds.GetRasterBand(1).DataType) + + for i in range(1, ds.RasterCount + 1): + band = ds.GetRasterBand(i) + color_intp = band.GetRasterColorInterpretation() + if color_intp == gdal.GCI_RedBand: + name = "red" + description = "Red Band" + elif color_intp == gdal.GCI_GreenBand: + name = "green" + description = "Green Band" + elif color_intp == gdal.GCI_BlueBand: + name = "blue" + description = "Blue Band" + else: + name = "unknown_band_%d" % i + description = "Unknown Band" + + range_type.addBand(Band( + name, name, description, + nil_values=[ + NilValue( + value=band.GetNoDataValue(), + reason="http://www.opengis.net/def/nil/OGC/1.0/unknown" + ) + ], + gdal_interpretation = color_intp + ) + ) + + return range_type + +#============================================================================== + +from eoxserver.resources.coverages.models import RangeTypeRecord +from eoxserver.resources.coverages.models import BandRecord +from eoxserver.resources.coverages.models import RangeType2Band + def getAllRangeTypeNames() : """Return a list of identifiers of all registered range-types.""" - return [ rec.name for rec in RangeType.objects.all() ] + return [ rec.name for rec in RangeTypeRecord.objects.all() ] def isRangeTypeName( name ) : """ @@ -297,73 +300,66 @@ range-type with given identifier``name``. """ - return ( 0 < RangeType.objects.filter(name=name).count() ) - + return ( 0 < RangeTypeRecord.objects.filter(name=name).count() ) def getRangeType( name ) : """ - Return range type ``name`` as JSON serializable dictionary. - The values are loaded from the DB. If there is no ``RangeType`` - record corresponding to the given name ``None`` is returned. + Return ``RangeType`` object for given ``name``. The object properties are + loaded from the DB. If there is no ``RangeTypeRecord`` corresponding to + the given name ``None`` is returned. """ try: # get range-type record - rt = RangeType.objects.get(name=name) + rt = RangeTypeRecord.objects.get(name=name) - bands = [] + band = [] - # loop over band records (ordering set in model) + # loop over band records for b in rt.bands.all() : nil_values=[] - if b.nil_value_set: - # loop over nil values - for n in b.nil_value_set.nil_values.all() : - - # append created nil-value dictionary - nil_values.append( { 'reason': n.reason, 'value': n.raw_value } ) - - - band = { - 'name' : b.name, - 'data_type' : gdal.GDT_TO_NAME.get(b.data_type,'Invalid'), - 'identifier' : b.identifier, - 'description' : b.description, - 'definition' : b.definition, - 'uom' : b.uom, - 'nil_values' : nil_values, - 'color_interpretation' : - gdal.GCI_TO_NAME.get(b.color_interpretation,'Invalid'), - } - - if b.raw_value_min is not None: - band["value_min"] = b.raw_value_min - if b.raw_value_max is not None: - band["value_max"] = b.raw_value_max + # loop over nil values + for n in b.nil_values.all() : + + # append created nil-value object + nil_values.append( NilValue( reason=n.reason, value=n.value ) ) - # append created band dictionary - bands.append(band) + # append created band object + band.append( + Band( + name = b.name, + identifier = b.identifier, + description = b.description, + definition = b.definition, + uom = b.uom, + nil_values = nil_values, + gdal_interpretation = b.gdal_interpretation + ) + ) - # return JSON serializable dictionary - return { 'name': rt.name, 'bands': bands } + # return created range-type object + return RangeType( name=rt.name, data_type=rt.data_type, bands=band ) - except RangeType.DoesNotExist : + except RangeTypeRecord.DoesNotExist : return None - def setRangeType( rtype ) : """ - Insert range type to the DB. The range type record is - defined by the ``rtype`` which is a dictionaty as returned by - ``getRangeType()`` or parsed form JSON. + Save range-type record to the DB. The range-type record is created + from the ``rtype`` which can be either a ``RangeType`` object or + parsed JSON dictionary. """ - # check the input's data type - if not isinstance( rtype , dict ) : + if isinstance( rtype , RangeType ) : + + # convert to a dictionary + rtype = rtype.toDict() + + elif not isinstance( rtype , dict ) : raise ValueError("Invalid input object type!") @@ -371,51 +367,25 @@ with transaction.commit_on_success(): - rt = RangeType.objects.create( name = rtype['name'] ) - - # compatibility with old range-type json format - dtype_global = rtype.get('data_type',None) - - for idx,band in enumerate(rtype['bands']) : - - # compatibility with old range-type json format - dtype = dtype_global if dtype_global else band['data_type'] - cint = band['gdal_interpretation'] if 'gdal_interpretation' in \ - band else band['color_interpretation'] - - # convert string to gdal code - dtype = gdal.NAME_TO_GDT[dtype.lower()] - cint = gdal.NAME_TO_GCI[cint.lower()] - - # prepare nil-value set - if band['nil_values']: - nvset = NilValueSet.objects.create( - name = "__%s_%2.2d__"%(rtype['name'],idx), - data_type = dtype ) - - for nval in band['nil_values'] : - - nv = NilValue.objects.create( - reason = nval['reason'], - raw_value = str(nval['value']), - nil_value_set = nvset ) - - # cheking value - tmp = nv.value - else: - nvset = None - - bn = Band.objects.create( - index = idx, - name = band['name'], - identifier = band['identifier'], - data_type = dtype, + rtr = RangeTypeRecord.objects.create( + name = rtype['name'], + data_type = gdal.NAME_TO_GDT[rtype['data_type'].lower()]) + + for band in rtype['bands'] : + + br = BandRecord.objects.create( + name = band['name'], + identifier = band['identifier'], description = band['description'], definition = band['definition'], uom = band['uom'], - color_interpretation = cint, - range_type = rt, - nil_value_set = nvset, - raw_value_min = band.get("value_min"), - raw_value_max = band.get("value_max") - ) + gdal_interpretation = gdal.NAME_TO_GCI[ + band['gdal_interpretation'].lower()]) + + RangeType2Band.objects.create( range_type=rtr, band=br, no=1 ) + + for nval in band['nil_values'] : + + br.nil_values.create( + reason = nval['reason'], + value = nval['value']) diff -Nru eoxserver-0.4.0beta2/eoxserver/resources/coverages/testbase.py eoxserver-0.3.2/eoxserver/resources/coverages/testbase.py --- eoxserver-0.4.0beta2/eoxserver/resources/coverages/testbase.py 1970-01-01 00:00:00.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/resources/coverages/testbase.py 2013-12-10 14:57:00.000000000 +0000 @@ -0,0 +1,274 @@ +#------------------------------------------------------------------------------- +# $Id$ +# +# Project: EOxServer +# Authors: Stephan Krause +# Stephan Meissl +# Fabian Schindler +# +#------------------------------------------------------------------------------- +# Copyright (C) 2011 EOX IT Services GmbH +# +# 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 of this Software or works derived from this 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. +#------------------------------------------------------------------------------- + +from eoxserver.core.system import System +from eoxserver.testing.core import ( + EOxServerTestCase, BASE_FIXTURES, CommandTestCase, CommandFaultTestCase +) +from eoxserver.resources.coverages.managers import CoverageIdManager + +EXTENDED_FIXTURES = BASE_FIXTURES + ["testing_coverages.json"] + +class CoverageIdManagementTestCase(EOxServerTestCase): + """ Base class for Coverage ID management test cases. """ + + def setUp(self): + super(CoverageIdManagementTestCase, self).setUp() + self.mgr = CoverageIdManager() + self.manage() + + def manage(self): + """ Override this function to extend management before testing. """ + pass + +class ManagementTestCase(EOxServerTestCase): + """ Base class for test cases targeting the + synchronization functionalities. + """ + + def setUp(self): + super(ManagementTestCase, self).setUp() + self.manage() + + def getManager(self, mgrtype=None, intf_id=None): + if mgrtype is None: + mgrtype = self.getType() + if intf_id is None: + intf_id = self.getInterfaceID() + + return System.getRegistry().findAndBind( + intf_id=intf_id, + params={ + "resources.coverages.interfaces.res_type": mgrtype + } + ) + + def getInterfaceID(self): + return "resources.coverages.interfaces.Manager" + + def getType(self): + raise NotImplementedError() + + def manage(self): + """ Override this function to test management functions. + """ + pass + + def testContents(self): + """ Stub testing method. Override this to make more + sophisticated checks for errors. + """ + pass + +class CreateTestCase(ManagementTestCase): + def create(self, mgrtype=None, intf_id=None, **kwargs): + mgr = self.getManager(mgrtype, intf_id) + return mgr.create(**kwargs) + +class UpdateTestCase(ManagementTestCase): + def update(self, mgrtype=None, intf_id=None, **kwargs): + mgr = self.getManager(mgrtype, intf_id) + return mgr.update(**kwargs) + +class DeleteTestCase(ManagementTestCase): + def delete(self, obj_id, mgrtype=None, intf_id=None): + mgr = self.getManager() + mgr.delete(obj_id) + +class SynchronizeTestCase(ManagementTestCase): + def synchronize(self, obj_id, mgrtype=None, intf_id=None, **kwargs): + mgr = self.getManager() + mgr.synchronize(obj_id, **kwargs) + +# rectified dataset test cases + +class RectifiedDatasetCreateTestCase(CreateTestCase): + def getType(self): + return "eo.rect_dataset" + +class RectifiedDatasetUpdateTestCase(UpdateTestCase): + def getType(self): + return "eo.rect_dataset" + +class RectifiedDatasetDeleteTestCase(DeleteTestCase): + def getType(self): + return "eo.rect_dataset" + +# rectified stitched mosaic manager test cases + +class RectifiedStitchedMosaicCreateTestCase(CreateTestCase): + def getType(self): + return "eo.rect_stitched_mosaic" + +class RectifiedStitchedMosaicUpdateTestCase(UpdateTestCase): + def getType(self): + return "eo.rect_stitched_mosaic" + +class RectifiedStitchedMosaicDeleteTestCase(DeleteTestCase): + def getType(self): + return "eo.rect_stitched_mosaic" + +class RectifiedStitchedMosaicSynchronizeTestCase(SynchronizeTestCase): + def getType(self): + return "eo.rect_stitched_mosaic" + +# dataset series manager test cases + +class DatasetSeriesCreateTestCase(CreateTestCase): + def getType(self): + return "eo.dataset_series" + +class DatasetSeriesUpdateTestCase(UpdateTestCase): + def getType(self): + return "eo.dataset_series" + +class DatasetSeriesDeleteTestCase(DeleteTestCase): + def getType(self): + return "eo.dataset_series" + +class DatasetSeriesSynchronizeTestCase(SynchronizeTestCase): + def getType(self): + return "eo.dataset_series" + + +class EODatasetMixIn(object): + def findDatasetsByFilters(self, *filters): + """ Convenience method to get a list of coverages by given filter + expressions. + """ + filter_exprs = [ + System.getRegistry().getFromFactory( + factory_id = "resources.coverages.filters.CoverageExpressionFactory", + params = filter_expr + ) + for filter_expr in filters + ] + + return System.getRegistry().bind( + "resources.coverages.wrappers.EOCoverageFactory" + ).find( + impl_ids = [ + "resources.coverages.wrappers.RectifiedDatasetWrapper", + "resources.coverages.wrappers.ReferenceableDatasetWrapper" + ], + filter_exprs = filter_exprs + ) + + def getDatasetById(self, cid): + """ Convenience method to get a coverage by its ID. + """ + return System.getRegistry().getFromFactory( + "resources.coverages.wrappers.EOCoverageFactory", + {"obj_id": cid} + ) + + +class RectifiedStitchedMosaicMixIn(object): + def getStitchedMosaicById(self, cid): + return System.getRegistry().getFromFactory( + "resources.coverages.wrappers.EOCoverageFactory", + {"obj_id": cid} + ) + + +class DatasetSeriesMixIn(object): + def getDatasetSeriesById(self, eoid): + return System.getRegistry().getFromFactory( + "resources.coverages.wrappers.DatasetSeriesFactory", + {"obj_id": eoid} + ) + + +class CoverageCommandTestCase(CommandTestCase): + fixtures = EXTENDED_FIXTURES + + +class CoverageCommandFaultTestCase(CommandFaultTestCase): + pass + +class CommandRegisterDatasetTestCase(CoverageCommandTestCase, EODatasetMixIn): + fixtures = BASE_FIXTURES # normally we want an empty database + + name = "eoxs_register_dataset" + coverages_to_be_registered = [] # list of dicts with two keys allowed: + # 'eo_id', 'coverage_id' + + def getCoveragesToBeRegistered(self): + result = {} + for ctbr in self.coverages_to_be_registered: + eo_id = ctbr.get("eo_id") + coverage_id = ctbr.get("coverage_id") + coverage = None + + if eo_id: + try: + coverage = self.findDatasetsByFilters({ + "op_name": "attr", + "operands": ("eo_id", "=", eo_id) + })[0] + except IndexError: + pass + + if coverage_id: + coverage = self.getDatasetById(coverage_id) + result[eo_id or coverage_id] = coverage + + return result + + def testCoverageRegistered(self): + for cid, coverage in self.getCoveragesToBeRegistered().items(): + if not coverage: + self.fail("Coverage with ID '%s' was not inserted." % cid) + + +class CommandInsertTestCase(CoverageCommandTestCase, DatasetSeriesMixIn, EODatasetMixIn): + name = "eoxs_insert_into_series" + + datasets_to_be_inserted = [] + dataset_series_id = "MER_FRS_1P_RGB_reduced" + + def testContents(self): + dss = self.getDatasetSeriesById(self.dataset_series_id) + for dataset_id in self.datasets_to_be_inserted: + self.assertIn(dataset_id, [coverage.getCoverageId() + for coverage in dss.getEOCoverages()]) + + +class CommandExcludeTestCase(CoverageCommandTestCase, DatasetSeriesMixIn, EODatasetMixIn): + name = "eoxs_remove_from_series" + + datasets_to_be_excluded = [] + dataset_series_id = "MER_FRS_1P_RGB_reduced" + + def testContents(self): + dss = self.getDatasetSeriesById(self.dataset_series_id) + for dataset_id in self.datasets_to_be_excluded: + self.assertNotIn(dataset_id, [coverage.getCoverageId() + for coverage in dss.getEOCoverages()]) diff -Nru eoxserver-0.4.0beta2/eoxserver/resources/coverages/tests.py eoxserver-0.3.2/eoxserver/resources/coverages/tests.py --- eoxserver-0.4.0beta2/eoxserver/resources/coverages/tests.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/resources/coverages/tests.py 2013-12-10 14:57:00.000000000 +0000 @@ -1,9 +1,9 @@ #----------------------------------------------------------------------- # # Project: EOxServer -# Authors: Fabian Schindler +# Authors: Stephan Krause # Stephan Meissl -# Stephan Krause +# Fabian Schindler # #------------------------------------------------------------------------------- # Copyright (C) 2011 EOX IT Services GmbH @@ -25,459 +25,1087 @@ # 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. -#------------------------------------------------------------------------------- +#----------------------------------------------------------------------- -from datetime import datetime -from StringIO import StringIO -from textwrap import dedent +import os.path +import shutil +import logging +from datetime import timedelta -from django.test import TestCase -from django.core.exceptions import ValidationError +from django.utils.datetime_safe import datetime from django.contrib.gis.geos import GEOSGeometry, Polygon, MultiPolygon -from django.utils.dateparse import parse_datetime -from django.utils.timezone import utc - -from eoxserver.core import env -from eoxserver.resources.coverages.models import * -from eoxserver.resources.coverages.metadata.formats import ( - native, eoom, dimap_general -) - - -def create(Class, **kwargs): - obj = Class(**kwargs) - obj.full_clean() - obj.save() - return obj - - -def refresh(*objects): - refr = lambda obj: type(obj).objects.get(identifier=obj.identifier) - if len(objects) == 1: - return refr(objects[0]) - return map(refr, objects) - -def union(*footprints): - u = None - for footprint in footprints: - if u is None: - u = footprint - else: - u = footprint.union(u) - return u - - -class GeometryMixIn(object): - def assertGeometryEqual(self, a, b, tolerance=0.05): - try: - if a.difference(b).empty: - return - except: - pass - self.assertTrue( - a.equals_exact(b, tolerance), - "%r != %r" % (a.wkt, b.wkt) - ) +from django.conf import settings +from eoxserver.core.system import System +from eoxserver.core.util.timetools import UTCOffsetTimeZoneInfo, isotime +from eoxserver.testing.core import BASE_FIXTURES +import eoxserver.resources.coverages.testbase as eoxstest +from eoxserver.resources.coverages.geo import GeospatialMetadata +from eoxserver.resources.coverages.metadata import EOMetadata +import eoxserver.resources.coverages.exceptions as exceptions + + +logger = logging.getLogger(__name__) + +# create new rectified dataset from a local path + +class RectifiedDatasetCreateWithLocalPathTestCase(eoxstest.RectifiedDatasetCreateTestCase): + def manage(self): + args = { + "local_path": os.path.join(settings.PROJECT_DIR, + "data/meris/MER_FRS_1P_reduced", + "ENVISAT-MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_uint16_reduced_compressed.tif"), + "range_type_name": "RGB", + } + self.wrapper = self.create(**args) -class ModelTests(GeometryMixIn, TestCase): - def setUp(self): - self.range_type = create(RangeType, - name="RGB" - ) - - self.rectified_1 = create(RectifiedDataset, - identifier="rectified-1", - footprint=GEOSGeometry("MULTIPOLYGON (((-111.6210939999999994 26.8588260000000005, -113.0273439999999994 -4.0786740000000004, -80.6835939999999994 -9.7036739999999995, -68.0273439999999994 15.6088260000000005, -111.6210939999999994 26.8588260000000005)))"), - begin_time=parse_datetime("2013-06-11T14:55:23Z"), - end_time=parse_datetime("2013-06-11T14:55:23Z"), - min_x=10, min_y=10, max_x=20, max_y=20, srid=4326, - size_x=100, size_y=100, - range_type=self.range_type - ) + def testContents(self): + pass + +# create new rectified dataset from a local path with metadata - self.rectified_2 = create(RectifiedDataset, - identifier="rectified-2", - footprint=GEOSGeometry("MULTIPOLYGON (((-28.0371090000000009 19.4760129999999982, -32.9589840000000009 -0.9146120000000000, -2.8125000000000000 -3.8150019999999998, 4.2187500000000000 19.1244510000000005, -28.0371090000000009 19.4760129999999982)))"), - begin_time=parse_datetime("2013-06-10T18:52:34Z"), - end_time=parse_datetime("2013-06-10T18:52:32Z"), - min_x=10, min_y=10, max_x=20, max_y=20, srid=4326, - size_x=100, size_y=100, - range_type=self.range_type - ) +class RectifiedDatasetCreateWithLocalPathAndMetadataTestCase(eoxstest.RectifiedDatasetCreateTestCase): + def manage(self): + args = { + "local_path": os.path.join( + settings.PROJECT_DIR, + "data/meris/MER_FRS_1P_reduced", + "ENVISAT-MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_uint16_reduced_compressed.tif" + ), + "range_type_name": "RGB", + "md_local_path": os.path.join( + settings.PROJECT_DIR, + "data/meris/MER_FRS_1P_reduced", + "ENVISAT-MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_uint16_reduced_compressed.xml" + ) + } + self.wrapper = self.create(**args) - self.rectified_3 = create(RectifiedDataset, - identifier="rectified-3", - footprint=GEOSGeometry("MULTIPOLYGON (((-85.5175780000000003 14.2904660000000003, -116.2792969999999997 -8.3853150000000003, -63.7207030000000003 -19.4595340000000014, -58.7988280000000003 7.2592160000000003, -85.5175780000000003 14.2904660000000003)))"), - begin_time=parse_datetime("2013-06-10T18:55:54Z"), - end_time=parse_datetime("2013-06-10T18:55:54Z"), - min_x=10, min_y=10, max_x=20, max_y=20, srid=4326, - size_x=100, size_y=100, - range_type=self.range_type +class RectifiedDatasetCreateWithContainerTestCase(eoxstest.RectifiedDatasetCreateTestCase): + fixtures = BASE_FIXTURES + + def _create_containers(self): + mosaic_mgr = System.getRegistry().findAndBind( + intf_id="resources.coverages.interfaces.Manager", + params={ + "resources.coverages.interfaces.res_type": "eo.rect_stitched_mosaic" + } ) + + self.stitched_mosaic = mosaic_mgr.create(**{ + "data_dirs": [], + "geo_metadata": GeospatialMetadata( + srid=4326, size_x=1023, size_y=451, + extent=(-3.75, 32.158895, 28.326165, 46.3) + ), + "range_type_name": "RGB", + "eo_metadata": EOMetadata( + "STITCHED_MOSAIC", + datetime.now(), + datetime.now(), + GEOSGeometry("POLYGON((-3 33, 27 33, 27 45, -3 45, -3 33))") + ), + "storage_dir": "/tmp/some/storage/dir" + }) + + def manage(self): + self._create_containers() + args = { + "local_path": os.path.join(settings.PROJECT_DIR, + "data/meris/mosaic_MER_FRS_1P_RGB_reduced", + "mosaic_ENVISAT-MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_RGB_reduced.tif"), + "range_type_name": "RGB", + "container": self.stitched_mosaic + } + self.wrapper = self.create(**args) - self.referenceable = create(ReferenceableDataset, - identifier="referenceable-1", - footprint=GEOSGeometry("MULTIPOLYGON (((-85.5175780000000003 14.2904660000000003, -116.2792969999999997 -8.3853150000000003, -63.7207030000000003 -19.4595340000000014, -58.7988280000000003 7.2592160000000003, -85.5175780000000003 14.2904660000000003)))"), - begin_time=parse_datetime("2013-06-10T18:55:54Z"), - end_time=parse_datetime("2013-06-10T18:55:54Z"), - min_x=10, min_y=10, max_x=20, max_y=20, srid=4326, - size_x=100, size_y=100, - range_type=self.range_type + def testContents(self): + self.assertTrue(self.stitched_mosaic.contains(self.wrapper), + "Stitched Mosaic has to contain the dataset.") + +class RectifiedDatasetCreateWithContainerIDsTestCase(eoxstest.RectifiedDatasetCreateTestCase): + fixtures = BASE_FIXTURES + + def _create_containers(self): + dss_mgr = System.getRegistry().findAndBind( + intf_id="resources.coverages.interfaces.Manager", + params={ + "resources.coverages.interfaces.res_type": "eo.dataset_series" + } ) - - self.mosaic = RectifiedStitchedMosaic( - identifier="mosaic-1", - min_x=10, min_y=10, max_x=20, max_y=20, srid=4326, - size_x=100, size_y=100, - range_type=self.range_type + + mosaic_mgr = System.getRegistry().findAndBind( + intf_id="resources.coverages.interfaces.Manager", + params={ + "resources.coverages.interfaces.res_type": "eo.rect_stitched_mosaic" + } ) + + self.dataset_series = dss_mgr.create(**{ + "data_dirs": [], + "eo_metadata": EOMetadata( + "DATASET_SERIES", + datetime.now(), + datetime.now(), + GEOSGeometry("POLYGON((-3 33, 27 33, 27 45, -3 45, -3 33))") + ) + }) + + self.stitched_mosaic = mosaic_mgr.create(**{ + "data_dirs": [], + "geo_metadata": GeospatialMetadata( + srid=4326, size_x=1023, size_y=451, + extent=(-3.75, 32.158895, 28.326165, 46.3) + ), + "range_type_name": "RGB", + "eo_metadata": EOMetadata( + "STITCHED_MOSAIC", + datetime.now(), + datetime.now(), + GEOSGeometry("POLYGON((-3 33, 27 33, 27 45, -3 45, -3 33))") + ), + "storage_dir": "/tmp/some/storage/dir" + }) + + def manage(self): + self._create_containers() + args = { + "local_path": os.path.join(settings.PROJECT_DIR, + "data/meris/mosaic_MER_FRS_1P_RGB_reduced", + "mosaic_ENVISAT-MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_RGB_reduced.tif"), + "range_type_name": "RGB", + "container_ids": [self.stitched_mosaic.getCoverageId(), self.dataset_series.getEOID()] + } + self.wrapper = self.create(**args) - # TODO: bug, requires identifier to be set manually again - self.mosaic.identifier = "mosaic-1" - self.mosaic.full_clean() - self.mosaic.save() + def testContents(self): + self.assertTrue(self.stitched_mosaic.contains(self.wrapper), + "Stitched Mosaic has to contain the dataset.") - #======================================================================= - # Collections - #======================================================================= + self.assertTrue(self.dataset_series.contains(self.wrapper), + "Dataset Series has to contain the dataset.") - self.series_1 = create(DatasetSeries, - identifier="series-1" - ) +# create new rectified dataset from a ftp path +class RectifiedDatasetCreateWithRemothePathTestCase(eoxstest.RectifiedDatasetCreateTestCase): + def manage(self): + args = { + "local_path": os.path.join( + settings.PROJECT_DIR, + "data/meris/MER_FRS_1P_reduced", + "ENVISAT-MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_uint16_reduced_compressed.tif" + ), + "range_type_name": "RGB", + "md_local_path": os.path.join( + settings.PROJECT_DIR, + "data/meris/MER_FRS_1P_reduced", + "ENVISAT-MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_uint16_reduced_compressed.xml" + ) + } + self.wrapper = self.create(**args) - self.series_2 = create(DatasetSeries, - identifier="series-2" - ) +# create new rectified dataset from a rasdaman location - def tearDown(self): - pass +class RectifiedDatasetCreateWithRasdamanLocationTestCase(eoxstest.RectifiedDatasetCreateTestCase): + def manage(self): + args = { + "collection": "MERIS", + "ras_host": "localhost", + "range_type_name": "RGB", + "geo_metadata": GeospatialMetadata( + srid=4326, + size_x=541, + size_y=449, + extent=(11.331755, 32.19025, 28.29481, 46.268645) + ), + "md_local_path": os.path.join( + settings.PROJECT_DIR, + "data/meris/mosaic_MER_FRS_1P_RGB_reduced", + "mosaic_ENVISAT-MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_RGB_reduced.xml" + ) + } + self.wrapper = self.create(**args) + def testType(self): + self.assertEqual(self.wrapper.getType(), "eo.rect_dataset") + - def test_insertion(self): - rectified_1, rectified_2, rectified_3 = ( - self.rectified_1, self.rectified_2, self.rectified_3 +class RectifiedDatasetUpdateGeoMetadataTestCase(eoxstest.RectifiedDatasetUpdateTestCase): + fixtures = eoxstest.EXTENDED_FIXTURES + + def manage(self): + args = { + "obj_id": "MER_FRS_1PNPDE20060830_100949_000001972050_00423_23523_0079_uint16_reduced_compressed", + "set": { + "geo_metadata": GeospatialMetadata( + srid=3035, + size_x=100, + size_y=100, + extent=(0, 0, 10000000, 10000000) + ) + } + } + self.update(**args) + + def testContents(self): + coverage = System.getRegistry().getFromFactory( + "resources.coverages.wrappers.EOCoverageFactory", + {"obj_id": "MER_FRS_1PNPDE20060830_100949_000001972050_00423_23523_0079_uint16_reduced_compressed"} ) - mosaic, series_1, series_2 = self.mosaic, self.series_1, self.series_2 - - mosaic.insert(rectified_1) - mosaic.insert(rectified_2) - mosaic.insert(rectified_3) - - self.assertIn(rectified_1, mosaic) - self.assertIn(rectified_2, mosaic) - self.assertIn(rectified_3, mosaic) - - series_1.insert(rectified_1) - series_1.insert(rectified_2) - series_1.insert(rectified_3) - series_1.insert(mosaic) - - self.assertIn(rectified_1, series_1) - self.assertIn(rectified_2, series_1) - self.assertIn(rectified_3, series_1) - self.assertIn(mosaic, series_1) - - series_2.insert(rectified_1) - series_2.insert(rectified_2) - series_2.insert(rectified_3) - series_2.insert(mosaic) - - self.assertIn(rectified_1, series_2) - self.assertIn(rectified_2, series_2) - self.assertIn(rectified_3, series_2) - self.assertIn(mosaic, series_2) - - self.assertEqual(len(mosaic), 3) - self.assertEqual(len(series_1), 4) - self.assertEqual(len(series_1), 4) + + self.assertEqual(3035, coverage.getAttrValue("srid")) + self.assertEqual((100, 100), (coverage.getAttrValue("size_x"), coverage.getAttrValue("size_y"))) + self.assertEqual((0, 0, 10000000, 10000000),( + coverage.getAttrValue("minx"), + coverage.getAttrValue("miny"), + coverage.getAttrValue("maxx"), + coverage.getAttrValue("maxy") + )) + +class RectifiedDatasetUpdateGeoMetadataViaSetAttrMetadataTestCase(eoxstest.RectifiedDatasetUpdateTestCase): + fixtures = eoxstest.EXTENDED_FIXTURES + + def manage(self): + args = { + "obj_id": "MER_FRS_1PNPDE20060830_100949_000001972050_00423_23523_0079_uint16_reduced_compressed", + "set": { + "srid": 3035, + "size_x": 100, + "size_y": 100, + "minx": 0, + "miny": 0, + "maxx": 10000000, + "maxy": 10000000 + } + } + self.update(**args) + + def testContents(self): + coverage = System.getRegistry().getFromFactory( + "resources.coverages.wrappers.EOCoverageFactory", + {"obj_id": "MER_FRS_1PNPDE20060830_100949_000001972050_00423_23523_0079_uint16_reduced_compressed"} + ) + + self.assertEqual(3035, coverage.getAttrValue("srid")) + self.assertEqual((100, 100), (coverage.getAttrValue("size_x"), coverage.getAttrValue("size_y"))) + self.assertEqual((0, 0, 10000000, 10000000),( + coverage.getAttrValue("minx"), + coverage.getAttrValue("miny"), + coverage.getAttrValue("maxx"), + coverage.getAttrValue("maxy") + )) +class RectifiedDatasetUpdateEOMetadataTestCase(eoxstest.RectifiedDatasetUpdateTestCase): + fixtures = eoxstest.EXTENDED_FIXTURES + + def manage(self): + import pytz + utc = pytz.timezone("UTC") + self.begin_time = datetime.now(utc) + self.end_time = datetime.now(utc) + self.footprint = MultiPolygon(Polygon.from_bbox((-3, 33, 12, 46))) + #GEOSGeometry("POLYGON((1 2, 3 2, 3 4, 1 4, 1 2))") + args = { + "obj_id": "mosaic_MER_FRS_1PNPDE20060830_100949_000001972050_00423_23523_0079_RGB_reduced", + "set": { + "eo_metadata": EOMetadata( + "SomeEOID", + self.begin_time, + self.end_time, + self.footprint + ) + } + } + self.update(**args) + + def testContents(self): + coverage = System.getRegistry().getFromFactory( + "resources.coverages.wrappers.EOCoverageFactory", + {"obj_id": "mosaic_MER_FRS_1PNPDE20060830_100949_000001972050_00423_23523_0079_RGB_reduced"} + ) + + self.assertEqual("SomeEOID", coverage.getEOID()) + self.assertEqual(self.begin_time, coverage.getBeginTime()) + self.assertEqual(self.end_time, coverage.getEndTime()) + self.assertTrue(self.footprint.equals_exact(coverage.getFootprint(), 0.001), + "Footprints mismatch.") - mosaic = RectifiedStitchedMosaic.objects.get(identifier="mosaic-1") - mosaic, series_1, series_2 = refresh(mosaic, series_1, series_2) +class RectifiedDatasetUpdateEOMetadataViaSetAttrTestCase(eoxstest.RectifiedDatasetUpdateTestCase): + fixtures = eoxstest.EXTENDED_FIXTURES + + def manage(self): + import pytz + utc = pytz.timezone("UTC") + self.begin_time = datetime.now(utc) + self.end_time = datetime.now(utc) + self.footprint = GEOSGeometry("MULTIPOLYGON(((-3 33, 12 33, 12 45, -3 45, -3 33)))") + args = { + "obj_id": "mosaic_MER_FRS_1PNPDE20060830_100949_000001972050_00423_23523_0079_RGB_reduced", + "set": { + "eo_id": "SomeEOID", + "footprint": self.footprint, + "begin_time": self.begin_time, + "end_time": self.end_time + } + } + self.update(**args) + + def testContents(self): + coverage = System.getRegistry().getFromFactory( + "resources.coverages.wrappers.EOCoverageFactory", + {"obj_id": "mosaic_MER_FRS_1PNPDE20060830_100949_000001972050_00423_23523_0079_RGB_reduced"} + ) + + self.assertEqual("SomeEOID", coverage.getEOID()) + self.assertEqual(self.begin_time, coverage.getBeginTime()) + self.assertEqual(self.end_time, coverage.getEndTime()) + self.assertEqual(self.footprint, coverage.getFootprint()) - # TODO: further check metadata - self.assertTrue(series_1.begin_time is not None) +class RectifiedDatasetUpdateLinkContainersTestCase(eoxstest.RectifiedDatasetUpdateTestCase): + fixtures = eoxstest.EXTENDED_FIXTURES + + def manage(self): + args = { + "obj_id": "mosaic_MER_FRS_1PNPDE20060830_100949_000001972050_00423_23523_0079_RGB_reduced", + "link": { + "container_ids": ["MER_FRS_1P_reduced"] + } + } + self.update(**args) + + def testContents(self): + coverage = System.getRegistry().getFromFactory( + "resources.coverages.wrappers.EOCoverageFactory", + {"obj_id": "mosaic_MER_FRS_1PNPDE20060830_100949_000001972050_00423_23523_0079_RGB_reduced"} + ) + + self.assertIn("MER_FRS_1P_reduced", [container.getEOID() for container in coverage.getContainers()]) - begin_time, end_time, all_rectified_footprints = collect_eo_metadata( - RectifiedDataset.objects.all() +class RectifiedDatasetUpdateUnlinkContainersTestCase(eoxstest.RectifiedDatasetUpdateTestCase): + fixtures = eoxstest.EXTENDED_FIXTURES + + def manage(self): + args = { + "obj_id": "mosaic_MER_FRS_1PNPDE20060830_100949_000001972050_00423_23523_0079_RGB_reduced", + "unlink": { + "container_ids": ["mosaic_MER_FRS_1P_RGB_reduced"] + } + } + self.update(**args) + + def testContents(self): + coverage = System.getRegistry().getFromFactory( + "resources.coverages.wrappers.EOCoverageFactory", + {"obj_id": "mosaic_MER_FRS_1PNPDE20060830_100949_000001972050_00423_23523_0079_RGB_reduced"} ) - time_extent = begin_time, end_time + + self.assertNotIn("mosaic_MER_FRS_1P_RGB_reduced", [container.getEOID() for container in coverage.getContainers()]) - extent_footprint = MultiPolygon( - Polygon.from_bbox(all_rectified_footprints.extent) +class RectifiedDatasetUpdateCoverageAndEOIDTestCase(eoxstest.RectifiedDatasetUpdateTestCase): + fixtures = eoxstest.EXTENDED_FIXTURES + + def manage(self): + args = { + "obj_id": "mosaic_MER_FRS_1PNPDE20060830_100949_000001972050_00423_23523_0079_RGB_reduced", + "set": { + "coverage_id": "SomeCoverageID", + "eo_id": "SomeEOID" + } + } + self.update(**args) + + def testContents(self): + coverage = System.getRegistry().getFromFactory( + "resources.coverages.wrappers.EOCoverageFactory", + {"obj_id": "SomeCoverageID"} ) + + self.assertEqual("SomeCoverageID", coverage.getCoverageId()) + self.assertEqual("SomeEOID", coverage.getEOID()) - self.assertGeometryEqual(series_1.footprint, extent_footprint) - self.assertGeometryEqual(series_2.footprint, extent_footprint) - self.assertGeometryEqual(mosaic.footprint, all_rectified_footprints) - self.assertEqual(series_1.time_extent, time_extent) - self.assertEqual(series_2.time_extent, time_extent) - self.assertEqual(mosaic.time_extent, time_extent) +# create new mosaic and add a local path to locations +class RectifiedStitchedMosaicCreateWithLocalPathTestCase(eoxstest.RectifiedStitchedMosaicCreateTestCase): + def manage(self): + args = { + "data_dirs": [{ + "path": os.path.join(settings.PROJECT_DIR, + "data/meris/mosaic_MER_FRS_1P_RGB_reduced"), + "search_pattern": "*.tif", + "type": "local" + }], + "geo_metadata": GeospatialMetadata( + srid=4326, size_x=1023, size_y=451, + extent=(-3.75, 32.158895, 28.326165, 46.3) + ), + "range_type_name": "RGB", + "eo_metadata": EOMetadata( + "STITCHED_MOSAIC", + datetime.now(), + datetime.now(), + GEOSGeometry("POLYGON((-3 33, 27 33, 27 45, -3 45, -3 33))") + ), + "storage_dir": "/tmp/some/storage/dir" + } + self.wrapper = self.create(**args) + + def testContents(self): + # test the number of datasets + self.assertEqual(len(self.wrapper.getDatasets()), 3) + + # test validity of datasets - for eo_obj in series_1: - pass +# create new mosaic and add a remote path to locations +class RectifiedStitchedMosaicCreateWithRemotePathTestCase(eoxstest.RectifiedStitchedMosaicCreateTestCase): + def manage(self): + args = { + "data_dirs": [{ + "path": "test/mosaic_MER_FRS_1P_RGB_reduced", + "search_pattern": "*.tif", + "type": "ftp", + + "host": "hma.eox.at", + "user": "anonymous", + "password": "" + }], + "geo_metadata": GeospatialMetadata( + srid=4326, size_x=1023, size_y=451, + extent=(-3.75, 32.158895, 28.326165, 46.3) + ), + "range_type_name": "RGB", + "eo_metadata": EOMetadata( + "SOMEEOID", + datetime.now(), + datetime.now(), + GEOSGeometry("POLYGON((-3 33, 27 33, 27 45, -3 45, -3 33))") + ), + "storage_dir": "/tmp/some/storage/dir" + } + self.wrapper = self.create(**args) + + def testContents(self): + self.assertEqual(len(self.wrapper.getDatasets()), 3) - def test_insertion_cascaded(self): - rectified_1, mosaic, series_1, series_2 = ( - self.rectified_1, self.mosaic, self.series_1, self.series_2 - ) - mosaic.insert(rectified_1) - series_1.insert(mosaic) - series_2.insert(series_1) +#TODO enable when implemented +# create new mosaic and add a rasdaman location to locations +#class RectifiedStitchedMosaicCreateWithRasdamanLocationTestCase(eoxstest.RectifiedStitchedMosaicCreateTestCase): +# def manage(self): +# args = { +# "data_dirs": [{ +# "type": "rasdaman", +# "host": "localhost", +# "collection": "MERIS" +# }], +# "geo_metadata": GeospatialMetadata( +# srid=4326, size_x=100, size_y=100, +# extent=(1, 2, 3, 4) +# ), +# "range_type_name": "RGB", +# "eo_metadata": EOMetadata( +# "SOMEEOID", +# datetime.now(), +# datetime.now(), +# GEOSGeometry("POLYGON((1 2, 3 2, 3 4, 1 4, 1 2))") +# ), +# "storage_dir": "/tmp/some/storage/dir" +# } +# self.wrapper = self.create(**args) - self.assertTrue(series_2.contains(rectified_1, recursive=True)) - self.assertFalse(series_2.contains(rectified_1)) - self.assertTrue(series_1.contains(rectified_1, recursive=True)) - self.assertFalse(series_1.contains(rectified_1)) +class RectifiedStitchedMosaicUpdateLinkDataSourcesTestCase(eoxstest.RectifiedStitchedMosaicUpdateTestCase): + fixtures = eoxstest.EXTENDED_FIXTURES + + def manage(self): + args = { + "obj_id": "mosaic_MER_FRS_1P_RGB_reduced", + "link": { + "data_dirs": [{ + "type": "local", + "path": os.path.join( + settings.PROJECT_DIR, + "data/meris/MER_FRS_1P_reduced", + "ENVISAT-MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_uint16_reduced_compressed.tif" + ), + "md_path": os.path.join( + settings.PROJECT_DIR, + "data/meris/MER_FRS_1P_reduced", + "ENVISAT-MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_uint16_reduced_compressed.xml" + ) + }] + } + } + + self.update(**args) + + def testContents(self): + pass - self.assertTrue(mosaic.contains(rectified_1, recursive=True)) - self.assertTrue(mosaic.contains(rectified_1)) - for obj in series_2.iter_cast(True): - pass +class RectifiedStitchedMosaicUpdateUnlinkDatasetsTestCase(eoxstest.RectifiedStitchedMosaicUpdateTestCase): + fixtures = eoxstest.EXTENDED_FIXTURES + + def manage(self): + args = { + "obj_id": "mosaic_MER_FRS_1P_RGB_reduced", + "unlink": { + "coverage_ids": [ + "mosaic_MER_FRS_1PNPDE20060830_100949_000001972050_00423_23523_0079_RGB_reduced" + ] + } + } + + self.update(**args) + + def testContents(self): + mosaic = System.getRegistry().getFromFactory( + "resources.coverages.wrappers.EOCoverageFactory", + {"obj_id": "mosaic_MER_FRS_1P_RGB_reduced"} + ) + + self.assertNotIn("mosaic_MER_FRS_1PNPDE20060830_100949_000001972050_00423_23523_0079_RGB_reduced", + [coverage.getCoverageId() for coverage in mosaic.getDatasets()]) + - def test_insertion_failed(self): - referenceable, mosaic = self.referenceable, self.mosaic +# create dataset series with a local path +class DatasetSeriesCreateWithLocalPathTestCase(eoxstest.DatasetSeriesCreateTestCase): + fixtures = BASE_FIXTURES + + def manage(self): + args = { + "data_dirs": [{ + "path": os.path.join(settings.PROJECT_DIR, + "data/meris/MER_FRS_1P_reduced"), + "search_pattern": "*.tif", + "type": "local" + }], + "eo_metadata": EOMetadata( + "SOMEEOID", + datetime.now(), + datetime.now(), + GEOSGeometry("POLYGON((1 2, 3 2, 3 4, 1 4, 1 2))") + ) + } + self.wrapper = self.create(**args) + + def testContents(self): + self.assertEqual(len(self.wrapper.getEOCoverages()), 3) + + +# create dataset series with a remote path - with self.assertRaises(ValidationError): - mosaic.insert(referenceable) +# alter dataset series to remove a location - mosaic = refresh(mosaic) - self.assertNotIn(referenceable, mosaic) +class DatasetSeriesRemoveLocationTestCase(eoxstest.DatasetSeriesUpdateTestCase): + fixtures = eoxstest.EXTENDED_FIXTURES + + def manage(self): + pass + + def testContents(self): + pass +class DatasetSeriesSynchronizeNewDirectoryTestCase(eoxstest.DatasetSeriesSynchronizeTestCase): + fixtures = BASE_FIXTURES + + def manage(self): + # create an empty series, with an unsynchronized data source + datasource_factory = System.getRegistry().bind( + "resources.coverages.data.DataSourceFactory" + ) + location_factory = System.getRegistry().bind( + "backends.factories.LocationFactory" + ) + + dataset_series_factory = System.getRegistry().bind( + "resources.coverages.wrappers.DatasetSeriesFactory" + ) + + # this code should not be duplicated; intentionally misused ;) + self.wrapper = dataset_series_factory.create( + impl_id="resources.coverages.wrappers.DatasetSeriesWrapper", + params={ + "data_sources": [datasource_factory.create( + type="data_source", + **{ + "location": location_factory.create( + **{ + "path": os.path.join(settings.PROJECT_DIR, + "data/meris/MER_FRS_1P_reduced"), + "type": "local" + } + ), + "search_pattern": "*.tif" + } + )], + "eo_metadata": EOMetadata( + "SOMEEOID", + datetime.now(), + datetime.now(), + GEOSGeometry("POLYGON((1 2, 3 2, 3 4, 1 4, 1 2))") + ) + } + ) + + self.synchronize(self.wrapper.getEOID()) + + def testContents(self): + wrapper = System.getRegistry().getFromFactory( + "resources.coverages.wrappers.DatasetSeriesFactory", + {"obj_id": "SOMEEOID"} + ) + + # after sync, the datasets shall be registered. + self.assertEqual(len(wrapper.getEOCoverages()), 3) + + self.assertEqual(wrapper.getBeginTime(), + min([dataset.getBeginTime() + for dataset in wrapper.getEOCoverages()])) + + self.assertEqual(wrapper.getEndTime(), + max([dataset.getEndTime() + for dataset in wrapper.getEOCoverages()])) + + footprint = wrapper.getEOCoverages()[0].getFootprint().envelope + for dataset in wrapper.getEOCoverages()[1:]: + footprint = footprint.union(dataset.getFootprint()).envelope + self.assertTrue(wrapper.getFootprint().envelope.equals_exact(footprint, 0.001), + "Footprints mismatch.") - def test_insertion_and_removal(self): - rectified_1, rectified_2, series_1 = ( - self.rectified_1, self.rectified_2, self.series_1 +class DatasetSeriesSynchronizeFileRemovedTestCase(eoxstest.DatasetSeriesSynchronizeTestCase): + fixtures = BASE_FIXTURES + + def _create_files(self): + src = os.path.join(settings.PROJECT_DIR, + "data/meris/mosaic_MER_FRS_1P_RGB_reduced") + + self.dst = os.path.join(settings.PROJECT_DIR, + "data/meris/TEMPORARY_mosaic_MER_FRS_1P_RGB_reduced") + shutil.copytree(src, self.dst) + + def manage(self): + self._create_files() + + args = { + "data_dirs": [{ + "path": os.path.join(settings.PROJECT_DIR, + "data/meris/TEMPORARY_mosaic_MER_FRS_1P_RGB_reduced"), + "search_pattern": "*.tif", + "type": "local" + }], + "eo_metadata": EOMetadata( + "SOMEEOID", + datetime.now(), + datetime.now(), + GEOSGeometry("POLYGON((1 2, 3 2, 3 4, 1 4, 1 2))") + ) + } + + mgr = self.getManager() + self.wrapper = mgr.create(**args) + + # delete one file + path = os.path.join( + settings.PROJECT_DIR, + "data/meris/TEMPORARY_mosaic_MER_FRS_1P_RGB_reduced", + "mosaic_ENVISAT-MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_RGB_reduced.tif" + ) + logger.info("Deleting file at path: %s"%path) + os.remove(path) + + self.synchronize(self.wrapper.getEOID()) + + def testContents(self): + # after sync, the datasets shall be registered. + + wrapper = System.getRegistry().getFromFactory( + "resources.coverages.wrappers.DatasetSeriesFactory", + {"obj_id": "SOMEEOID"} ) - series_1.insert(rectified_1) - series_1.insert(rectified_2) + + self.assertEqual(len(wrapper.getEOCoverages()), 2) + + self.assertEqual(wrapper.getBeginTime(), + min([dataset.getBeginTime() + for dataset in wrapper.getEOCoverages()])) + + self.assertEqual(wrapper.getEndTime(), + max([dataset.getEndTime() + for dataset in wrapper.getEOCoverages()])) + + footprint = wrapper.getEOCoverages()[0].getFootprint().envelope + for dataset in wrapper.getEOCoverages()[1:]: + footprint = footprint.union(dataset.getFootprint()).envelope + self.assertTrue(wrapper.getFootprint().envelope.equals_exact(footprint, 0.001), + "Footprints mismatch.") + + def tearDown(self): + super(DatasetSeriesSynchronizeFileRemovedTestCase, self).tearDown() + shutil.rmtree(self.dst) - series_1 = refresh(series_1) +class DatasetSeriesUpdateLinkCoveragesTestCase(eoxstest.DatasetSeriesUpdateTestCase): + fixtures = eoxstest.EXTENDED_FIXTURES + + def manage(self): + args = { + "obj_id": "MER_FRS_1P_reduced", + "link": { + "coverage_ids": [ + "mosaic_MER_FRS_1PNPDE20060830_100949_000001972050_00423_23523_0079_RGB_reduced" + ] + } + } + + self.update(**args) + + def testContents(self): + dataset_series = System.getRegistry().getFromFactory( + "resources.coverages.wrappers.DatasetSeriesFactory", + {"obj_id": "MER_FRS_1P_reduced"} + ) + + self.assertIn("mosaic_MER_FRS_1PNPDE20060830_100949_000001972050_00423_23523_0079_RGB_reduced", + [coverage.getCoverageId() for coverage in dataset_series.getEOCoverages()]) - series_1.remove(rectified_2) +class DatasetSeriesUpdateLinkCoverages2TestCase(eoxstest.DatasetSeriesUpdateTestCase): + fixtures = eoxstest.EXTENDED_FIXTURES + + def manage(self): + args = { + "obj_id": "MER_FRS_1P_RGB_reduced", + "link": { + "coverage_ids": [ + "MER_FRS_1PNPDE20060830_100949_000001972050_00423_23523_0079_uint16_reduced_compressed" + ] + } + } + + self.update(**args) + + def testContents(self): + dataset_series = System.getRegistry().getFromFactory( + "resources.coverages.wrappers.DatasetSeriesFactory", + {"obj_id": "MER_FRS_1P_RGB_reduced"} + ) + + self.assertIn("MER_FRS_1PNPDE20060830_100949_000001972050_00423_23523_0079_uint16_reduced_compressed", + [coverage.getCoverageId() for coverage in dataset_series.getEOCoverages()]) - series_1 = refresh(series_1) - self.assertEqual(rectified_1.time_extent, series_1.time_extent) - self.assertGeometryEqual( - MultiPolygon( - Polygon.from_bbox(rectified_1.footprint.extent) - ), - series_1.footprint +class DatasetSeriesUpdateUnlinkCoveragesTestCase(eoxstest.DatasetSeriesUpdateTestCase): + fixtures = eoxstest.EXTENDED_FIXTURES + + def manage(self): + args = { + "obj_id": "MER_FRS_1P_reduced", + "unlink": { + "coverage_ids": [ + "MER_FRS_1PNPDE20060830_100949_000001972050_00423_23523_0079_uint16_reduced_compressed" + ] + } + } + + self.update(**args) + + def testContents(self): + dataset_series = System.getRegistry().getFromFactory( + "resources.coverages.wrappers.DatasetSeriesFactory", + {"obj_id": "MER_FRS_1P_reduced"} ) + + self.assertNotIn("MER_FRS_1PNPDE20060830_100949_000001972050_00423_23523_0079_uint16_reduced_compressed", + [coverage.getCoverageId() for coverage in dataset_series.getEOCoverages()]) +#=============================================================================== +# Coverage ID management +#=============================================================================== + +class CoverageIdReserveTestCase(eoxstest.CoverageIdManagementTestCase): + def manage(self): + self.mgr.reserve("SomeCoverageID", until=datetime.now() + timedelta(days=1)) - def test_propagate_eo_metadata_change(self): - rectified_1, series_1 = self.rectified_1, self.series_1 + def testNotAvailable(self): + self.assertFalse(self.mgr.available("SomeCoverageID")) - series_1.insert(rectified_1) - - new_begin_time = parse_datetime("2010-06-11T14:55:23Z") - new_end_time = parse_datetime("2010-06-11T14:55:23Z") +class CoverageIdReserveWithSameRequestIdTestCase(eoxstest.CoverageIdManagementTestCase): + def manage(self): + self.mgr.reserve("SomeCoverageID", "RequestID", until=datetime.now() + timedelta(days=1)) + + def testReserveAgain(self): + try: + self.mgr.reserve("SomeCoverageID", "RequestID", until=datetime.now() + timedelta(days=2)) + except exceptions.CoverageIdReservedError: + self.fail("Reserving with same request ID should not raise.") + +class CoverageIdReserveDefaultUntilTestCase(eoxstest.CoverageIdManagementTestCase): + def manage(self): + self.mgr.reserve("SomeCoverageID") + + def testNotAvailable(self): + self.assertFalse(self.mgr.available("SomeCoverageID")) - rectified_1.begin_time = new_begin_time - rectified_1.end_time = new_end_time - rectified_1.full_clean() - rectified_1.save() +class CoverageIdReleaseTestCase(eoxstest.CoverageIdManagementTestCase): + def manage(self): + self.mgr.reserve("SomeCoverageID", until=datetime.now() + timedelta(days=1)) + self.mgr.release("SomeCoverageID") + + def testAvailable(self): + self.assertTrue(self.mgr.available("SomeCoverageID")) + +class CoverageIdAlreadyReservedTestCase(eoxstest.CoverageIdManagementTestCase): + def manage(self): + self.mgr.reserve("SomeCoverageID", until=datetime.now() + timedelta(days=1)) + + def testReserved(self): + try: + self.mgr.reserve("SomeCoverageID") + except exceptions.CoverageIdReservedError: + pass + else: + self.fail("Reservation of reserved ID did not raise the according " + "exception") - series_1 = refresh(series_1) +class CoverageIdInUseTestCase(eoxstest.CoverageIdManagementTestCase): + fixtures = eoxstest.EXTENDED_FIXTURES + + def testIdInUseRectifiedDataset(self): + try: + self.mgr.reserve("mosaic_MER_FRS_1PNPDE20060822_092058_000001972050_00308_23408_0077_RGB_reduced") + except exceptions.CoverageIdInUseError: + pass + else: + self.fail("Reservation of ID in use did not raise the according " + "exception") + + def testIdInUseRectifiedStitchedMosaic(self): + try: + self.mgr.reserve("mosaic_MER_FRS_1P_RGB_reduced") + except exceptions.CoverageIdInUseError: + pass + else: + self.fail("Reservation of ID in use did not raise the according " + "exception") - self.assertEqual(series_1.begin_time, new_begin_time) - self.assertEqual(series_1.end_time, new_end_time) +class CoverageIdReleaseFailTestCase(eoxstest.CoverageIdManagementTestCase): + def testRelease(self): + try: + self.mgr.release("SomeID", True) + except exceptions.CoverageIdReleaseError: + pass + else: + self.fail("No exception thrown when unreserved ID was released.") +class CoverageIdAvailableTestCase(eoxstest.CoverageIdManagementTestCase): + fixtures = eoxstest.EXTENDED_FIXTURES + + def testNotAvailable(self): + self.assertFalse(self.mgr.available("mosaic_MER_FRS_1PNPDE20060822_092058_000001972050_00308_23408_0077_RGB_reduced")) - def test_insert_in_self_fails(self): - series_1 = self.series_1 - with self.assertRaises(ValidationError): - series_1.insert(series_1) + def testAvailable(self): + self.assertTrue(self.mgr.available("SomeID")) + +class CoverageIdRequestIdTestCase(eoxstest.CoverageIdManagementTestCase): + def manage(self): + self.mgr.reserve("SomeCoverageID", "SomeRequestID", until=datetime.now() + timedelta(days=1)) + self.mgr.reserve("SomeCoverageID2", "SomeRequestID", until=datetime.now() + timedelta(days=1)) + + def testCoverageIds(self): + self.assertItemsEqual( + ["SomeCoverageID", "SomeCoverageID2"], + self.mgr.getCoverageIds("SomeRequestID") + ) + + def testRequestId(self): + self.assertEqual("SomeRequestID", self.mgr.getRequestId("SomeCoverageID")) + self.assertEqual("SomeRequestID", self.mgr.getRequestId("SomeCoverageID2")) + + +#=============================================================================== +# CLI Command tests +#=============================================================================== + +# eoxs_register_dataset + +class RegisterLocalDatasetSimpleTestCase(eoxstest.CommandRegisterDatasetTestCase): + kwargs = { + "d": "autotest/data/meris/MER_FRS_1P_reduced/ENVISAT-MER_FRS_1PNPDE20060822_092058_000001972050_00308_23408_0077_uint16_reduced_compressed.tif", + "rangetype": "RGB" + } + coverages_to_be_registered = [ + {"eo_id": "MER_FRS_1PNPDE20060822_092058_000001972050_00308_23408_0077_uint16_reduced_compressed"} + ] + + +class RegisterLocalDatasetWithCoverageIdTestCase(eoxstest.CommandRegisterDatasetTestCase): + kwargs = { + "d": "autotest/data/meris/MER_FRS_1P_reduced/ENVISAT-MER_FRS_1PNPDE20060822_092058_000001972050_00308_23408_0077_uint16_reduced_compressed.tif", + "rangetype": "RGB", + "i": "someCoverageID" + } + coverages_to_be_registered = [{"coverage_id": "someCoverageID"}] + +class RegisterLocalDatasetMultipleTestCase(eoxstest.CommandRegisterDatasetTestCase): + kwargs = { + "d": ("autotest/data/meris/MER_FRS_1P_reduced/ENVISAT-MER_FRS_1PNPDE20060822_092058_000001972050_00308_23408_0077_uint16_reduced_compressed.tif", + "autotest/data/meris/MER_FRS_1P_reduced/ENVISAT-MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_uint16_reduced_compressed.tif", + "autotest/data/meris/MER_FRS_1P_reduced/ENVISAT-MER_FRS_1PNPDE20060830_100949_000001972050_00423_23523_0079_uint16_reduced_compressed.tif"), + "rangetype": "RGB" + } + coverages_to_be_registered = [ + {"eo_id": "MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_uint16_reduced_compressed"}, + {"eo_id": "MER_FRS_1PNPDE20060822_092058_000001972050_00308_23408_0077_uint16_reduced_compressed"}, + {"eo_id": "MER_FRS_1PNPDE20060830_100949_000001972050_00423_23523_0079_uint16_reduced_compressed"} + ] + +class RegisterRemoteDatasetTestCase(eoxstest.CommandRegisterDatasetTestCase): + kwargs = { + "d": "test/mosaic_MER_FRS_1P_RGB_reduced/mosaic_ENVISAT-MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_RGB_reduced.tif", + "m": "test/mosaic_MER_FRS_1P_RGB_reduced/mosaic_ENVISAT-MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_RGB_reduced.xml", + "mode": "ftp", + "host": "hma.eox.at", + "user": "anonymous", + "rangetype": "RGB" + } + coverages_to_be_registered = [ + {"eo_id": "mosaic_MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_RGB_reduced_ftp"} + ] + + +# TODO: TCs for rasdaman coverages + +# no rasdaman publicly available + +# TODO: TCs for default-... and visible options + +class RegisterLocalDatasetVisibleTestCase(eoxstest.CommandRegisterDatasetTestCase): + kwargs = { + "d": "autotest/data/meris/MER_FRS_1P_reduced/ENVISAT-MER_FRS_1PNPDE20060822_092058_000001972050_00308_23408_0077_uint16_reduced_compressed.tif", + "rangetype": "RGB", + "visible": "True" + } + coverages_to_be_registered = [ + {"eo_id": "MER_FRS_1PNPDE20060822_092058_000001972050_00308_23408_0077_uint16_reduced_compressed"} + ] + + def testCoverageVisible(self): + coverage = self.getCoveragesToBeRegistered().values()[0] + self.assertTrue(coverage.getAttrValue("visible")) + + +class RegisterLocalDatasetInvisibleTestCase(eoxstest.CommandRegisterDatasetTestCase): + kwargs = { + "d": "autotest/data/meris/MER_FRS_1P_reduced/ENVISAT-MER_FRS_1PNPDE20060822_092058_000001972050_00308_23408_0077_uint16_reduced_compressed.tif", + "rangetype": "RGB", + "invisible": "True" + } + coverages_to_be_registered = [ + {"eo_id": "MER_FRS_1PNPDE20060822_092058_000001972050_00308_23408_0077_uint16_reduced_compressed"} + ] + + def testCoverageInvisible(self): + coverage = self.getCoveragesToBeRegistered().values()[0] + self.assertFalse(coverage.getAttrValue("visible")) + + +class RegisterLocalDatasetDefaultsTestCase(eoxstest.CommandRegisterDatasetTestCase): + srid = 3035 + size = (100, 100) + extent = (3000000, -2400000, 4400000, -1200000) + poly = MultiPolygon([Polygon.from_bbox((0, 0, 10, 10))]) + begin_time = datetime(2012, 06, 10, 12, 30, tzinfo=UTCOffsetTimeZoneInfo()) + end_time = datetime(2012, 06, 10, 12, 45, tzinfo=UTCOffsetTimeZoneInfo()) + + kwargs = { + "d": "autotest/data/meris/mosaic_cache/mosaic_MER_FRS_1P_RGB_reduced/tiles/000/000/tile_merged_000001_000000.tiff", + "rangetype": "RGB", + "default-srid": str(srid), + "default-size": "%s,%s" % size, + "default-extent": "%s,%s,%s,%s" % extent, + "default-begin-time": isotime(begin_time), + "default-end-time": isotime(end_time), + "default-footprint": poly.wkt, + "coverage-id": "A", + "traceback": "True" + + } + coverages_to_be_registered = [ + {"coverage_id": "A"} + ] + + def testDefaults(self): + coverage = self.getDatasetById("A") + + self.assertEqual(self.srid, coverage.getSRID()) + self.assertEqual(self.size, coverage.getSize()) + self.assertEqual(self.extent, coverage.getExtent()) + self.assertEqual(self.begin_time, coverage.getBeginTime()) + self.assertEqual(self.end_time, coverage.getEndTime()) + self.assertEqual(self.poly, coverage.getFootprint()) - def test_circular_reference_fails(self): - series_1, series_2 = self.series_1, self.series_2 - with self.assertRaises(ValidationError): - series_1.insert(series_2) - series_2.insert(series_1) +# TODO: TCs for datasetseries/stitchedmosaic insertions +# eoxs_add_dataset_series -class MetadataFormatTests(GeometryMixIn, TestCase): - def test_native_reader(self): - xml = """ - - some_unique_id - 2013-08-27T10:00:00Z - 2013-08-27T10:00:10Z - - - 0 0 20 0 20 10 0 10 0 0 - - - - 10 10 40 10 40 30 10 30 10 10 - - - - - """ - reader = native.NativeFormat(env) - self.assertTrue(reader.test(xml)) - values = reader.read(xml) +# eoxs_synchronize - self.assertEqual({ - "identifier": "some_unique_id", - "begin_time": datetime(2013, 8, 27, 10, 0, 0, tzinfo=utc), - "end_time": datetime(2013, 8, 27, 10, 0, 10, tzinfo=utc), - "footprint": MultiPolygon( - Polygon.from_bbox((0, 0, 10, 20)), - Polygon.from_bbox((10, 10, 30, 40)) - ), - "format": "native" - }, values) - def test_native_writer(self): - values = { - "identifier": "some_unique_id", - "begin_time": datetime(2013, 8, 27, 10, 0, 0, tzinfo=utc), - "end_time": datetime(2013, 8, 27, 10, 0, 10, tzinfo=utc), - "footprint": MultiPolygon( - Polygon.from_bbox((0, 0, 10, 20)), - Polygon.from_bbox((10, 10, 30, 40)) - ) - } - writer = native.NativeFormat(env) +# eoxs_insert_into_series - f = StringIO() - writer.write(values, f, pretty=True) - self.assertEqual(dedent("""\ - - some_unique_id - 2013-08-27T10:00:00Z - 2013-08-27T10:00:10Z - - - 0.000000 0.000000 20.000000 0.000000 20.000000 10.000000 0.000000 10.000000 0.000000 0.000000 - - - 10.000000 10.000000 40.000000 10.000000 40.000000 30.000000 10.000000 30.000000 10.000000 10.000000 - - - - """), dedent(f.getvalue())) - - def test_eoom_reader(self): - xml = """ - - - - - 2006-08-16T09:09:29Z - 2006-08-16T09:12:46Z - - - - - 2006-08-16T11:03:08Z - - - - - - - ENVISAT - - - - - MERIS - - - - - OPTICAL - - - - - - - - - - - - - - 20 10 40 10 40 30 20 30 20 10 - - - - - - - - - 60 50 80 50 80 70 60 70 60 50 - - - - - - - - - - - - MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_uint16_reduced_compressed - NOMINAL - MER_FRS_1P - ARCHIVED - - - PDHS-E - - - - - PDHS-E - - - - - - """ - - reader = eoom.EOOMFormatReader(env) - self.assertTrue(reader.test(xml)) - values = reader.read(xml) - - expected = { - "identifier": "MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_uint16_reduced_compressed", - "begin_time": datetime(2006, 8, 16, 9, 9, 29, tzinfo=utc), - "end_time": datetime(2006, 8, 16, 9, 12, 46, tzinfo=utc), - "footprint": MultiPolygon( - Polygon.from_bbox((10, 20, 30, 40)), - Polygon.from_bbox((50, 60, 70, 80)) - ), - 'format': 'eogml' - } - self.assertEqual(expected, values) +class InsertByIdTestCase(eoxstest.CommandInsertTestCase): + args = ("MER_FRS_1PNPDE20060830_100949_000001972050_00423_23523_0079_uint16_reduced_compressed", + "MER_FRS_1P_RGB_reduced") + + datasets_to_be_inserted = ["MER_FRS_1PNPDE20060830_100949_000001972050_00423_23523_0079_uint16_reduced_compressed"] +class InsertByIdExplicitTestCase(eoxstest.CommandInsertTestCase): + args = ( + "--datasets", + "MER_FRS_1PNPDE20060830_100949_000001972050_00423_23523_0079_uint16_reduced_compressed", + "MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_uint16_reduced_compressed", + "MER_FRS_1PNPDE20060822_092058_000001972050_00308_23408_0077_uint16_reduced_compressed", + "--dataset-series", + "MER_FRS_1P_RGB_reduced" + ) + datasets_to_be_inserted = [ + "MER_FRS_1PNPDE20060830_100949_000001972050_00423_23523_0079_uint16_reduced_compressed", + "MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_uint16_reduced_compressed", + "MER_FRS_1PNPDE20060822_092058_000001972050_00308_23408_0077_uint16_reduced_compressed" + ] + + +class InsertByUnknownIdFaultTestCase(eoxstest.CoverageCommandFaultTestCase): + args = ("invalidID", "MER_FRS_1P_RGB_reduced") + + +# eoxs_remove_from_series + +class ExcludeByIdTestCase(eoxstest.CommandExcludeTestCase): + args = ("mosaic_MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_RGB_reduced", + "MER_FRS_1P_RGB_reduced") + + datasets_to_be_excluded = ["mosaic_MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_RGB_reduced"] - def test_dimap_reader(self): - xml = """ - """ - # TODO: find example DIMAP - +class ExcludeByIdExplicitTestCase(eoxstest.CommandExcludeTestCase): + args = ( + "--datasets", + "mosaic_MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_RGB_reduced", + "mosaic_MER_FRS_1PNPDE20060830_100949_000001972050_00423_23523_0079_RGB_reduced", + "mosaic_MER_FRS_1PNPDE20060822_092058_000001972050_00308_23408_0077_RGB_reduced", + "--dataset-series", + "MER_FRS_1P_RGB_reduced" + ) + datasets_to_be_excluded = [ + "mosaic_MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_RGB_reduced", + "mosaic_MER_FRS_1PNPDE20060830_100949_000001972050_00423_23523_0079_RGB_reduced", + "mosaic_MER_FRS_1PNPDE20060822_092058_000001972050_00308_23408_0077_RGB_reduced" + ] +class ExcludeByUnknownIdFaultTestCase(eoxstest.CoverageCommandFaultTestCase): + args = ("invalidID", "MER_FRS_1P_RGB_reduced") diff -Nru eoxserver-0.4.0beta2/eoxserver/resources/coverages/util.py eoxserver-0.3.2/eoxserver/resources/coverages/util.py --- eoxserver-0.4.0beta2/eoxserver/resources/coverages/util.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/resources/coverages/util.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,240 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# Stephan Meissl -# Stephan Krause -# -#------------------------------------------------------------------------------- -# Copyright (C) 2011 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -import operator - -from django.db.models import Min, Max -from django.contrib.gis.db.models import Union -from django.contrib.gis.geos import MultiPolygon, Polygon -from django.utils.timezone import is_naive, make_aware, get_current_timezone -from django.utils.dateparse import parse_datetime - -from eoxserver.contrib import gdal - - -def pk_equals(first, second): - """ Helper function to check if the ``pk`` attributes of two models are - equal. - """ - return first.pk == second.pk - - -def detect_circular_reference(eo_object, collection, supercollection_getter, - equals=pk_equals): - """ Utility function to detect circular references in model hierarchies. - - :param eo_object: the :class:`EOObject - ` to check - :param collection: the :class:`Collection - ` to - check against - :param supercollection_getter: a callable that shall return the collections - a single collection is contained in - :param equals: the equality checking function; defaults to :func:`pk_equals` - """ - - #print "Checking for circular reference: %s %s" %(eo_object, collection) - if equals(eo_object, collection): - #print "Circular reference detected: %s %s" %(eo_object, collection) - return True - - for collection in supercollection_getter(collection): - if detect_circular_reference(eo_object, collection, - supercollection_getter, equals): - return True - - return False - - -def collect_eo_metadata(qs, insert=None, exclude=None, bbox=False): - """ Helper function to collect EO metadata from all EOObjects in a queryset, - plus additionals from a list and exclude others from a different list. If - bbox is ``True`` then the returned polygon will only be a minimal bounding - box of the collected footprints. - - :param qs: the :class:`django.db.QuerySet` to collect all EO metadata from - :param insert: an iterable of all objects that are to be inserted (thus not - entailed in the queryset) and should be considered when - collection metadata - :param exclude: an iterable of objects that are considered to be excluded - from the list and should not be used for metadata collection - :param bbox: if this is set to ``True`` the footprint will only be - represented as a minimal BBOX polygon of all collected - footprints. This is preferable for large collections. - """ - - values = qs.exclude( - pk__in=[eo_object.pk for eo_object in exclude or ()] - ).aggregate( - begin_time=Min("begin_time"), end_time=Max("end_time"), - footprint=Union("footprint") - ) - - begin_time, end_time, footprint = ( - values["begin_time"], values["end_time"], values["footprint"] - ) - - # workaround for Django 1.4 bug: aggregate times are strings - if isinstance(begin_time, basestring): - begin_time = parse_datetime(begin_time) - - if isinstance(end_time, basestring): - end_time = parse_datetime(end_time) - - if begin_time and is_naive(begin_time): - begin_time = make_aware(begin_time, get_current_timezone()) - if end_time and is_naive(end_time): - end_time = make_aware(end_time, get_current_timezone()) - - for eo_object in insert or (): - if begin_time is None: - begin_time = eo_object.begin_time - elif eo_object.begin_time is not None: - begin_time = min(begin_time, eo_object.begin_time) - - if end_time is None: - end_time = eo_object.end_time - elif eo_object.end_time is not None: - end_time = max(end_time, eo_object.end_time) - - if footprint is None: - footprint = eo_object.footprint - elif eo_object.footprint is not None: - footprint = footprint.union(eo_object.footprint) - - if not isinstance(footprint, MultiPolygon) and footprint is not None: - footprint = MultiPolygon(footprint) - - if bbox and footprint is not None: - footprint = MultiPolygon(Polygon.from_bbox(footprint.extent)) - - return begin_time, end_time, footprint - - -def is_same_grid(coverages, epsilon=1e-10): - """ Function to determine if the given coverages share the same base grid. - Returns a boolean value, whether or not the coverages share a common grid. - - :param coverages: an iterable of :class:`Coverages - ` - :param epsilon: the maximum difference each grid can have to be still - considered equal - """ - - if len(coverages) < 2: - raise ValueError("Not enough coverages given.") - - first = coverages[0] - first_ext = first.extent - first_res = first.resolution - first_srid = first.srid - first_proj = first.projection - - e = epsilon - - for other in coverages[1:]: - other_ext = other.extent - other_res = other.resolution - - # check projection/srid - if first_srid != other.srid or first_proj != other.projection: - return False - - # check dimensions - if len(first_res) != len(other_res): - return False - - # check offset vectors - for a, b in zip(first_res, other_res): - if abs(a - b) > epsilon: - return False - - # check base grids - diff_origins = tuple(map(operator.sub, first_ext[:2], other_ext[:2])) - - v = tuple(map(operator.div, diff_origins, other_res)) - - if (abs(v[0] - round(v[0])) > e or abs(v[1] - round(v[1])) > e): - return False - - return True - - -def parse_raw_value(raw_value, dt): - """ Parse a raw value from a string according to a given data type. - - :param raw_value: the raw string value - :param dt: the data type enumeration - :returns: the parsed value - """ - if raw_value is None: # allow null values - return None - - is_float = False - is_complex = False - - if dt in gdal.GDT_INTEGRAL_TYPES: - value = int(raw_value) - - elif dt in gdal.GDT_FLOAT_TYPES: - value = float(raw_value) - is_float = True - - elif dt in gdal.GDT_INTEGRAL_COMPLEX_TYPES: - value = complex(raw_value) - is_complex = True - - elif dt in gdal.GDT_FLOAT_COMPLEX_TYPES: - value = complex(raw_value) - is_complex = True - is_float = True - - else: - value = None - - # range check makes sense for integral values only - if not is_float: - limits = gdal.GDT_NUMERIC_LIMITS.get(dt) - - if limits and value is not None: - def within(v, low, high): - return (v >= low and v <= high) - - error = ValueError( - "Stored value is out of the limits for the data type" - ) - if not is_complex and not within(value, *limits): - raise error - elif is_complex: - if (not within(value.real, limits[0].real, limits[1].real) - or not within(value.real, limits[0].real, limits[1].real)): - raise error - - return value diff -Nru eoxserver-0.4.0beta2/eoxserver/resources/coverages/validators.py eoxserver-0.3.2/eoxserver/resources/coverages/validators.py --- eoxserver-0.4.0beta2/eoxserver/resources/coverages/validators.py 1970-01-01 00:00:00.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/resources/coverages/validators.py 2013-12-10 14:57:00.000000000 +0000 @@ -0,0 +1,92 @@ +#------------------------------------------------------------------------------- +# $Id$ +# +# Project: EOxServer +# Authors: Stephan Krause +# Stephan Meissl +# +#------------------------------------------------------------------------------- +# Copyright (C) 2011 EOX IT Services GmbH +# +# 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 of this Software or works derived from this 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. +#------------------------------------------------------------------------------- + +import os.path +from StringIO import StringIO + +from lxml import etree + +from django.conf import settings +from django.core.exceptions import ValidationError + +class EOOMSchema(object): + # TODO: Change to online schema once available. + # Alternatively add configuration parameter to eoxserver.conf + SCHEMA_LOCATION = os.path.join(settings.PROJECT_DIR, "..", "schemas", "omeo", "eop.xsd") + schema = None + + @classmethod + def getSchema(cls): + if cls.schema is None: + f = open(cls.SCHEMA_LOCATION) + cls.schema = etree.XMLSchema(etree.parse(f)) + f.close() + + return cls.schema + +def validateEOOM(xml): + schema = EOOMSchema.getSchema() + + try: + schema.assertValid(etree.fromstring(xml)) + except etree.Error as e: + raise ValidationError(str(e)) + +#TODO: +#def validateEOMetadata(...): +# """ +# This method validates the consistency of the EO Metadata record, i.e.: +# * check that the begin time in timestamp_begin is the same as in EO GML +# * check that the end time in timestamp_end is the same as in EO GML +# * check that the footprint is the same as in EO GML +# """ +# EPSILON = 1e-10 +# +# if self.eo_gml: +# md_int = MetadataInterfaceFactory.getMetadataInterface(self.eo_gml, "eogml") +# +# if self.timestamp_begin != md_int.getBeginTime().replace(tzinfo=None): +# raise ValidationError("EO GML acquisition begin time does not match.") +# if self.timestamp_end != md_int.getEndTime().replace(tzinfo=None): +# raise ValidationError("EO GML acquisition end time does not match.") +# if self.footprint is not None: +# if not self.footprint.equals_exact(GEOSGeometry(md_int.getFootprint()), EPSILON * max(self.footprint.extent)): # compare the footprints with a tolerance in order to account for rounding and string conversion errors +# raise ValidationError("EO GML footprint does not match.") + +def validateCoverageIDnotInEOOM(coverageID, xml): + if xml is None or len(xml) == 0: + return + + tree = etree.fromstring(xml.encode("ascii")) + element = tree.find(".//*[@{http://www.opengis.net/gml/3.2}id='%s']" % coverageID) + + if element is not None: + raise ValidationError("The XML element '%s' contains a gml:id " + "which is equal to the coverage ID '%s'." % + (element.tag, coverageID)) diff -Nru eoxserver-0.4.0beta2/eoxserver/resources/coverages/wrappers.py eoxserver-0.3.2/eoxserver/resources/coverages/wrappers.py --- eoxserver-0.4.0beta2/eoxserver/resources/coverages/wrappers.py 1970-01-01 00:00:00.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/resources/coverages/wrappers.py 2013-12-10 14:57:00.000000000 +0000 @@ -0,0 +1,1856 @@ +#----------------------------------------------------------------------- +# $Id$ +# +# Project: EOxServer +# Authors: Stephan Krause +# Stephan Meissl +# Martin Paces +# +#------------------------------------------------------------------------------- +# Copyright (C) 2011 EOX IT Services GmbH +# +# 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 of this Software or works derived from this 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. +#----------------------------------------------------------------------- + +""" +This module provides implementations of coverage interfaces as +defined in :mod:`eoxserver.resources.coverages.interfaces`. These +classes wrap the resources stored in the database and augment them +with additional application logic. +""" + +import os.path +import operator +import logging + +from django.contrib.gis.geos import GEOSGeometry +from django.contrib.gis.geos.geometry import MultiPolygon, Polygon +from django.contrib.gis.db.models import Union + +from eoxserver.core.system import System +from eoxserver.core.resources import ( + ResourceFactoryInterface, ResourceWrapper, ResourceFactory +) +from eoxserver.core.exceptions import InternalError +from eoxserver.resources.coverages.models import ( + PlainCoverageRecord, RectifiedDatasetRecord, + ReferenceableDatasetRecord, RectifiedStitchedMosaicRecord, + DatasetSeriesRecord, EOMetadataRecord, + ExtentRecord, LineageRecord, RangeTypeRecord, + LayerMetadataRecord +) +from eoxserver.resources.coverages.interfaces import ( + CoverageInterface, RectifiedDatasetInterface, + ReferenceableDatasetInterface, RectifiedStitchedMosaicInterface, + DatasetSeriesInterface +) +from eoxserver.resources.coverages.rangetype import ( + Band, NilValue, RangeType +) + + +logger = logging.getLogger(__name__) + +#----------------------------------------------------------------------- +# Wrapper implementations +#----------------------------------------------------------------------- + +class CoverageWrapper(ResourceWrapper): + """ + This is the base class for all coverage wrappers. It is a partial + implementation of :class:`~.CoverageInterface`. It inherits from + :class:`~.ResourceWrapper`. + """ + + #------------------------------------------------------------------- + # CoverageInterface methods + #------------------------------------------------------------------- + + @property + def __model(self): + return self._ResourceWrapper__model + + + def isAutomatic(self): + """ + Returns ``True`` if the coverage is automatic or ``False`` otherwise. + """ + + return self.__model.automatic + + + def getCoverageId(self): + """ + Returns the Coverage ID. + """ + + return self.__model.coverage_id + + def getCoverageSubtype(self): + """ + This method shall return the coverage subtype as defined in + the WCS 2.0 EO-AP (EO-WCS). It must be overridden by concrete + coverage wrappers. By default this method raises + :exc:`~.InternalError`. + + See the definition of + :meth:`~.CoverageInterface.getCoverageSubtype` in + :class:`~.CoverageInterface` for possible return values. + """ + + raise InternalError("Not implemented.") + + def getType(self): + """ + This method shall return the internal coverage type code. It + must be overridden by concrete coverage wrappers. By default + this method raises :exc:`~.InternalError`. + + See the definition of :meth:`~.CoverageInterface.getType` in + :class:`~.CoverageInterface` for possible return values. + """ + + raise InternalError("Not implemented.") + + def getSize(self): + """ + This method shall return a tuple ``(xsize, ysize)`` for the + coverage wrapped by the implementation. It has to be overridden + by concrete coverage wrappers. By default this method raises + :exc:`~.InternalError`. + """ + + raise InternalError("Not implemented.") + + def getRangeType(self): + """ + This method returns the range type of the coverage as + :class:`~.RangeType` object. + """ + + range_type = RangeType( + name=self.__model.range_type.name, + data_type=self.__model.range_type.data_type + ) + + for band_record in self.__model.range_type.bands.order_by("rangetype2band__no"): + nil_values = [ + NilValue(value=nv.value, reason=nv.reason)\ + for nv in band_record.nil_values.all() + ] + + range_type.addBand(Band( + name=band_record.name, + identifier=band_record.identifier, + definition=band_record.definition, + description=band_record.description, + uom=band_record.uom, + nil_values=nil_values, + gdal_interpretation=band_record.gdal_interpretation + )) + + return range_type + + def getDataStructureType(self): + """ + Returns the data structure type of the coverage. To be implemented + by subclasses, raises :exc:`~.InternalError` by default. + """ + raise InternalError("Not implemented.") + + def getData(self): + """ + Returns the a :class:`~.CoverageDataWrapper` object that wraps the + coverage data, raises :exc:`~.InternalError` by default. + """ + raise InternalError("Not implemented.") + + def getLayerMetadata(self): + """ + Returns a list of ``(metadata_key, metadata_value)`` pairs + that represent MapServer metadata tags to be attached to + MapServer layers. + """ + + return self.__model.layer_metadata.values_list("key", "value") + + def matches(self, filter_exprs): + """ + Returns True if the Coverage matches the given filter + expressions and False otherwise. + """ + + for filter_expr in filter_exprs: + filter = System.getRegistry().findAndBind( + intf_id = "core.filters.Filter", + params = { + "core.filters.res_class_id": self.__class__.__get_impl_id__(), + "core.filters.expr_class_id": filter_expr.__class__.__get_impl_id__(), + } + ) + + if not filter.resourceMatches(filter_expr, self): + return False + + return True + + def _get_create_dict(self, params): + create_dict = super(CoverageWrapper, self)._get_create_dict(params) + + if "coverage_id" not in params: + raise InternalError( + "Missing mandatory 'coverage_id' parameter for RectifiedDataset creation." + ) + + if "range_type_name" not in params: + raise InternalError( + "Missing mandatory 'range_type_name' parameter for RectifiedDataset creation." + ) + + create_dict["coverage_id"] = params["coverage_id"] + + try: + create_dict["range_type"] = RangeTypeRecord.objects.get( + name=params["range_type_name"] + ) + except RangeTypeRecord.DoesNotExist: + raise InternalError( + "Unknown range type name '%s'" % params["range_type_name"] + ) + + if "data_source" in params and params["data_source"] is not None: + create_dict["automatic"] = params.get("automatic", True) + create_dict["data_source"] = params["data_source"].getRecord() + else: + create_dict["automatic"] = False + create_dict["data_source"] = None + + layer_metadata = params.get("layer_metadata") + + if layer_metadata: + create_dict["layer_metadata"] = [] + + for key, value in layer_metadata.items(): + create_dict["layer_metadata"].append( + LayerMetadataRecord.objects.get_or_create( + key=key, value=value + )[0] + ) + + return create_dict + + def _updateModel(self, link_kwargs, unlink_kwargs, set_kwargs): + super(CoverageWrapper, self)._updateModel(link_kwargs, unlink_kwargs, set_kwargs) + + coverage_id = set_kwargs.get("coverage_id") + range_type_name = set_kwargs.get("range_type_name") + layer_metadata = set_kwargs.get("layer_metadata", {}) + automatic = set_kwargs.get("automatic") + data_source = set_kwargs.get("data_source") + + if coverage_id is not None: + self.__model.coverage_id = coverage_id + + if range_type_name is not None: + self.__model.range_type = RangeTypeRecord.objects.get( + name=range_type_name + ) + + for key, value in layer_metadata: + pass # TODO implement setting + + if automatic is not None: + self.__model.automatic = automatic + + if data_source: + pass # TODO + +class CommonGridWrapper(object): + """ + Common base class shared by :class:`~.RectifiedGridWrapper` + and :class:`~.ReferenceableGridWrapper` + """ + + @property + def __model(self): + return self._ResourceWrapper__model + + def _get_create_dict(self, params): + create_dict = super(CommonGridWrapper, self)._get_create_dict(params) + + if "geo_metadata" not in params: + raise InternalError( + "Missing mandatory 'coverage_id' parameter for dataset creation." + ) + + geo_metadata = params["geo_metadata"] + + create_dict["extent"] = ExtentRecord.objects.create( + srid = geo_metadata.srid, + size_x = geo_metadata.size_x, + size_y = geo_metadata.size_y, + minx = geo_metadata.extent[0], + miny = geo_metadata.extent[1], + maxx = geo_metadata.extent[2], + maxy = geo_metadata.extent[3] + ) + + return create_dict + + def _updateModel(self, link_kwargs, unlink_kwargs, set_kwargs): + super(CommonGridWrapper, self)._updateModel(link_kwargs, unlink_kwargs, set_kwargs) + + geo_md = set_kwargs.get("geo_metadata") + if geo_md: + extent = self.__model.extent + extent.srid = geo_md.srid + extent.size_x = geo_md.size_x + extent.size_y = geo_md.size_y + extent.minx = geo_md.extent[0] + extent.miny = geo_md.extent[1] + extent.maxx = geo_md.extent[2] + extent.maxy = geo_md.extent[3] + + extent.save() + + def getSRID(self): + """ + Returns the SRID of the coverage CRS. + """ + + return self.__model.extent.srid + + def getExtent(self): + """ + Returns the coverage extent as a 4-tuple of floating point + coordinates ``(minx, miny, maxx, maxy)`` expressed in the + coverage CRS as defined by the SRID returned by :meth:`getSRID`. + """ + + return ( + self.__model.extent.minx, + self.__model.extent.miny, + self.__model.extent.maxx, + self.__model.extent.maxy + ) + +class RectifiedGridWrapper(CommonGridWrapper): + """ + This wrapper is intended as a mix-in for coverages that rely on a + rectified grid. It implements :class:`~.RectifiedGridInterface`. + """ + + def getResolution(self): + """ + Returns the coverage resolution as a 2-tuple of float values for the + x and y axes ``(resx, resy)`` expressed in the unit of measure of the + coverage CRS as defined by the SRID returned by :meth:`getSRID`. + """ + extent = self.getExtent() + size = self.getSize() + return ( + (extent[2] - extent[0]) / float(size[0]), + (extent[3] - extent[1]) / float(size[1]) + ) + +class ReferenceableGridWrapper(CommonGridWrapper): + """ + This wrapper is intended as a mix-in for coverages that rely on + referenceable grids. It implements :class:`~.ReferenceableGridInterface`. + """ + pass + + +class PackagedDataWrapper(object): + """ + This wrapper is intended as a mix-in for coverages that are stored as + data packages. + """ + @property + def __model(self): + return self._ResourceWrapper__model + + # TODO: replace by appropriate data package implementation + + #def _createFileRecord(self, file_info): + #return FileRecord.objects.create( + #path = file_info.filename, + #metadata_path = file_info.md_filename, + #metadata_format = file_info.md_format + #) + + #def _updateFileRecord(self, file_info): + #self.__model.file.path = file_info.filename + #self.__model.file.metadata_path = file_info.md_filename + #self.__model.file.metadata_format = file_info.md_format + + def getDataStructureType(self): + """ + Returns the data structure type of the underlying data package + """ + # TODO: this implementation is inefficient as the data package wrapper + # is discarded and cannot be reused, thus forcing a second database hit + # when accessing the actual data + return self.getData().getDataStructureType() + + def getData(self): + """ + Return the data package wrapper associated with the coverage, i.e. an + instance of a subclass of :class:`~.DataPackageWrapper`. + """ + return System.getRegistry().getFromFactory( + factory_id = "resources.coverages.data.DataPackageFactory", + params = { + "record": self.__model.data_package + } + ) + +class TiledDataWrapper(object): + """ + This wrapper is intended as a mix-in for coverages that are stored in tile + indices. + """ + @property + def __model(self): + return self._ResourceWrapper__model + + def getDataStructureType(self): + """ + Returns ``"index"``. + """ + # this is a shortcut; it has to be overridden if any time different + # data structure types for tiled data should be implemented + return "index" + + def getData(self): + """ + Returns a :class:`TileIndexWrapper` instance. + """ + return System.getRegistry().getFromFactory( + factory_id = "resources.coverages.data.TileIndexFactory", + params = { + "record": self.__model.tile_index + } + ) + +class EOMetadataWrapper(object): + """ + This wrapper class is intended as a mix-in for EO coverages and + dataset series as defined in the WCS 2.0 EO-AP (EO-WCS). + """ + @property + def __model(self): + return self._ResourceWrapper__model + + def _createEOMetadataRecord(self, file_info): + return EOMetadataRecord.objects.create( + timestamp_begin = file_info.timestamp_begin, + timestamp_end = file_info.timestamp_end, + footprint = GEOSGeometry(file_info.footprint_wkt), + eo_gml = file_info.md_xml_text + ) + + def _updateEOMetadataRecord(self, file_info): + self.__model.eo_metadata.timestamp_begin = \ + file_info.timestamp_begin + self.__model.eo_metadata.timestamp_end = \ + file_info.timestamp_end + self.__model.eo_metadata.footprint = \ + GEOSGeometry(file_info.footprint_wkt) + self.__model.eo_metadata.eo_gml = file_info.md_xml_text + + def _get_create_dict(self, params): + create_dict = super(EOMetadataWrapper, self)._get_create_dict(params) + + if "eo_metadata" not in params: + raise InternalError( + "Missing mandatory 'eo_metadata' parameter for RectifiedDataset creation." + ) + + eo_metadata = params["eo_metadata"] + + create_dict["eo_id"] = eo_metadata.getEOID() + + md_format = eo_metadata.getMetadataFormat() + if md_format and md_format.getName() == "eogml": + eo_gml = eo_metadata.getRawMetadata() + else: + eo_gml = "" + + footprint = eo_metadata.getFootprint() + if type(footprint) != MultiPolygon: + footprint = MultiPolygon(footprint) + + create_dict["eo_metadata"] = EOMetadataRecord.objects.create( + timestamp_begin = eo_metadata.getBeginTime(), + timestamp_end = eo_metadata.getEndTime(), + footprint = footprint, + eo_gml = eo_gml + ) + + return create_dict + + def _updateModel(self, link_kwargs, unlink_kwargs, set_kwargs): + super(EOMetadataWrapper, self)._updateModel(link_kwargs, unlink_kwargs, set_kwargs) + + eo_id = set_kwargs.get("eo_id") + eo_md = set_kwargs.get("eo_metadata") + if eo_md: + footprint = eo_md.getFootprint() + if type(footprint) != MultiPolygon: + footprint = MultiPolygon(footprint) + + record = self.__model.eo_metadata + record.timestamp_begin = eo_md.getBeginTime() + record.timestamp_end = eo_md.getEndTime() + record.footprint = footprint + + md_format = eo_md.getMetadataFormat() + if md_format and md_format.getName() == "eogml": + record.eo_gml = eo_md.getRawMetadata() + else: + record.eo_gml = "" + + self.__model.eo_id = eo_md.getEOID() + + self.__model.eo_metadata = record + self.__model.eo_metadata.save() + + if eo_id is not None: + self.__model.eo_id = eo_id + + + def getEOID(self): + """ + Returns the EO ID of the object. + """ + + return self.__model.eo_id + + def getBeginTime(self): + """ + Returns the acquisition begin time as :class:`datetime.datetime` + object. + """ + + return self.__model.eo_metadata.timestamp_begin + + def getEndTime(self): + """ + Returns the acquisition end time as :class:`datetime.datetime` + object. + """ + + return self.__model.eo_metadata.timestamp_end + + def getFootprint(self): + """ + Returns the acquisition footprint as + :class:`django.contrib.gis.geos.GEOSGeometry` object in + the EPSG:4326 CRS. + """ + + return self.__model.eo_metadata.footprint + + def getWGS84Extent(self): + """ + Returns the WGS 84 extent as 4-tuple of floating point + coordinates ``(minlon, minlat, maxlon, maxlat)``. + """ + + return self.__model.eo_metadata.footprint.extent + + def getEOGML(self): + """ + Returns the EO O&M XML text stored in the metadata. + """ + + return self.__model.eo_metadata.eo_gml + +class EOCoverageWrapper(EOMetadataWrapper, CoverageWrapper): + """ + This is a partial implementation of :class:`~.EOCoverageInterface`. + It inherits from :class:`CoverageWrapper` and + :class:`EOMetadataWrapper`. + """ + + def _get_create_dict(self, params): + create_dict = super(EOCoverageWrapper, self)._get_create_dict(params) + + create_dict["lineage"] = LineageRecord.objects.create() + + return create_dict + + def _updateModel(self, link_kwargs, unlink_kwargs, set_kwargs): + super(EOCoverageWrapper, self)._updateModel(link_kwargs, unlink_kwargs, set_kwargs) + + containers = link_kwargs.get("containers", []) + if "container" in link_kwargs: + containers.append(link_kwargs["container"]) + + for container in containers: + container.addCoverage(self) + + containers = unlink_kwargs.get("containers", []) + if "container" in unlink_kwargs: + containers.append(unlink_kwargs["container"]) + + for container in containers: + container.removeCoverage(self) + + # TODO lineage update? + + + def getEOCoverageSubtype(self): + """ + This method shall return the EO Coverage subtype according to + the WCS 2.0 EO-AP (EO-WCS). It must be overridden by child + implementations. By default :exc:`~.InternalError` is raised. + """ + + raise InternalError("Not implemented.") + + def getDatasets(self, filter_exprs=None): + """ + This method shall return the datasets associated with this + coverage, possibly filtered by the optional filter expressions. + It must be overridden by child implementations. By default + :exc:`~.InternalError` is raised. + """ + + raise InternalError("Not implemented.") + + def getLineage(self): + """ + Returns ``None``. + + .. note:: The lineage element has yet to be specified in + detail in the WCS 2.0 EO-AP (EO-WCS). + """ + + return None + +class EODatasetWrapper(PackagedDataWrapper, EOCoverageWrapper): + """ + This is the base class for EO Dataset wrapper implementations. It + inherits from :class:`EOCoverageWrapper` and + :class:`PackagedDataWrapper`. + """ + + def _get_create_dict(self, params): + create_dict = super(EODatasetWrapper, self)._get_create_dict(params) + + if "data_package" not in params: + raise InternalError( + "Missing mandatory 'data_package' parameter for RectifiedDataset creation." + ) + + create_dict["data_package"] = params["data_package"].getRecord() + + if "container" in params: + create_dict["visible"] = params.get("visible", False) + else: + create_dict["visible"] = params.get("visible", True) + + return create_dict + + def _updateModel(self, link_kwargs, unlink_kwargs, set_kwargs): + super(EODatasetWrapper, self)._updateModel(link_kwargs, unlink_kwargs, set_kwargs) + + visible = set_kwargs.get("visible") + if visible is not None: + self.__model.visible = visible + + data_package = set_kwargs.get("data_package") + if data_package: + # TODO implement + pass + + def getDatasets(self, filter_exprs=None): + """ + This method applies the given filter expressions to the + model and returns a list containing the wrapper in case the + filters are matched or an empty list otherwise. + """ + + if filter_exprs is not None: + if not self.matches(filter_exprs): + return [] + + return [self] + +class RectifiedDatasetWrapper(RectifiedGridWrapper, EODatasetWrapper): + """ + This is the wrapper for Rectified Datasets. It inherits from + :class:`EODatasetWrapper` and :class:`RectifiedGridWrapper`. It + implements :class:`~.RectifiedDatasetInterface`. + + .. attribute:: FIELDS + + * ``eo_id``: the EO ID of the dataset; value must be a string + * ``begin_time``: the begin time of the eo metadata entry + * ``end_time``: the end time of the eo metadata entry + * ``footprint``: the footprint of the dataset + * ``srid``: the SRID of the dataset's CRS; value must be an integer + * ``size_x``: the width of the coverage in pixels; value must be + an integer + * ``size_y``: the height of the coverage in pixels; value must be + an integer + * ``minx``: the left hand bound of the dataset's extent; value must + be numeric + * ``miny``: the lower bound of the dataset's extent; value must be + numeric + * ``maxx``: the right hand bound of the dataset's extent; value must + be numeric + * ``maxy``: the upper bound of the dataset's extent; value must be + numeric + * ``visible``: the visibility of the coverage (for DescribeCoverage + requests); boolean + * ``automatic``: if the dataset was automatically created or by hand; + boolean + """ + + REGISTRY_CONF = { + "name": "Rectified Dataset Wrapper", + "impl_id": "resources.coverages.wrappers.RectifiedDatasetWrapper", + "model_class": RectifiedDatasetRecord, + "id_field": "coverage_id", + "factory_ids": ("resources.coverages.wrappers.EOCoverageFactory",) + } + + FIELDS = { + "eo_id": "eo_id", + "data_package": "data_package", + "srid": "extent__srid", + "size_x": "extent__size_x", + "size_y": "extent__size_y", + "minx": "extent__minx", + "miny": "extent__miny", + "maxx": "extent__maxx", + "maxy": "extent__maxy", + "visible": "visible", + "automatic": "automatic", + "begin_time": "eo_metadata__timestamp_begin", + "end_time": "eo_metadata__timestamp_end", + "footprint": "eo_metadata__footprint" + + # TODO layer metadata? + } + + #------------------------------------------------------------------- + # ResourceInterface implementations + #------------------------------------------------------------------- + + # NOTE: partially implemented in ResourceWrapper + + def __get_model(self): + return self._ResourceWrapper__model + + def __set_model(self, model): + self._ResourceWrapper__model = model + + __model = property(__get_model, __set_model) + + # _get_create_dict inherited from superclasses and mix-ins + + def _create_model(self, create_dict): + self.__model = RectifiedDatasetRecord.objects.create(**create_dict) + + def _post_create(self, params): + if "container" in params and params["container"]: + params["container"].addCoverage(self) + containers = params.get("containers", []) + for container in containers: + container.addCoverage(self) + + #------------------------------------------------------------------- + # CoverageInterface implementations + #------------------------------------------------------------------- + + # NOTE: partially implemented in CoverageWrapper + + def getCoverageSubtype(self): + """ + Returns ``RectifiedGridCoverage``. + """ + + return "RectifiedGridCoverage" + + def getType(self): + """ + Returns ``eo.rect_dataset`` + """ + + return "eo.rect_dataset" + + def getSize(self): + """ + Returns the pixel size of the dataset as 2-tuple of integers + ``(size_x, size_y)``. + """ + + return (self.__model.extent.size_x, self.__model.extent.size_y) + + #------------------------------------------------------------------- + # EOCoverageInterface implementations + #------------------------------------------------------------------- + + def getEOCoverageSubtype(self): + """ + Returns ``RectifiedDataset``. + """ + + return "RectifiedDataset" + + def getContainers(self): + """ + This method returns a list of :class:`DatasetSeriesWrapper` and + :class:`RectifiedStitchedMosaicWrapper` objects containing this + Rectified Dataset, or an empty list. + """ + cov_factory = System.getRegistry().bind( + "resources.coverages.wrappers.EOCoverageFactory" + ) + + dss_factory = System.getRegistry().bind( + "resources.coverages.wrappers.DatasetSeriesFactory" + ) + + self_expr = System.getRegistry().getFromFactory( + "resources.coverages.filters.CoverageExpressionFactory", + {"op_name": "contains", "operands": (self.__model.pk,)} + ) + + wrappers = [] + wrappers.extend(cov_factory.find( + impl_ids=["resources.coverages.wrappers.RectifiedStitchedMosaicWrapper"], + filter_exprs=[self_expr] + )) + wrappers.extend(dss_factory.find(filter_exprs=[self_expr])) + + return wrappers + + def getContainerCount(self): + """ + This method returns the number of Dataset Series and + Rectified Stitched Mosaics containing this Rectified Dataset. + """ + return self.__model.dataset_series_set.count() + \ + self.__model.rect_stitched_mosaics.count() + + def contains(self, wrapper): + """ + Always returns ``False``. A Dataset does not contain other + Datasets. + """ + return False + + def containedIn(self, wrapper): + """ + Returns ``True`` if this Rectified Dataset is contained in the + Rectified Stitched Mosaic or Dataset Series specified by its + ``wrapper``, ``False`` otherwise. + """ + res_id = wrapper.getModel().pk + + return self.__model.dataset_series_set.filter(pk=res_id).count() > 0 or \ + self.__model.rect_stitched_mosaics.filter(pk=res_id).count() > 0 + +RectifiedDatasetWrapperImplementation = \ +RectifiedDatasetInterface.implement(RectifiedDatasetWrapper) + +class ReferenceableDatasetWrapper(ReferenceableGridWrapper, EODatasetWrapper): + """ + This is the wrapper for Referenceable Datasets. It inherits from + :class:`EODatasetWrapper` and :class:`ReferenceableGridWrapper`. + + .. attribute:: FIELDS + + * ``eo_id``: the EO ID of the dataset; value must be a string + * ``begin_time``: the begin time of the eo metadata entry + * ``end_time``: the end time of the eo metadata entry + * ``footprint``: the footprint of the dataset + * ``filename``: the path to the dataset; value must be a string + * ``metadata_filename``: the path to the accompanying metadata + file; value must be a string + * ``srid``: the SRID of the dataset's CRS; value must be an integer + * ``size_x``: the width of the coverage in pixels; value must be + an integer + * ``size_y``: the height of the coverage in pixels; value must be + an integer + * ``minx``: the left hand bound of the dataset's extent; value must + be numeric + * ``miny``: the lower bound of the dataset's extent; value must be + numeric + * ``maxx``: the right hand bound of the dataset's extent; value must + be numeric + * ``maxy``: the upper bound of the dataset's extent; value must be + * ``visible``: the ``visible`` attribute of the dataset; value must + be boolean + * ``automatic``: the ``automatic`` attribute of the dataset; value + must be boolean + + .. note:: The design of Referenceable Datasets is still TBD. + """ + + REGISTRY_CONF = { + "name": "Referenceable Dataset Wrapper", + "impl_id": "resources.coverages.wrappers.ReferenceableDatasetWrapper", + "model_class": ReferenceableDatasetRecord, + "id_field": "coverage_id", + "factory_ids": ("resources.coverages.wrappers.EOCoverageFactory",) + } + + FIELDS = { + "eo_id": "eo_id", + "data_package": "data_package", + "srid": "extent__srid", + "size_x": "extent__size_x", + "size_y": "extent__size_y", + "minx": "extent__minx", + "miny": "extent__miny", + "maxx": "extent__maxx", + "maxy": "extent__maxy", + "visible": "visible", + "automatic": "automatic", + "begin_time": "eo_metadata__timestamp_begin", + "end_time": "eo_metadata__timestamp_end", + "footprint": "eo_metadata__footprint" + } + + #------------------------------------------------------------------- + # ResourceInterface implementations + #------------------------------------------------------------------- + + # NOTE: partially implemented in ResourceWrapper + + def __get_model(self): + return self._ResourceWrapper__model + + def __set_model(self, model): + self._ResourceWrapper__model = model + + __model = property(__get_model, __set_model) + + def _create_model(self, create_dict): + self.__model = ReferenceableDatasetRecord.objects.create(**create_dict) + + def _post_create(self, params): + if "container" in params and params["container"]: + params["container"].addCoverage(self) + containers = params.get("containers", []) + for container in containers: + container.addCoverage(self) + + #------------------------------------------------------------------- + # CoverageInterface implementations + #------------------------------------------------------------------- + + # NOTE: partially implemented in CoverageWrapper + + def getCoverageSubtype(self): + """ + Returns ``ReferenceableGridCoverage``. + """ + + return "ReferenceableGridCoverage" + + def getType(self): + """ + Returns ``eo.ref_dataset`` + """ + + return "eo.ref_dataset" + + def getSize(self): + """ + Returns the pixel size of the dataset as 2-tuple of integers + ``(size_x, size_y)``. + """ + + return (self.__model.extent.size_x, self.__model.extent.size_y) + + #------------------------------------------------------------------- + # EOCoverageInterface implementations + #------------------------------------------------------------------- + + def getEOCoverageSubtype(self): + """ + Returns ``ReferenceableDataset``. + """ + + return "ReferenceableDataset" + + def getContainers(self): + """ + This method returns a list of :class:`DatasetSeriesWrapper` + objects containing this Referenceable Dataset, or an empty list. + """ + dss_factory = System.getRegistry().bind( + "resources.coverages.wrappers.DatasetSeriesFactory" + ) + + self_expr = System.getRegistry().getFromFactory( + "resources.coverages.filters.CoverageExpressionFactory", + {"op_name": "contains", "operands": (self.__model.pk,)} + ) + + return dss_factory.find(filter_exprs=[self_expr]) + + def getContainerCount(self): + """ + This method returns the number of Dataset Series containing + this Referenceable Dataset. + """ + return self.__model.dataset_series_set.count() + + def contains(self, wrapper): + """ + Always returns ``False``. A Dataset cannot contain other + Datasets. + """ + return False + + def containedIn(self, wrapper): + """ + This method returns ``True`` if this Referenceable Dataset is + contained in the Dataset Series specified by its ``wrapper``, + ``False`` otherwise. + """ + res_id = wrapper.getModel().pk + + return self.__model.dataset_series_set.filter(pk=res_id).count() > 0 + +ReferenceableDatasetWrapperImplementation = \ +ReferenceableDatasetInterface.implement(ReferenceableDatasetWrapper) + +class RectifiedStitchedMosaicWrapper(TiledDataWrapper, RectifiedGridWrapper, EOCoverageWrapper): + """ + This is the wrapper for Rectified Stitched Mosaics. It inherits + from :class:`EOCoverageWrapper` and :class:`RectifiedGridWrapper`. + It implements :class:`~.RectifiedStitchedMosaicInterface`. + + .. attribute:: FIELDS + + * ``eo_id``: the EO ID of the mosaic; value must be a string + * ``begin_time``: the begin time of the eo metadata entry + * ``end_time``: the end time of the eo metadata entry + * ``footprint``: the footprint of the mosaic + * ``srid``: the SRID of the mosaic's CRS; value must be an integer + * ``size_x``: the width of the coverage in pixels; value must be + an integer + * ``size_y``: the height of the coverage in pixels; value must be + an integer + * ``minx``: the left hand bound of the mosaic's extent; value must + be numeric + * ``miny``: the lower bound of the mosaic's extent; value must be + numeric + * ``maxx``: the right hand bound of the mosaic's extent; value must + be numeric + * ``maxy``: the upper bound of the mosaic's extent; value must be + numeric + """ + + REGISTRY_CONF = { + "name": "Rectified Stitched Mosaic Wrapper", + "impl_id": "resources.coverages.wrappers.RectifiedStitchedMosaicWrapper", + "model_class": RectifiedStitchedMosaicRecord, + "id_field": "coverage_id", + "factory_ids": ("resources.coverages.wrappers.EOCoverageFactory",) + } + + FIELDS = { + "eo_id": "eo_id", + "srid": "extent__srid", + "size_x": "extent__size_x", + "size_y": "extent__size_y", + "minx": "extent__minx", + "miny": "extent__miny", + "maxx": "extent__maxx", + "maxy": "extent__maxy", + "begin_time": "eo_metadata__timestamp_begin", + "end_time": "eo_metadata__timestamp_end", + "footprint": "eo_metadata__footprint" + } + + #------------------------------------------------------------------- + # ResourceInterface implementations + #------------------------------------------------------------------- + + def __get_model(self): + return self._ResourceWrapper__model + + def __set_model(self, model): + self._ResourceWrapper__model = model + + __model = property(__get_model, __set_model) + + def __init__(self): + super(RectifiedStitchedMosaicWrapper, self).__init__() + + self.__block_md_update = False + + # NOTE: partially implemented in ResourceWrapper + + def _get_create_dict(self, params): + create_dict = super(RectifiedStitchedMosaicWrapper, self)._get_create_dict(params) + + if "tile_index" not in params: + raise InternalError( + "Missing mandatory 'tile_index' parameter for RectifiedStitchedMosaic creation." + ) + + create_dict["tile_index"] = params["tile_index"].getRecord() + + return create_dict + + def _create_model(self, create_dict): + self.__model = RectifiedStitchedMosaicRecord.objects.create( + **create_dict + ) + + def _post_create(self, params): + container = params.get("container") + if container is not None: + container.addCoverage(self) + + containers = params.get("containers", []) + for container in containers: + container.addCoverage(self) + + for coverage in params.get("coverages", []): + self.addCoverage(coverage) + + if "data_sources" in params: + for data_source in params["data_sources"]: + self.__model.data_sources.add(data_source.getRecord()) + + def _updateModel(self, link_kwargs, unlink_kwargs, set_kwargs): + super(RectifiedStitchedMosaicWrapper, self)._updateModel(link_kwargs, unlink_kwargs, set_kwargs) + + try: + self.__block_md_update = True + + data_sources = link_kwargs.get("data_sources", []) + coverages = link_kwargs.get("coverages", []) + for data_source in data_sources: + self.__model.data_sources.add(data_source.getRecord()) + for coverage in coverages: + self.addCoverage(coverage) + + data_sources = unlink_kwargs.get("data_sources", []) + coverages = unlink_kwargs.get("coverages", []) + for data_source in data_sources: + self.__model.data_sources.remove(data_source.getRecord()) + for coverage in coverages: + self.removeCoverage(coverage) + + self._updateMetadata() + finally: + self.__block_md_update = False + + # TODO: tile_index + + def _updateMetadata(self): + qs = self.__model.rect_datasets.all() + + if len(qs): + eo_metadata_set = EOMetadataRecord.objects.filter( + rectifieddatasetrecord_set__in = qs + ) + + try: + begin_time = min(eo_metadata_set.values_list('timestamp_begin', flat=True)) + end_time = max(eo_metadata_set.values_list('timestamp_end', flat=True)) + # Work around for issue in Django 1.5 + except TypeError: + begin_time = min([t.timestamp_begin for t in eo_metadata_set]) + end_time = max([t.timestamp_end for t in eo_metadata_set]) + + footprint = eo_metadata_set.aggregate( + Union('footprint') + )["footprint__union"] + + if footprint.geom_type.upper() != "MULTIPOLYGON": + footprint = MultiPolygon(footprint) + + self.__model.eo_metadata.timestamp_begin = begin_time + self.__model.eo_metadata.timestamp_end = end_time + self.__model.eo_metadata.footprint = footprint + + self.__model.eo_metadata.save() + + #------------------------------------------------------------------- + # CoverageInterface implementations + #------------------------------------------------------------------- + + # NOTE: partially implemented in CoverageWrapper + + def getCoverageSubtype(self): + """ + Returns ``RectifiedGridCoverage``. + """ + + return "RectifiedGridCoverage" + + def getType(self): + """ + Returns ``eo.rect_stitched_mosaic`` + """ + + return "eo.rect_stitched_mosaic" + + def getSize(self): + """ + Returns the pixel size of the mosaic as 2-tuple of integers + ``(size_x, size_y)``. + """ + + return (self.__model.extent.size_x, self.__model.extent.size_y) + + #------------------------------------------------------------------- + # EOCoverageInterface implementations + #------------------------------------------------------------------- + + def getEOCoverageSubtype(self): + """ + Returns ``RectifiedStitchedMosaic``. + """ + + return "RectifiedStitchedMosaic" + + def getDatasets(self, filter_exprs=None): + """ + Returns a list of :class:`RectifiedDatasetWrapper` objects + contained in the stitched mosaic wrapped by the implementation. + It accepts an optional ``filter_exprs`` parameter which is + expected to be a list of filter expressions + (see module :mod:`eoxserver.resources.coverages.filters`) or + ``None``. Only the datasets matching the filters will be + returned; in case no matching coverages are found an empty list + will be returned. + """ + + _filter_exprs = [] + + if filter_exprs is not None: + _filter_exprs.extend(filter_exprs) + + self_expr = System.getRegistry().getFromFactory( + "resources.coverages.filters.CoverageExpressionFactory", + {"op_name": "contained_in", "operands": (self.__model.pk,)} + ) + _filter_exprs.append(self_expr) + + factory = System.getRegistry().bind( + "resources.coverages.wrappers.EOCoverageFactory" + ) + + return factory.find( + impl_ids=["resources.coverages.wrappers.RectifiedDatasetWrapper"], + filter_exprs=_filter_exprs + ) + + def getContainers(self): + """ + This method returns a list of :class:`DatasetSeriesWrapper` + objects containing this Stitched Mosaic or an empty list. + """ + + dss_factory = System.getRegistry().bind( + "resources.coverages.wrappers.DatasetSeriesFactory" + ) + + self_expr = System.getRegistry().getFromFactory( + "resources.coverages.filters.CoverageExpressionFactory", + {"op_name": "contains", "operands": (self.__model.pk,)} + ) + + return dss_factory.find(filter_exprs=[self_expr]) + + def getContainerCount(self): + """ + This method returns the number of Dataset Series containing + this Stitched Mosaic. + """ + return self.__model.dataset_series_set.count() + + def contains(self, wrapper): + """ + This method returns ``True`` if the a Rectified Dataset specified + by its ``wrapper`` is contained within this Stitched Mosaic, + ``False`` otherwise. + """ + res_id = wrapper.getModel().pk + + return self.__model.rect_datasets.filter(pk=res_id).count() > 0 + + def containedIn(self, wrapper): + """ + This method returns ``True`` if this Stitched Mosaic is + contained in the Dataset Series specified by its ``wrapper``, + ``False`` otherwise. + """ + res_id = wrapper.getModel().pk + + return self.__model.dataset_series_set.filter(pk=res_id).count() > 0 + + #------------------------------------------------------------------- + # RectifiedStitchedMosaicInterface methods + #------------------------------------------------------------------- + + def getShapeFilePath(self): + """ + Returns the path to the shape file. + """ + return os.path.join(self.__model.storage_dir, "tindex.shp") + + def addCoverage(self, wrapper): + """ + Adds a Rectified Dataset specified by its wrapper. An + :exc:`InternalError` is raised if the wrapper type is not equal to + ``eo.rect_dataset`` or if the grids of the dataset is not compatible to + the grid of the Rectified Stitched Mosaic. + """ + res_type = wrapper.getType() + res_id = wrapper.getModel().pk + + if res_type != "eo.rect_dataset": + raise InternalError( + "Cannot add coverages of type '%s' to Rectified Stitched Mosaics" %\ + res_type + ) + + # check if SRIDs are equal + if self.getSRID() != wrapper.getSRID(): + raise InternalError( + "Cannot add coverage: SRID mismatch (%d != %s)" % ( + self.getSRID(), wrapper.getSRID() + ) + ) + + EPSILON = 1e-10 + + cov_extent = wrapper.getExtent() + this_extent = self.getExtent() + + cov_offsets = wrapper.getResolution() + this_offsets = self.getResolution() + + # check if offset vectors are the same + if (abs(this_offsets[0] - cov_offsets[0]) > EPSILON + or abs(this_offsets[1] - cov_offsets[1]) > EPSILON): + raise InternalError( + "Cannot add coverage: offset vector mismatch (%s != %s)" % ( + this_offsets, cov_offsets + ) + ) + + # check if grids are the same + diff_origins = tuple(map(operator.sub, this_extent[:2], cov_extent[:2])) + v = tuple(map(operator.div, diff_origins, cov_offsets)) + if (abs(v[0] - round(v[0])) > EPSILON + or abs(v[1] - round(v[1])) > EPSILON): + raise InternalError("Cannot add coverage: grid mismatch.") + + # check if range types are the same + if self.getRangeType() != wrapper.getRangeType(): + raise InternalError( + "Cannot add coverage: range type mismatch (%s, %s)." % ( + self.getRangeType().name, wrapper.getRangeType().name + ) + ) + + self.__model.rect_datasets.add(res_id) + + if not self.__block_md_update: + self._updateMetadata() + + def removeCoverage(self, wrapper): + """ + Removes a Rectified Dataset specified by its wrapper. An + :exc:`InternalError` is raised if the wrapper type is not equal to + ``eo.rect_dataset``. + """ + res_type = wrapper.getType() + res_id = wrapper.getModel().pk + + if res_type != "eo.rect_dataset": + raise InternalError( + "Cannot remove coverages of type '%s' from Rectified Stitched Mosaics" %\ + res_type + ) + else: + self.__model.rect_datasets.remove(res_id) + + if not self.__block_md_update: + self._updateMetadata() + + def getDataDirs(self): + """ + This method returns a list of directories which hold the + stitched mosaic data. + """ + + return list( + self.__model.data_dirs.values_list('dir', flat=True) + ) + + def getDataSources(self): + data_source_factory = System.getRegistry().bind( + "resources.coverages.data.DataSourceFactory" + ) + return [data_source_factory.get(record=record) + for record in self.__model.data_sources.all()] + + + def getImagePattern(self): + """ + Returns the filename pattern for image files to be included + into the stitched mosaic. The pattern is expressed in the format + accepted by :func:`fnmatch.fnmatch`. + """ + + return self.__model.image_pattern + +RectifiedStitchedMosaicWrapperImplementation = \ +RectifiedStitchedMosaicInterface.implement(RectifiedStitchedMosaicWrapper) + +class DatasetSeriesWrapper(EOMetadataWrapper, ResourceWrapper): + """ + This is the wrapper for Dataset Series. It inherits from + :class:`EOMetadataWrapper`. It implements :class:`DatasetSeriesInterface`. + + .. attribute:: FIELDS + + * ``eo_id``: the EO ID of the dataset series; value must be a string + * ``begin_time``: the begin time of the eo metadata entry + * ``end_time``: the end time of the eo metadata entry + * ``footprint``: the footprint of the mosaic + """ + + REGISTRY_CONF = { + "name": "Dataset Series Wrapper", + "impl_id": "resources.coverages.wrappers.DatasetSeriesWrapper", + "model_class": DatasetSeriesRecord, + "id_field": "eo_id", + "factory_ids": ("resources.coverages.wrappers.DatasetSeriesFactory", ) + } + + FIELDS = { + "eo_id": "eo_id", + "begin_time": "eo_metadata__timestamp_begin", + "end_time": "eo_metadata__timestamp_end", + "footprint": "eo_metadata__footprint" + } + + #------------------------------------------------------------------- + # ResourceInterface methods + #------------------------------------------------------------------- + + # NOTE: partially implemented by ResourceWrapper + + def __init__(self): + super(DatasetSeriesWrapper, self).__init__() + + self.__block_md_update = False + + def __get_model(self): + return self._ResourceWrapper__model + + def __set_model(self, model): + self._ResourceWrapper__model = model + + __model = property(__get_model, __set_model) + + def _get_create_dict(self, params): + create_dict = super(DatasetSeriesWrapper, self)._get_create_dict(params) + + layer_metadata = params.get("layer_metadata") + + if layer_metadata: + create_dict["layer_metadata"] = [] + + for key, value in layer_metadata.items(): + create_dict["layer_metadata"].append( + LayerMetadataRecord.objects.get_or_create( + key=key, value=value + )[0] + ) + + return create_dict + + def _create_model(self, create_dict): + self.__model = DatasetSeriesRecord.objects.create(**create_dict) + + def _post_create(self, params): + for data_source in params.get("data_sources", []): + self.__model.data_sources.add(data_source.getRecord()) + + for coverage in params.get("coverages", []): + self.addCoverage(coverage) + + def _updateModel(self, link_kwargs, unlink_kwargs, set_kwargs): + super(DatasetSeriesWrapper, self)._updateModel(link_kwargs, unlink_kwargs, set_kwargs) + + try: + self.__block_md_update = True + + # link + data_sources = link_kwargs.get("data_sources", []) + coverages = link_kwargs.get("coverages", []) + for data_source in data_sources: + self.__model.data_sources.add(data_source.getRecord()) + for coverage in coverages: + self.addCoverage(coverage) + + # unlink + data_sources = unlink_kwargs.get("data_sources", []) + coverages = unlink_kwargs.get("coverages", []) + for data_source in data_sources: + self.__model.data_sources.remove(data_source.getRecord()) + for coverage in coverages: + self.removeCoverage(coverage) + + self._updateMetadata() + finally: + self.__block_md_update = False + + def _aggregateMetadata(self, eo_metadata_set): + try: + begin_time = min(eo_metadata_set.values_list('timestamp_begin', flat=True)) + end_time = max(eo_metadata_set.values_list('timestamp_end', flat=True)) + # Work around for issue in Django 1.5 + except TypeError: + begin_time = min([t.timestamp_begin for t in eo_metadata_set]) + end_time = max([t.timestamp_end for t in eo_metadata_set]) + + try: + # use the aggregate calculation if provided + footprint = MultiPolygon( + Polygon.from_bbox(eo_metadata_set.extent(field_name="footprint")) + ) + except: + # manual collection of footprints (required for backends other than + # SpatiaLite or PostGIS) + logger.warn("Performing manual envelope calculation.") + envelopes = MultiPolygon([ + md.footprint.envelope for md in eo_metadata_set + ]) + footprint = envelopes.envelope + + return (begin_time, end_time, footprint) + + def _updateMetadata(self, added_coverage=None): + begin_time = None + footprint = None + + rect_qs = self.__model.rect_datasets.all() + ref_qs = self.__model.ref_datasets.all() + mosaic_qs = self.__model.rect_stitched_mosaics.all() + + # if a coverage was added, do not accumulate metadata, instead make a + # simplified approach and only add the metadata of the new coverage + if added_coverage: + eo_metadata = self.__model.eo_metadata + sum_count = rect_qs.count() + ref_qs.count() + mosaic_qs.count() + + # only perform shortcut when there is guranteed metadata of + # included coverages + if sum_count > 1: + logger.info("Performing shortcut metadata calculation.") + eo_metadata.timestamp_begin = min( + eo_metadata.timestamp_begin, added_coverage.getBeginTime() + ) + eo_metadata.timestamp_end = max( + eo_metadata.timestamp_end, added_coverage.getEndTime() + ) + + # calculate a new combined envelope if necessary + new_envelope = added_coverage.getFootprint().envelope + series_envelopes = eo_metadata.footprint + if not series_envelopes.contains(new_envelope): + series_envelopes.append(new_envelope) + eo_metadata.footprint = MultiPolygon( + series_envelopes.envelope + ) + + eo_metadata.full_clean() + eo_metadata.save() + + # exit now + return + + # perform the normal metadata aggregation here + if len(rect_qs): + eo_metadata_set = EOMetadataRecord.objects.filter( + rectifieddatasetrecord_set__in = rect_qs + ) + + begin_time, end_time, footprint = self._aggregateMetadata( + eo_metadata_set + ) + + + if len(ref_qs): + eo_metadata_set = EOMetadataRecord.objects.filter( + referenceabledatasetrecord_set__in = ref_qs + ) + + begin_time_ref, end_time_ref, footprint_ref = self._aggregateMetadata( + eo_metadata_set + ) + + if begin_time: + begin_time = min(begin_time, begin_time_ref) + end_time = max(end_time, end_time_ref) + footprint = footprint.union(footprint_ref) + else: + begin_time = begin_time_ref + end_time = end_time_ref + footprint = footprint_ref + + + if len(mosaic_qs): + eo_metadata_set = EOMetadataRecord.objects.filter( + rectifiedstitchedmosaicrecord_set__in = mosaic_qs + ) + + begin_time_mosaics, end_time_mosaics, footprint_mosaics = self._aggregateMetadata( + eo_metadata_set + ) + + if begin_time: + begin_time = min(begin_time, begin_time_mosaics) + end_time = max(end_time, end_time_mosaics) + footprint = footprint.union(footprint_mosaics) + else: + begin_time = begin_time_mosaics + end_time = end_time_mosaics + footprint = footprint_mosaics + + if footprint and footprint.geom_type.upper() != "MULTIPOLYGON": + footprint = MultiPolygon(footprint) + elif footprint is None: + pass + + # if there are no children do not update the metadata + if begin_time: + self.__model.eo_metadata.timestamp_begin = begin_time + self.__model.eo_metadata.timestamp_end = end_time + self.__model.eo_metadata.footprint = footprint + + self.__model.eo_metadata.full_clean() + self.__model.eo_metadata.save() + + #------------------------------------------------------------------- + # DatasetSeriesInterface methods + #------------------------------------------------------------------- + + def getType(self): + """ + Returns ``"eo.dataset_series"``. + """ + + return "eo.dataset_series" + + def getEOCoverages(self, filter_exprs=None): + """ + This method returns a list of EOCoverage wrappers (for datasets + and stitched mosaics) associated with the dataset series wrapped + by the implementation. It accepts an optional ``filter_exprs`` + parameter which is expected to be a list of filter expressions + (see module :mod:`eoxserver.resources.coverages.filters`) or + ``None``. Only the EOCoverages matching the filters will be + returned; in case no matching coverages are found an empty list + will be returned. + """ + + return self._get_contained_coverages(filter_exprs=filter_exprs) + + + def getDatasets(self, filter_exprs=None): + """ + This method returns a list of RectifiedDataset or ReferenceableDataset + wrappers associated with the dataset series. It accepts an optional + ``filter_exprs`` parameter which is expected to be a list of filter + expressions (see module :mod:`eoxserver.resources.coverages.filters`) or + ``None``. Only the Datasets matching the filters will be returned; in + case no matching Datasets are found an empty list will be returned. + """ + + return self._get_contained_coverages( + impl_ids = [ + "resources.coverages.wrappers.RectifiedDatasetWrapper", + "resources.coverages.wrappers.ReferenceableDatasetWrapper" + ], + filter_exprs = filter_exprs + ) + + def getLayerMetadata(self): + """ + Returns a list of ``(metadata_key, metadata_value)`` pairs + that represent MapServer metadata tags to be attached to + MapServer layers. + """ + + return self.__model.layer_metadata.values_list("key", "value") + + def contains(self, wrapper): + """ + This method returns ``True`` if the Dataset Series contains + the EO Coverage specifiec by its ``wrapper``, ``False`` + otherwise. + """ + res_id = wrapper.getModel().pk + + return self.__model.rect_datasets.filter(pk=res_id).count() > 0 or \ + self.__model.ref_datasets.filter(pk=res_id).count() > 0 or \ + self.__model.rect_stitched_mosaics.filter(pk=res_id).count() > 0 + + def addCoverage(self, wrapper): + """ + Adds the EO coverage of type ``res_type`` with primary key + ``res_id`` to the dataset series. An :exc:`InternalError` is + raised if the type cannot be handled by Dataset Series. + Supported wrapper types are: + + * ``eo.rect_dataset`` + * ``eo.ref_dataset`` + * ``eo.rect_stitched_mosaic`` + """ + res_type = wrapper.getType() + res_id = wrapper.getModel().pk + + if res_type == "eo.rect_dataset": + self.__model.rect_datasets.add(res_id) + elif res_type == "eo.ref_dataset": + self.__model.ref_datasets.add(res_id) + elif res_type == "eo.rect_stitched_mosaic": + self.__model.rect_stitched_mosaics.add(res_id) + else: + raise InternalError( + "Cannot add coverages of type '%s' to Dataset Series" %\ + res_type + ) + + if not self.__block_md_update: + self._updateMetadata(wrapper) + + def removeCoverage(self, wrapper): + """ + Removes the EO coverage specified by its ``wrapper`` from the + dataset series. An :exc:`InternalError` is + raised if the type cannot be handled by Dataset Series. + Supported wrapper types are: + + * ``eo.rect_dataset`` + * ``eo.ref_dataset`` + * ``eo.rect_stitched_mosaic`` + """ + res_type = wrapper.getType() + res_id = wrapper.getModel().pk + + if res_type == "eo.rect_dataset": + self.__model.rect_datasets.remove(res_id) + elif res_type == "eo.ref_dataset": + self.__model.ref_datasets.remove(res_id) + elif res_type == "eo.rect_stitched_mosaic": + self.__model.rect_stitched_mosaics.remove(res_id) + else: + raise InternalError( + "Cannot add coverages of type '%s' to Dataset Series" %\ + res_type + ) + + if not self.__block_md_update: + self._updateMetadata() + + def getDataDirs(self): + """ + This method returns a list of directories which hold the + dataset series data. + """ + + return list( + self.__model.data_dirs.values_list('dir', flat=True) + ) + + def getDataSources(self): + data_source_factory = System.getRegistry().bind( + "resources.coverages.data.DataSourceFactory" + ) + return [data_source_factory.get(record=record) + for record in self.__model.data_sources.all()] + + def getImagePattern(self): + """ + Returns the filename pattern for image files to be included + into the stitched mosaic. The pattern is expressed in the format + accepted by :func:`fnmatch.fnmatch`. + """ + + return self.__model.image_pattern + + def _get_contained_coverages(self, impl_ids=None, filter_exprs=None): + _filter_exprs = [] + + if filter_exprs is not None: + _filter_exprs.extend(filter_exprs) + + self_expr = System.getRegistry().getFromFactory( + "resources.coverages.filters.CoverageExpressionFactory", + {"op_name": "contained_in", "operands": (self.__model.pk,)} + ) + _filter_exprs.append(self_expr) + + factory = System.getRegistry().bind( + "resources.coverages.wrappers.EOCoverageFactory" + ) + + if impl_ids is None: + return factory.find(filter_exprs=_filter_exprs) + else: + return factory.find(impl_ids=impl_ids, filter_exprs=_filter_exprs) + + +DatasetSeriesWrapperImplementation = \ +DatasetSeriesInterface.implement(DatasetSeriesWrapper) + +#----------------------------------------------------------------------- +# Factories +#----------------------------------------------------------------------- + +class EOCoverageFactory(ResourceFactory): + REGISTRY_CONF = { + "name": "EO Coverage Factory", + "impl_id": "resources.coverages.wrappers.EOCoverageFactory" + } + +EOCoverageFactoryImplementation = \ +ResourceFactoryInterface.implement(EOCoverageFactory) + +class DatasetSeriesFactory(ResourceFactory): + REGISTRY_CONF = { + "name": "Dataset Series Factory", + "impl_id": "resources.coverages.wrappers.DatasetSeriesFactory" + } + +DatasetSeriesFactoryImplementation = \ +ResourceFactoryInterface.implement(DatasetSeriesFactory) diff -Nru eoxserver-0.4.0beta2/eoxserver/resources/processes/admin.py eoxserver-0.3.2/eoxserver/resources/processes/admin.py --- eoxserver-0.4.0beta2/eoxserver/resources/processes/admin.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/resources/processes/admin.py 2013-12-10 14:57:00.000000000 +0000 @@ -1,4 +1,5 @@ #----------------------------------------------------------------------- +# $Id$ # # Description: # @@ -15,8 +16,8 @@ # 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 +# 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 diff -Nru eoxserver-0.4.0beta2/eoxserver/resources/processes/migrations/0001_initial.py eoxserver-0.3.2/eoxserver/resources/processes/migrations/0001_initial.py --- eoxserver-0.4.0beta2/eoxserver/resources/processes/migrations/0001_initial.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/resources/processes/migrations/0001_initial.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,145 +0,0 @@ -# -*- coding: utf-8 -*- -from south.utils import datetime_utils as datetime -from south.db import db -from south.v2 import SchemaMigration -from django.db import models - - -class Migration(SchemaMigration): - - def forwards(self, orm): - # Adding model 'Type' - db.create_table(u'processes_type', ( - (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('identifier', self.gf('django.db.models.fields.CharField')(unique=True, max_length=64)), - ('handler', self.gf('django.db.models.fields.CharField')(max_length=1024)), - ('maxstart', self.gf('django.db.models.fields.IntegerField')(default=3)), - ('timeout', self.gf('django.db.models.fields.FloatField')(default=3600.0)), - ('timeret', self.gf('django.db.models.fields.FloatField')(default=-1.0)), - )) - db.send_create_signal(u'processes', ['Type']) - - # Adding model 'Instance' - db.create_table(u'processes_instance', ( - (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('type', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['processes.Type'], on_delete=models.PROTECT)), - ('identifier', self.gf('django.db.models.fields.CharField')(max_length=64)), - ('timeInsert', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), - ('timeUpdate', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)), - ('status', self.gf('django.db.models.fields.IntegerField')()), - )) - db.send_create_signal(u'processes', ['Instance']) - - # Adding unique constraint on 'Instance', fields ['identifier', 'type'] - db.create_unique(u'processes_instance', ['identifier', 'type_id']) - - # Adding model 'Task' - db.create_table(u'processes_task', ( - (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('instance', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['processes.Instance'])), - ('time', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)), - ('lock', self.gf('django.db.models.fields.BigIntegerField')(default=0)), - )) - db.send_create_signal(u'processes', ['Task']) - - # Adding model 'LogRecord' - db.create_table(u'processes_logrecord', ( - (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('instance', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['processes.Instance'])), - ('time', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)), - ('status', self.gf('django.db.models.fields.IntegerField')()), - ('message', self.gf('django.db.models.fields.TextField')()), - )) - db.send_create_signal(u'processes', ['LogRecord']) - - # Adding model 'Response' - db.create_table(u'processes_response', ( - (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('instance', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['processes.Instance'], unique=True)), - ('response', self.gf('django.db.models.fields.TextField')()), - ('mimeType', self.gf('django.db.models.fields.TextField')()), - )) - db.send_create_signal(u'processes', ['Response']) - - # Adding model 'Input' - db.create_table(u'processes_input', ( - (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('instance', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['processes.Instance'], unique=True)), - ('input', self.gf('django.db.models.fields.TextField')()), - )) - db.send_create_signal(u'processes', ['Input']) - - - def backwards(self, orm): - # Removing unique constraint on 'Instance', fields ['identifier', 'type'] - db.delete_unique(u'processes_instance', ['identifier', 'type_id']) - - # Deleting model 'Type' - db.delete_table(u'processes_type') - - # Deleting model 'Instance' - db.delete_table(u'processes_instance') - - # Deleting model 'Task' - db.delete_table(u'processes_task') - - # Deleting model 'LogRecord' - db.delete_table(u'processes_logrecord') - - # Deleting model 'Response' - db.delete_table(u'processes_response') - - # Deleting model 'Input' - db.delete_table(u'processes_input') - - - models = { - u'processes.input': { - 'Meta': {'object_name': 'Input'}, - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'input': ('django.db.models.fields.TextField', [], {}), - 'instance': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['processes.Instance']", 'unique': 'True'}) - }, - u'processes.instance': { - 'Meta': {'unique_together': "(('identifier', 'type'),)", 'object_name': 'Instance'}, - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'identifier': ('django.db.models.fields.CharField', [], {'max_length': '64'}), - 'status': ('django.db.models.fields.IntegerField', [], {}), - 'timeInsert': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), - 'timeUpdate': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), - 'type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['processes.Type']", 'on_delete': 'models.PROTECT'}) - }, - u'processes.logrecord': { - 'Meta': {'object_name': 'LogRecord'}, - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'instance': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['processes.Instance']"}), - 'message': ('django.db.models.fields.TextField', [], {}), - 'status': ('django.db.models.fields.IntegerField', [], {}), - 'time': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}) - }, - u'processes.response': { - 'Meta': {'object_name': 'Response'}, - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'instance': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['processes.Instance']", 'unique': 'True'}), - 'mimeType': ('django.db.models.fields.TextField', [], {}), - 'response': ('django.db.models.fields.TextField', [], {}) - }, - u'processes.task': { - 'Meta': {'object_name': 'Task'}, - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'instance': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['processes.Instance']"}), - 'lock': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}), - 'time': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}) - }, - u'processes.type': { - 'Meta': {'object_name': 'Type'}, - 'handler': ('django.db.models.fields.CharField', [], {'max_length': '1024'}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'identifier': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '64'}), - 'maxstart': ('django.db.models.fields.IntegerField', [], {'default': '3'}), - 'timeout': ('django.db.models.fields.FloatField', [], {'default': '3600.0'}), - 'timeret': ('django.db.models.fields.FloatField', [], {'default': '-1.0'}) - } - } - - complete_apps = ['processes'] \ No newline at end of file diff -Nru eoxserver-0.4.0beta2/eoxserver/resources/processes/models.py eoxserver-0.3.2/eoxserver/resources/processes/models.py --- eoxserver-0.4.0beta2/eoxserver/resources/processes/models.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/resources/processes/models.py 2013-12-10 14:57:00.000000000 +0000 @@ -1,4 +1,5 @@ #----------------------------------------------------------------------- +# $Id$ # # Description: # @@ -15,8 +16,8 @@ # 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 +# 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 @@ -57,9 +58,15 @@ DB fields: identifier - process class ID; handler - python dot path to handler function; - timeout - time in second after which the unfinished process is considered to be abandoned and it is restarted (number of restarts is limited by maxstart, default timeout is 3600 s); - timeret - retention time - period of time to keep finished processes in case of zero or negative value the results will be kept forever (default is -1); - maxstart - max. number of attempt to execute the task (first run and possible restarts) When the number of (re)starts is exceeded the task is marked as failed and rejected from further processing. (default is 3). + timeout - time in second after which the unfinished process is + considered to be abandoned and it is restarted + (number of restarts is limited by maxstart, default timeout is 3600 s); + timeret - retention time - period of time to keep finished processes + in case of zero or negative value the results will be kept forever + (default is -1); + maxstart - max. number of attempt to execute the task (first run and possible restarts) + When the number of (re)starts is exceeded the task is marked as failed + and rejected from further processing. (default is 3). """ identifier = models.CharField( max_length=64 , unique=True , blank=False , null=False, editable = False ) diff -Nru eoxserver-0.4.0beta2/eoxserver/resources/processes/tests.py eoxserver-0.3.2/eoxserver/resources/processes/tests.py --- eoxserver-0.4.0beta2/eoxserver/resources/processes/tests.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/resources/processes/tests.py 2013-12-10 14:57:00.000000000 +0000 @@ -1,4 +1,5 @@ #----------------------------------------------------------------------- +# $Id$ # # Description: # @@ -15,8 +16,8 @@ # 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 +# 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 diff -Nru eoxserver-0.4.0beta2/eoxserver/resources/processes/tracker.py eoxserver-0.3.2/eoxserver/resources/processes/tracker.py --- eoxserver-0.4.0beta2/eoxserver/resources/processes/tracker.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/resources/processes/tracker.py 2013-12-10 14:57:00.000000000 +0000 @@ -1,4 +1,5 @@ #----------------------------------------------------------------------- +# $Id$ # # Description: # @@ -15,8 +16,8 @@ # 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 +# 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 diff -Nru eoxserver-0.4.0beta2/eoxserver/resources/processes/views.py eoxserver-0.3.2/eoxserver/resources/processes/views.py --- eoxserver-0.4.0beta2/eoxserver/resources/processes/views.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/resources/processes/views.py 2013-12-10 14:57:00.000000000 +0000 @@ -1,4 +1,5 @@ #----------------------------------------------------------------------- +# $Id$ # # Description: # @@ -15,8 +16,8 @@ # 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 +# 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 diff -Nru eoxserver-0.4.0beta2/eoxserver/scripts/eoxserver-admin.py eoxserver-0.3.2/eoxserver/scripts/eoxserver-admin.py --- eoxserver-0.4.0beta2/eoxserver/scripts/eoxserver-admin.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/scripts/eoxserver-admin.py 2013-12-10 14:57:00.000000000 +0000 @@ -1,5 +1,6 @@ #!/usr/bin/python #------------------------------------------------------------------------------- +# $Id$ # # Project: EOxServer # Authors: Stephan Krause @@ -12,8 +13,8 @@ # 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 +# 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 diff -Nru eoxserver-0.4.0beta2/eoxserver/services/auth/base.py eoxserver-0.3.2/eoxserver/services/auth/base.py --- eoxserver-0.4.0beta2/eoxserver/services/auth/base.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/auth/base.py 2013-12-10 14:57:00.000000000 +0000 @@ -1,4 +1,5 @@ #------------------------------------------------------------------------------- +# $Id$ # # Project: EOxServer # Authors: Stephan Krause @@ -33,13 +34,13 @@ import logging -from django.http import HttpResponse - -from eoxserver.core import Component, ExtensionPoint, env -from eoxserver.core.config import get_eoxserver_config -from eoxserver.core.decoders import config -from eoxserver.services.auth.exceptions import AuthorisationException -from eoxserver.services.auth.interfaces import PolicyDecisionPointInterface +from eoxserver.core.system import System +from eoxserver.core.interfaces import Method, ObjectArg +from eoxserver.core.registry import RegisteredInterface +from eoxserver.core.readers import ConfigReaderInterface +from eoxserver.services.requests import OWSRequest, Response +from eoxserver.core.util.xmltools import DOMElementToXML +from eoxserver.services.owscommon import OWSCommonExceptionHandler, OWSCommonExceptionEncoder logger = logging.getLogger(__name__) @@ -48,18 +49,95 @@ # Config reader #------------------------------------------------------------------------------- -class AuthConfigReader(config.Reader): - section = "services.auth.base" +class AuthConfigReader(object): + REGISTRY_CONF = { + "name": "Authorization Config Reader", + "impl_id": "services.auth.base.AuthConfigReader" + } + + def validate(self, config): + pass + + def getAttributeMappingDictionary(self): + return \ + System.getConfig().getConfigValue("services.auth.base", "attribute_mapping") + + def getAuthorisationServiceURL(self): + return \ + System.getConfig().getConfigValue("services.auth.base", "authz_service") + + def getServiceID(self): + return \ + System.getConfig().getConfigValue("services.auth.base", "serviceID") + + def getAllowLocal(self): + if System.getConfig().getConfigValue("services.auth.base", "allowLocal") == 'True': + return True + return False + def getPDPType(self): + return \ + System.getConfig().getConfigValue("services.auth.base", "pdp_type") - attribute_mapping = config.Option() - authz_service = config.Option() - serviceID = config.Option() - allowLocal = config.Option(type=bool) - pdp_type = config.Option() +AuthConfigReaderImplementation = \ +ConfigReaderInterface.implement(AuthConfigReader) #------------------------------------------------------------------------------- +# AuthorizationResponse +#------------------------------------------------------------------------------- + +class AuthorizationResponse(Response): + """ + A simple base class that contains a response text, content type, headers + and status, as well as an ``authorized`` flag. It inherits from + :class:`~.Response`. + """ + + def __init__(self, content='', content_type='text/xml', headers={}, status=None, authorized=False): + super(AuthorizationResponse, self).__init__( + content, content_type, headers, status + ) + + self.authorized = authorized + +#------------------------------------------------------------------------------- +# PDP Interface +#------------------------------------------------------------------------------- + +class PolicyDecisionPointInterface(RegisteredInterface): + """ + This is the interface for Policy Decision Point (PDP) implementations. + + .. method:: authorize(ows_req) + + This method takes an :class:`~.OWSRequest` object as input and returns an + :class:`~.AuthorizationResponse` instance. It is expected to check if + the authenticated user (if any) is authorized to access the requested + resource and set the ``authorized`` flag of the response accordingly. + + In case the user is not authorized, the content and status of the + response shall be filled with an error message and the appropriate + HTTP Status Code (403). + + The method shall not raise any exceptions. + """ + + REGISTRY_CONF = { + "name": "Policy Decision Point Interface", + "intf_id": "services.auth.base.PolicyDecisionPointInterface", + "binding_method": "kvp", + "registry_keys": ( + "services.auth.base.pdp_type", + ) + } + + authorize = Method( + ObjectArg("ows_req", arg_class=OWSRequest), + returns = ObjectArg("@return", arg_class=AuthorizationResponse) + ) + +#------------------------------------------------------------------------------- # PDP Base Class #------------------------------------------------------------------------------- @@ -69,7 +147,14 @@ authorization request handling. """ - def authorize(self, request): + PARAM_SCHEMA = { + "service": {"xml_location": "/@service", "xml_type": "string", "kvp_key": "service", "kvp_type": "string"}, + "version": {"xml_location": "/@version", "xml_type": "string", "kvp_key": "version", "kvp_type": "string"}, + "operation": {"xml_location": "/", "xml_type": "localName", "kvp_key": "request", "kvp_type": "string"} + } + + + def authorize(self, ows_req): """ This method handles authorization requests according to the requirements given in the :class:`PolicyDecisionPointInterface` @@ -79,24 +164,49 @@ actual authorization decision logic. """ - reader = AuthConfigReader(get_eoxserver_config()) + ows_req.setSchema(self.PARAM_SCHEMA) # This code segment allows local clients bypassing the # Authorisation process. - if reader.allowLocal: - remoteAddress = request.META['REMOTE_ADDR'] + if (AuthConfigReader().getAllowLocal()): + remoteAddress = ows_req.http_req.META['REMOTE_ADDR'] # Check all possibilities, also IPv6 - if remoteAddress in ('127.0.0.1', 'localhost', '::1'): - return True - - authorized, message = self._decide(request) - if authorized: - return True - else: - raise AuthorisationException(message) + if remoteAddress == '127.0.0.1' or \ + remoteAddress == 'localhost' or \ + remoteAddress == '::1' : + return AuthorizationResponse(authorized = True) + + schemas = { + "http://www.opengis.net/ows/2.0": "http://schemas.opengis.net/ows/2.0/owsAll.xsd" + } + try: + authorized, message = self._decide(ows_req) + if authorized: + return AuthorizationResponse(authorized = True) + else: + return AuthorizationResponse( + content = DOMElementToXML( + OWSCommonExceptionEncoder(schemas).encodeExceptionReport( + message, "AccessForbidden" + )), + content_type = "text/xml", + status = 403, + authorized = False + ) + except Exception, e: + logger.error(str(e)) + return AuthorizationResponse( + content = DOMElementToXML( + OWSCommonExceptionEncoder(schemas).encodeExceptionReport( + "Internal Server Error", "NoApplicableCode" + )), + content_type = "text/xml", + status = 500, + authorized = False + ) - def _decide(self, request): + def _decide(self, ows_req): # This method shall implement the actual authorization decision # logic. It gets an :class:`~.OWSRequest` object as input and shall @@ -112,20 +222,20 @@ # utility functions #------------------------------------------------------------------------------- -class PDPComponent(Component): - pdps = ExtensionPoint(PolicyDecisionPointInterface) - - def get_pdp(self, pdp_type): - for pdp in self.pdps: - if pdp.pdp_type == pdp_type: - return pdp - return None - - def getPDP(): - reader = AuthConfigReader(get_eoxserver_config()) - if not reader.pdp_type or reader.pdp_type == "none": + pdp_type = System.getRegistry().bind( + "services.auth.base.AuthConfigReader" + ).getPDPType() + + if not pdp_type or pdp_type == "none": logger.debug("Authorization deactivated.") return None + else: - return PDPComponent(env).get_pdp(reader.pdp_type) + + return System.getRegistry().findAndBind( + intf_id = "services.auth.base.PolicyDecisionPointInterface", + params = { + "services.auth.base.pdp_type": pdp_type + } + ) diff -Nru eoxserver-0.4.0beta2/eoxserver/services/auth/charonpdp.py eoxserver-0.3.2/eoxserver/services/auth/charonpdp.py --- eoxserver-0.4.0beta2/eoxserver/services/auth/charonpdp.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/auth/charonpdp.py 2013-12-10 14:57:00.000000000 +0000 @@ -1,4 +1,5 @@ #------------------------------------------------------------------------------- +# $Id$ # # Project: EOxServer # Authors: Stephan Krause @@ -33,13 +34,10 @@ import xml.dom.minidom import eoxserver from urlparse import urlparse +from eoxserver.services.auth.base import BasePDP, \ + PolicyDecisionPointInterface, \ + AuthConfigReader -from eoxserver.core import implements -from eoxserver.core.config import get_eoxserver_config -from eoxserver.services.ows.decoders import get_decoder -from eoxserver.services.auth.base import BasePDP, AuthConfigReader -from eoxserver.services.auth.interfaces import PolicyDecisionPointInterface - logger = logging.getLogger(__name__) @@ -80,25 +78,40 @@ #------------------------------------------------------------------------------- class CharonPDP(BasePDP): - implements(PolicyDecisionPointInterface) + # please do not remove this dictionary; it is needed for EOxServer internal # processes - - pdp_type = "charon" + REGISTRY_CONF = { + "name": "Charon Policy Decision Point", + "impl_id": "services.auth.charonpdp.CharonPDP", + "registry_values": { + "services.auth.base.pdp_type": "charon" + } + } def __init__(self, client=None): - cfgReader = AuthConfigReader(get_eoxserver_config()) - url = cfgReader.authorisationServiceURL + cfgReader = AuthConfigReader() + url = cfgReader.getAuthorisationServiceURL() # For tests - self.client = client or AuthorisationClient(url) + if (client is None): + self.client = AuthorisationClient(url) + else : + self.client = client + self.attribMapping = {} - self.serviceID = cfgReader.serviceID or "default" + self.serviceID = cfgReader.getServiceID() + if (self.serviceID is None or \ + self.serviceID == '' ): + self.serviceID = 'default' dictLocation = cfgReader.getAttributeMappingDictionary() - if not dictLocation or dictLocation == "default": + if (dictLocation is None or \ + dictLocation == '' or \ + dictLocation == 'default'): + basePath = os.path.split(eoxserver.__file__)[0] dictLocation = os.path.join(basePath, 'conf', 'defaultAttributeDictionary') @@ -106,32 +119,27 @@ CHAR_ASSIGN = '=' try: - logger.debug( - "Loading attribute dictionary from the file %s" % dictLocation - ) - with open(dictLocation) as f: - for line in f: - if CHAR_COMMENT in line: - line, comment = line.split(CHAR_COMMENT, 1) - if CHAR_ASSIGN in line: - key, value = line.split(CHAR_ASSIGN, 1) - key = key.strip() - value = value.strip() - self.attribMapping[key] = value - logger.debug( - "Adding SAML attribute to dictionary: %s = %s" - % (key, value) - ) + logger.debug("Loading attribute dictionary from the file "+str(dictLocation)) + f = open(dictLocation) + for line in f: + if CHAR_COMMENT in line: + line, comment = line.split(CHAR_COMMENT, 1) + if CHAR_ASSIGN in line: + key, value = line.split(CHAR_ASSIGN, 1) + key = key.strip() + value = value.strip() + self.attribMapping[key] = value + logger.debug("Adding SAML attribute to dictionary:"+\ + str(key)+"="+str(value)) + f.close() except IOError : - logger.warn( - "Cannot read dictionary for attributes mapping from the path: " - "%s" % dictLocation - ) + logger.warn("Cannot read dictionary for attributes mapping from the "+\ + "path: "+str(dictLocation)) # Extracts the asserted subject attributes from the OWS Request - def _getAssertedAttributes(self, request): - httpHeader = request.META + def _getAssertedAttributes(self, ows_req): + httpHeader = ows_req.http_req.META attributes = {} # adding the REMOTE_ADDR from HTTP header to subject attributes @@ -140,21 +148,18 @@ for key, value in self.attribMapping.iteritems(): if key in httpHeader: attributes[key] = httpHeader[value] - logger.debug( - "Found SAML attribute %s with value %s in incoming " - "request." % (key, httpHeader[value])) + logger.debug("Found SAML attribute "+str(key)+" with value "+\ + str(httpHeader[value])+" in incoming request.") else: - logger.info( - "The key '%s' specified in the mapping dictionary was not " - "found in the HTTP headers." % key - ) + logger.info('The key \''+key+'\' specified in the mapping ' +\ + 'dictionary was not found in the HTTP headers.') return attributes # Extracts the resource specific attributes from the OWS Request - def _getResourceAttributes(self, request): - httpHeader = request.META + def _getResourceAttributes(self, ows_req): + httpHeader = ows_req.http_req.META attributes = {} if self.serviceID == 'default' : @@ -162,30 +167,30 @@ else : attributes[attrib_resource] = self.serviceID - decoder = get_decoder(request) - attributes['serviceType'] = decoder.service.lower() + attributes['serviceType'] = str(ows_req.getParamValue("service")).lower() attributes['serverName'] = httpHeader['SERVER_NAME'] return attributes # performs the actual authz. decision - def _decide(self, request): + def _decide(self, ows_req): + + userAttributes = self._getAssertedAttributes(ows_req) + resourceAttributes = self._getResourceAttributes(ows_req) - decoder = get_decoder(request) - userAttributes = self._getAssertedAttributes(request) - resourceAttributes = self._getResourceAttributes(request) - - result = self.client.authorize( - userAttributes, resourceAttributes, decoder.request.lower() - ) + result = self.client.authorize(userAttributes, resourceAttributes, str(ows_req.getParamValue("operation")).lower()) return result + +# please do not remove this line +CharonPDPImplementation = PolicyDecisionPointInterface.implement(CharonPDP) + #------------------------------------------------------------------------------- # SOAP client for the CHARON Policy Management and Authorization Service #------------------------------------------------------------------------------- -class AuthorisationClient(object): +class AuthorisationClient: """ SOAP client for the CHARON Policy Management and Authorisation Service @@ -310,5 +315,18 @@ #------------------------------------------------------------------------------- class AuthorisationClientException(Exception): - """ Exception that is thrown by the AuthorisationClient in case of an error """ + Exception that is thrown by the AuthorisationClient in case of an error + + .. method:: __init__(authz_service_url) + + Constructor with an exception message + + .. method:: __str__() + + Returns the exception message + """ + def __init__(self, message): + self.message = message + def __str__(self): + return repr(self.message) diff -Nru eoxserver-0.4.0beta2/eoxserver/services/auth/dummypdp.py eoxserver-0.3.2/eoxserver/services/auth/dummypdp.py --- eoxserver-0.4.0beta2/eoxserver/services/auth/dummypdp.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/auth/dummypdp.py 2013-12-10 14:57:00.000000000 +0000 @@ -5,41 +5,47 @@ import xml.dom.minidom import eoxserver from urlparse import urlparse - -from eoxserver.core import implements -from eoxserver.services.auth.base import BasePDP -from eoxserver.services.auth.interfaces import PolicyDecisionPointInterface +from eoxserver.services.auth.base import BasePDP, \ + PolicyDecisionPointInterface, \ + AuthConfigReader from eoxserver.services.auth.charonpdp import CharonPDP logger = logging.getLogger(__name__) -validUser = { - 'uid': 'jdoe', - 'cn': 'Doe John', - 'sn': 'Doe', - 'description': 'Authorized User' -} +validUser = {'uid': 'jdoe', 'cn': 'Doe John', 'sn': 'Doe', 'description': 'Authorized User'} + class DummyPDP(BasePDP): - implements(PolicyDecisionPointInterface) + + REGISTRY_CONF = { + "name": "Dummy Policy Decision Point", + "impl_id": "services.auth.dummypdp.DummyPDP", + "registry_values": { + "services.auth.base.pdp_type": "dummypdp" + } + } def __init__(self): self.pdp = CharonPDP(DummyAuthzClient()) - def _decide(self, request): - httpHeader = request.META + def _decide(self, ows_req): + + httpHeader = ows_req.http_req.META #checks if a attribute 'DUMMY_MODE' is in the headers if 'DUMMY_MODE' in httpHeader: logger.info("Security Test: 'DUMMY_MODE' parameter in HTTP header") - return self.pdp._decide(request) + return self.pdp._decide(ows_req) else : return (True, 'No authorisation testing') +DummyPDPImplementation = PolicyDecisionPointInterface.implement(DummyPDP) class DummyAuthzClient(object): + def authorize(self, userAttributes, resourceAttributes, action): + for key, value in validUser.iteritems(): if key in userAttributes: if value != userAttributes[key]: diff -Nru eoxserver-0.4.0beta2/eoxserver/services/auth/exceptions.py eoxserver-0.3.2/eoxserver/services/auth/exceptions.py --- eoxserver-0.4.0beta2/eoxserver/services/auth/exceptions.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/auth/exceptions.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,33 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -class AuthorisationException(Exception): - """ - """ - - code = "AccessForbidden" diff -Nru eoxserver-0.4.0beta2/eoxserver/services/auth/interfaces.py eoxserver-0.3.2/eoxserver/services/auth/interfaces.py --- eoxserver-0.4.0beta2/eoxserver/services/auth/interfaces.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/auth/interfaces.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,21 +0,0 @@ -class PolicyDecisionPointInterface(object): - """ This is the interface for Policy Decision Point (PDP) implementations. - """ - - @property - def pdp_type(self): - """ The type name of this PDP. - """ - - def authorize(self, request): - """ This method takes an :class:`~.OWSRequest` object as input and returns an - :class:`~.AuthorizationResponse` instance. It is expected to check if - the authenticated user (if any) is authorized to access the requested - resource and set the ``authorized`` flag of the response accordingly. - - In case the user is not authorized, the content and status of the - response shall be filled with an error message and the appropriate - HTTP Status Code (403). - - The method shall not raise any exceptions. - """ diff -Nru eoxserver-0.4.0beta2/eoxserver/services/auth/middleware.py eoxserver-0.3.2/eoxserver/services/auth/middleware.py --- eoxserver-0.4.0beta2/eoxserver/services/auth/middleware.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/auth/middleware.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,105 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -import functools - -from django.http import HttpResponse - -from eoxserver.services.auth.base import getPDP -from eoxserver.services.auth.exceptions import AuthorisationException -from eoxserver.services.ows.common.v20.encoders import OWS20ExceptionXMLEncoder - - -class PDPMiddleware(object): - """ Middleware to allow authorization agains a Policy Decision Point. This - middleware will be used for *all* requests and *all* configured views. - If you only want to provide PDP authorization for a single view, use the - `pdp_protect`. - """ - - def process_view(self, request, view_func, view_args, view_kwargs): - pdp = getPDP() - if pdp: - try: - authorized = pdp.authorize(request) - message = "Not authorized" - code = "AccessForbidden" - except AuthorisationException, e: - authorized = False - message = str(e) - code = e.code - - if not authorized: - encoder = OWS20ExceptionXMLEncoder() - return HttpResponse( - encoder.serialize( - encoder.encode_exception(message, "2.0.0", code) - ), - encoder.content_type, status=403 - ) - - -def pdp_protect(view): - """ Wrapper function for views that shall be protected by PDP authorization. - This function can be used as a decorator of a view function, or as a - modifier to be used in the url configuration file. - e.g: - :: - - urlpatterns = patterns('', - ... - url(r'^ows', pdp_protect(ows)), - ... - ) - """ - - @functools.wraps(view) - def wrapped(request, *args, **kwargs): - pdp = getPDP() - if pdp: - try: - authorized = pdp.authorize(request) - message = "Not authorized" - code = "NotAuthorized" - except AuthorisationException, e: - authorized = False - message = str(e) - code = e.code - - if not authorized: - encoder = OWS20ExceptionXMLEncoder() - return HttpResponse( - encoder.serialize( - encoder.encode_exception(message, "2.0.0", code) - ), - encoder.content_type, status=403 - ) - - return view(request, *args, **kwargs) - - return wrapped diff -Nru eoxserver-0.4.0beta2/eoxserver/services/base.py eoxserver-0.3.2/eoxserver/services/base.py --- eoxserver-0.4.0beta2/eoxserver/services/base.py 1970-01-01 00:00:00.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/base.py 2013-12-10 14:57:00.000000000 +0000 @@ -0,0 +1,151 @@ +#------------------------------------------------------------------------------- +# $Id$ +# +# Project: EOxServer +# Authors: Stephan Krause +# Stephan Meissl +# +#------------------------------------------------------------------------------- +# Copyright (C) 2011 EOX IT Services GmbH +# +# 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 of this Software or works derived from this 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. +#------------------------------------------------------------------------------- + +from traceback import format_exc +import logging + +from django.conf import settings + +from eoxserver.core.exceptions import InternalError +from eoxserver.core.util.xmltools import DOMElementToXML +from eoxserver.services.requests import Response +from eoxserver.services.exceptions import ( + InvalidRequestException, VersionNegotiationException +) + + +logger = logging.getLogger(__name__) + +class BaseRequestHandler(object): + """ + Base class for all EOxServer Handler Implementations. + + There are two private methods that have to be overridden by child classes. + + .. method:: _handleException(req, exception) + + Abstract method which must be overridden by child classes to + provide specific exception reports. If the exception report + cannot be generated in this specific handler, the exception + should be re-raised. This is also the default behaviour. + + The method expects an :class:`~.OWSRequest` object ``req`` and + the exception that has been raised as input. It should return a + :class:`~.Response` object containing the exception report. + + .. method:: _processRequest(req) + + Abstract method which must be overridden to provide the specific + request handling logic. Should not be invoked from external code, + use the :meth:`handle` method instead. It expects an + :class:`~.OWSRequest` object ``req`` as input and should return a + :class:`~.Response` object containing the response to the request. + The default method does not do anything. + """ + def _handleException(self, req, exception): + raise + + def _processRequest(self, req): + pass + + def handle(self, req): + """ + Basic request handling method which should be invoked from + external code. This method invokes the :meth:`_processRequest` method + and returns the resulting :class:`~.Response` object unless an + exception is raised. In the latter case :meth:`_handleException` is + called and the appropriate response is returned. + """ + + try: + return self._processRequest(req) + except Exception, e: + return self._handleException(req, e) + +class BaseExceptionHandler(object): + """ + This is the basic handler for exceptions. It allows to generate exception + reports. + """ + def __init__(self, schemas=None): + self.schemas = schemas + + def _filterExceptions(self, exception): + raise + + def _getEncoder(self): + raise InternalError("Not implemented.") + + def _getExceptionReport(self, req, exception, encoder): + if isinstance(exception, VersionNegotiationException): + return DOMElementToXML(encoder.encodeVersionNegotiationException(exception)) + elif isinstance(exception, InvalidRequestException): + return DOMElementToXML(encoder.encodeInvalidRequestException(exception)) + else: + return DOMElementToXML(encoder.encodeException(exception)) + + def _getHTTPStatus(self, exception): + return 400 + + def _logError(self, req, exception): + logger.error(str(req.getParams())) + logger.error(str(exception)) + logger.debug(format_exc()) + + def _getContentType(self, exception): + raise InternalError("Not implemented.") + + def handleException(self, req, exception): + """ + This method can be invoked in order to handle an exception and produce + an exception report. It starts by logging the error to the default + log. Then the appropriate XML encoder is fetched (the + :meth:`_getEncoder` method has to be overridden by the subclass). + Finally, the exception report itself is encoded and the appropriate HTTP + status code determined. The method returns a :class:`~.Response` object + containing this information. + """ + + self._logError(req, exception) + + if settings.DEBUG: + self._filterExceptions(exception) + + encoder = self._getEncoder() + + content = self._getExceptionReport(req, exception, encoder) + + status = self._getHTTPStatus(exception) + + return Response( + content = content, + content_type = self._getContentType(exception), + headers = {}, + status = status + ) diff -Nru eoxserver-0.4.0beta2/eoxserver/services/connectors.py eoxserver-0.3.2/eoxserver/services/connectors.py --- eoxserver-0.4.0beta2/eoxserver/services/connectors.py 1970-01-01 00:00:00.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/connectors.py 2013-12-10 14:57:00.000000000 +0000 @@ -0,0 +1,193 @@ +#------------------------------------------------------------------------------- +# $Id$ +# +# Project: EOxServer +# Authors: Stephan Krause +# Stephan Meissl +# +#------------------------------------------------------------------------------- +# Copyright (C) 2011 EOX IT Services GmbH +# +# 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 of this Software or works derived from this 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. +#------------------------------------------------------------------------------- + +""" +Connectors are used to configure the data sources for MapServer requests. +Because of the different nature of the data sources (files, tile indices, +rasdaman databases) they have to be set up differently. Connectors allow to +do this transparently. +""" + +import os.path + +from eoxserver.contrib import osr +from eoxserver.services.mapserver import MapServerDataConnectorInterface +from eoxserver.resources.coverages import crss + + +class FileConnector(object): + """ + The :class:`FileConnector` class is the most common connector. It + configures a file as data source for the MapServer request. + """ + REGISTRY_CONF = { + "name": "Local File Connector", + "impl_id": "services.connectors.LocalFileConnector", # TODO: change this to FileConnector + "registry_values": { + "services.mapserver.data_structure_type": "file", + } + } + + def configure(self, layer, eo_object, filter_exprs = None): + """ + This method takes three arguments: ``layer`` is a MapServer + :class:`layerObj` instance, ``eo_object`` a EO-WCS object (either + a :class:`~.RectifiedDatasetWrapper` or + :class:`~.RectifiedStitchedMosaicWrapper` instance) and the optional + ``filter_exprs`` argument is currently not used. + + The method configures the MapServer layer by setting its ``data`` + property to the path. It invokes the + :meth:`~.DataPackageWrapper.prepareData` method of the + :class:`~.DataPackageWrapper` instance related to the object. + + The method also sets the projection on the layer. + """ + data_package = eo_object.getData() + data_package.prepareAccess() + + layer.data = data_package.getGDALDatasetIdentifier() + + # set layer's projection + layer.setProjection( crss.asProj4Str( eo_object.getSRID() )) + + return layer + +FileConnectorImplementation = \ +MapServerDataConnectorInterface.implement(FileConnector) + +class TiledPackageConnector(object): + """ + The :class:`TiledPackageConnector` class is intended for + :class:`~.RectifiedStitchedMosaicWrapper` instances that store their + data in tile indices. + """ + REGISTRY_CONF = { + "name": "Tiled Package Connector", + "impl_id": "services.connectors.TiledPackageConnector", + "registry_values": { + "services.mapserver.data_structure_type": "index" + } + } + + def configure(self, layer, eo_object, filter_exprs = None): + """ + This method takes three arguments: ``layer`` is a MapServer + :class:`layerObj` instance, ``eo_object`` a + :class:`~.RectifiedStitchedMosaicWrapper` instance and the optional + ``filter_exprs`` argument is currently not used. + + The method sets the ``tileindex`` property of the MapServer layer to + point to the shape file where the paths of the tiles are stored. + + The method also sets the projection on the layer. + """ + tile_index = eo_object.getData() + path = tile_index.getShapeFilePath() + + layer.tileindex = os.path.abspath(path) + layer.tileitem = "location" + + # set layer's projection + layer.setProjection( crss.asProj4Str( eo_object.getSRID() )) + + return layer + +TiledPackageConnectorImplementation = \ +MapServerDataConnectorInterface.implement(TiledPackageConnector) + +class RasdamanArrayConnector(object): + """ + The :class:`RasdamanArrayConnector` class is intended for + :class:`~.RectifiedDatasetWrapper` instances that store their + data in rasdaman arrays. + """ + REGISTRY_CONF = { + "name": "Rasdaman Array Connector", + "impl_id": "services.connectors.RasdamanArrayConnector", + "registry_values": { + "services.mapserver.data_structure_type": "rasdaman_array" + } + } + + def configure(self, layer, eo_object, filter_exprs = None): + """ + This method takes three arguments: ``layer`` is a MapServer + :class:`layerObj` instance, ``eo_object`` a + :class:`~.RectifiedDatasetWrapper` instance and the optional + ``filter_exprs`` argument is currently not used. + + The method sets the ``data`` property of the MapServer layer to the + connection string to the rasdaman database array, see the `GDAL + rasdaman format `_ + page for details. + + Furthermore, the projection settings on the layer are configured + according to the metadata in the EOxServer database. As the + rasdaman arrays have pixel coordinates only, the parameters for + conversion from pixel coordinates to the spatial reference system have + to be set explicitly. + """ + data_package = eo_object.getData() + data_package.prepareAccess() + + layer.data = data_package.getGDALDatasetIdentifier() + + #--------------------------------------------------------------- + # define custom coordinate system + #--------------------------------------------------------------- + + srid = eo_object.getSRID() + + srs = osr.SpatialReference() + srs.ImportFromEPSG(srid) + + minx, miny, maxx, maxy = eo_object.getExtent() + size_x, size_y = eo_object.getSize() + + false_northing = srs.GetProjParm("false_northing") + false_easting = srs.GetProjParm("false_easting") + x_0 = false_easting + minx + y_0 = false_northing + miny + to_unit = (maxx - minx) / float(size_x) + + if srs.IsProjected(): + + proj_str = "+init=epsg:%d +x_0=%f +y_0=%f +to_meters=%f" %\ + (srid, x_0, y_0, to_unit) + else: + proj_str = "+init=epsg:%d +x_0=%f +y_0=%f +to_degrees=%f" %\ + (srid, x_0, y_0, to_unit) + + layer.setProjection(proj_str) + + return layer + +RasdamanArrayConnectorImplementation = \ +MapServerDataConnectorInterface.implement(RasdamanArrayConnector) diff -Nru eoxserver-0.4.0beta2/eoxserver/services/exceptions.py eoxserver-0.3.2/eoxserver/services/exceptions.py --- eoxserver-0.4.0beta2/eoxserver/services/exceptions.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/exceptions.py 2013-12-10 14:57:00.000000000 +0000 @@ -1,4 +1,5 @@ #------------------------------------------------------------------------------- +# $Id$ # # Project: EOxServer # Authors: Stephan Krause @@ -10,8 +11,8 @@ # 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 +# 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 @@ -26,247 +27,50 @@ # THE SOFTWARE. #------------------------------------------------------------------------------- +from eoxserver.core.exceptions import EOxSException -class InvalidRequestException(Exception): +class InvalidRequestException(EOxSException): """ This exception indicates that the request was invalid and an exception report shall be returned to the client. - + The constructor takes three arguments, namely ``msg``, the error message, - ``code``, the error code, and ``locator``, which is needed in OWS + ``error_code``, the error code, and ``locator``, which is needed in OWS exception reports for indicating which part of the request produced the error. - + How exactly the exception reports are constructed is not defined by the exception, but by exception handlers. """ - def __init__(self, msg, code=None, locator=None): + def __init__(self, msg, error_code, locator): super(InvalidRequestException, self).__init__(msg) - - self.code = code or "InvalidRequest" + + self.msg = msg + self.error_code = error_code self.locator = locator - + def __str__(self): - return "Invalid Request: Code: %s; Locator: %s; Message: '%s'" % ( - self.code, self.locator, - super(InvalidRequestException, self).__str__() + return "Invalid Request: ErrorCode: %s; Locator: %s; Message: '%s'" % ( + self.error_code, self.locator, self.msg ) - -class VersionNegotiationException(Exception): +class VersionNegotiationException(EOxSException): """ This exception indicates that version negotiation fails. Such errors can happen with OWS 2.0 compliant "new-style" version negotation. """ - code = "VersionNegotiationFailed" - - def __str__(self): - return "Version negotiation failed." - - -class LocatorListException(Exception): - """ Base class for exceptions that report that a number of items are - missing or invalid - """ - def __init__(self, items): - self.items = items - - @property - def locator(self): - "This property provides a list of all missing/invalid items." - return " ".join(self.items) - + pass -class InvalidAxisLabelException(Exception): +class InvalidAxisLabelException(EOxSException): """ This exception indicates that an invalid axis name was chosen in a WCS 2.0 subsetting parameter. """ - code = "InvalidAxisLabel" + pass - def __init__(self, axis_label): - super(InvalidAxisLabelException, self).__init__( - "Invalid axis label: '%s'." % axis_label - ) - self.locator = axis_label - - -class InvalidSubsettingException(Exception): +class InvalidSubsettingException(EOxSException): """ This exception indicates an invalid WCS 2.0 subsetting parameter was submitted. """ - code = "InvalidSubsetting" - locator = "subset" - - -class InvalidSubsettingCrsException(Exception): - """ - This exception indicates an invalid WCS 2.0 subsettingCrs parameter was - submitted. - """ - code = "SubsettingCrs-NotSupported" - locator = "subsettingCrs" - - -class InvalidOutputCrsException(Exception): - """ - This exception indicates an invalid WCS 2.0 outputCrs parameter was - submitted. - """ - code = "OutputCrs-NotSupported" - locator = "outputCrs" - - -class NoSuchCoverageException(LocatorListException): - """ This exception indicates that the requested coverage(s) do not - exist. - """ - code = "NoSuchCoverage" - - def __str__(self): - return "No such Coverage%s with ID: %s" % ( - "" if len(self.items) == 1 else "s", - ", ".join(map(lambda i: "'%s'" % i, self.items)) - ) - - -class NoSuchDatasetSeriesOrCoverageException(LocatorListException): - """ This exception indicates that the requested coverage(s) or dataset - series do not exist. - """ - code = "NoSuchDatasetSeriesOrCoverage" - - def __str__(self): - return "No such Coverage%s or Dataset Series with ID: %s" % ( - " " if len(self.items) == 1 else "s", - ", ".join(map(lambda i: "'%s'" % i, self.items)) - ) - - -class OperationNotSupportedException(Exception): - """ Exception to be thrown when some operations are not supported or - disabled. - """ - def __init__(self, message, operation=None): - super(OperationNotSupportedException, self).__init__(message) - self.operation = operation - - @property - def locator(self): - return self.operation - - code = "OperationNotSupported" - - -class ServiceNotSupportedException(OperationNotSupportedException): - """ Exception to be thrown when a specific OWS service is not enabled. - """ - def __init__(self, service): - self.service = service - - def __str__(self): - if self.service: - return "Service '%s' is not supported." % self.service.upper() - else: - return "Service is not supported." - - -class VersionNotSupportedException(Exception): - """ Exception to be thrown when a specific OWS service version is not - supported. - """ - def __init__(self, service, version): - self.service = service - self.version = version - - def __str__(self): - if self.service: - return "Service '%s' version '%s' is not supported." % ( - self.service, self.version - ) - else: - return "Version '%s' is not supported." % self.version - - code = "InvalidParameterValue" - - -class InterpolationMethodNotSupportedException(Exception): - """ - This exception indicates a not supported interpolation method. - """ - code = "InterpolationMethodNotSupported" - locator = "interpolation" - - -class RenderException(Exception): - """ Rendering related exception. - """ - def __init__(self, message, locator, is_parameter=True): - super(RenderException, self).__init__(message) - self.locator = locator - self.is_parameter = is_parameter - - @property - def code(self): - return ( - "InvalidParameterValue" if self.is_parameter else "InvalidRequest" - ) - - -class NoSuchFieldException(Exception): - """ Error in RangeSubsetting when band does not exist. - """ - - code = "NoSuchField" - - def __init__(self, msg, locator): - super(NoSuchFieldException, self).__init__(msg) - self.locator = locator - - -class InvalidFieldSequenceException(Exception): - """ Error in RangeSubsetting for illegal intervals. - """ - code = "InvalidFieldSequence" - - def __init__(self, msg, locator): - super(NoSuchFieldException, self).__init__(msg) - self.locator = locator - - -class InvalidScaleFactorException(Exception): - """ Error in ScaleFactor and ScaleAxis operations - """ - code = "InvalidScaleFactor" - - def __init__(self, scalefactor): - super(InvalidScaleFactorException, self).__init__( - "Scalefactor '%s' is not valid" % scalefactor - ) - self.locator = scalefactor - - -class InvalidScaleExtentException(Exception): - """ Error in ScaleExtent operations - """ - code = "InvalidExtent" - - def __init__(self, low, high): - super(InvalidScaleExtentException, self).__init__( - "ScaleExtent '%s:%s' is not valid" % (low, high) - ) - self.locator = high - - -class ScaleAxisUndefinedException(Exception): - """ Error in all scaling operations involving an axis - """ - - code = "ScaleAxisUndefined" - - def __init__(self, axis): - super(ScaleAxisUndefinedException, self).__init__( - "Scale axis '%s' is undefined" % axis - ) - self.locator = axis + pass diff -Nru eoxserver-0.4.0beta2/eoxserver/services/gdal/wcs/referenceable_dataset_renderer.py eoxserver-0.3.2/eoxserver/services/gdal/wcs/referenceable_dataset_renderer.py --- eoxserver-0.4.0beta2/eoxserver/services/gdal/wcs/referenceable_dataset_renderer.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/gdal/wcs/referenceable_dataset_renderer.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,315 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -from os.path import splitext, abspath -from datetime import datetime -from uuid import uuid4 -import logging - -from django.contrib.gis.geos import GEOSGeometry - -from eoxserver.core import Component, implements -from eoxserver.core.config import get_eoxserver_config -from eoxserver.core.decoders import config -from eoxserver.core.util.rect import Rect -from eoxserver.backends.access import connect -from eoxserver.contrib import gdal, osr -from eoxserver.contrib.vrt import VRTBuilder -from eoxserver.resources.coverages import models -from eoxserver.services.ows.version import Version -from eoxserver.services.result import ResultFile, ResultBuffer -from eoxserver.services.ows.wcs.interfaces import WCSCoverageRendererInterface -from eoxserver.services.ows.wcs.v20.encoders import WCS20EOXMLEncoder -from eoxserver.services.exceptions import ( - RenderException, OperationNotSupportedException -) -from eoxserver.processing.gdal import reftools - - -logger = logging.getLogger(__name__) - - -class GDALReferenceableDatasetRenderer(Component): - implements(WCSCoverageRendererInterface) - - versions = (Version(2, 0),) - - def supports(self, params): - return ( - issubclass(params.coverage.real_type, models.ReferenceableDataset) - and params.version in self.versions - ) - - - def render(self, params): - # get the requested coverage, data items and range type. - coverage = params.coverage - data_items = coverage.data_items.filter(semantic__startswith="bands") - range_type = coverage.range_type - - subsets = params.subsets - - # GDAL source dataset. Either a single file dataset or a composed VRT - # dataset. - src_ds = self.get_source_dataset( - coverage, data_items, range_type - ) - - # retrieve area of interest of the source image according to given - # subsets - src_rect, dst_rect = self.get_source_and_dest_rect(src_ds, subsets) - - # deduct "native" format of the source image - native_format = data_items[0].format if len(data_items) == 1 else None - - # get the requested image format, which defaults to the native format - # if available - frmt = params.format or native_format - - if not frmt: - raise RenderException("No format specified.", "format") - - if params.scalefactor is not None or params.scales: - raise RenderException( - "ReferenceableDataset cannot be scaled.", - "scalefactor" if params.scalefactor is not None else "scale" - ) - - maxsize = WCSConfigReader(get_eoxserver_config()).maxsize - if maxsize is not None: - if maxsize < dst_rect.size_x or maxsize < dst_rect.size_y: - raise RenderException( - "Requested image size %dpx x %dpx exceeds the allowed " - "limit maxsize=%dpx." % ( - dst_rect.size_x, dst_rect.size_y, maxsize - ), "size" - ) - - # perform subsetting either with or without rangesubsetting - subsetted_ds = self.perform_subset( - src_ds, range_type, src_rect, dst_rect, params.rangesubset - ) - - # encode the processed dataset and save it to the filesystem - out_ds, out_driver = self.encode( - subsetted_ds, frmt, getattr(params, "encoding_params", {}) - ) - - driver_metadata = out_driver.GetMetadata_Dict() - mime_type = driver_metadata.get("DMD_MIMETYPE") - extension = driver_metadata.get("DMD_EXTENSION") - - time_stamp = datetime.now().strftime("%Y%m%d%H%M%S") - filename_base = "%s_%s" % (coverage.identifier, time_stamp) - - result_set = [ - ResultFile( - path, mime_type, "%s.%s" % (filename_base, extension), - ("cid:coverage/%s" % coverage.identifier) if i == 0 else None - ) for i, path in enumerate(out_ds.GetFileList()) - ] - - if params.mediatype and params.mediatype.startswith("multipart"): - reference = "cid:coverage/%s" % result_set[0].filename - - if subsets.has_x and subsets.has_y: - footprint = GEOSGeometry(reftools.get_footprint_wkt(out_ds)) - if not subsets.srid: - extent = footprint.extent - else: - extent = subsets.xy_bbox - encoder_subset = ( - subsets.srid, src_rect.size, extent, footprint - ) - else: - encoder_subset = None - - encoder = WCS20EOXMLEncoder() - content = encoder.serialize( - encoder.encode_referenceable_dataset( - coverage, range_type, reference, mime_type, encoder_subset - ) - ) - result_set.insert(0, ResultBuffer(content, encoder.content_type)) - - return result_set - - - def get_source_dataset(self, coverage, data_items, range_type): - if len(data_items) == 1: - return gdal.OpenShared(abspath(connect(data_items[0]))) - else: - vrt = VRTBuilder( - coverage.size_x, coverage.size_y, - vrt_filename=temp_vsimem_filename() - ) - - # sort in ascending order according to semantic - data_items = sorted(data_items, key=(lambda d: d.semantic)) - - gcps = [] - compound_index = 0 - for data_item in data_items: - path = abspath(connect(data_item)) - - # iterate over all bands of the data item - for set_index, item_index in self._data_item_band_indices(data_item): - if set_index != compound_index + 1: - raise ValueError - compound_index = set_index - - band = range_type[set_index] - vrt.add_band(band.data_type) - vrt.add_simple_source( - set_index, path, item_index - ) - - return vrt.dataset - - - def get_source_and_dest_rect(self, dataset, subsets): - size_x, size_y = dataset.RasterXSize, dataset.RasterYSize - image_rect = Rect(0, 0, size_x, size_y) - - if not subsets: - subset_rect = image_rect - - # pixel subset - elif subsets.srid is None: # means "imageCRS" - minx, miny, maxx, maxy = subsets.xy_bbox - - minx = int(minx) if minx is not None else image_rect.offset_x - miny = int(miny) if miny is not None else image_rect.offset_y - maxx = int(maxx) if maxx is not None else image_rect.upper_x - maxy = int(maxy) if maxy is not None else image_rect.upper_y - - subset_rect = Rect(minx, miny, maxx-minx+1, maxy-miny+1) - - # subset in geographical coordinates - else: - vrt = VRTBuilder(*image_rect.size) - vrt.copy_gcps(dataset) - - options = reftools.suggest_transformer(dataset) - - subset_rect = reftools.rect_from_subset( - vrt.dataset, subsets.srid, *subsets.xy_bbox, **options - ) - - # check whether or not the subsets intersect with the image - if not image_rect.intersects(subset_rect): - raise RenderException("Subset outside coverage extent.", "subset") - - src_rect = subset_rect #& image_rect # TODO: why no intersection?? - dst_rect = src_rect - subset_rect.offset - - return src_rect, dst_rect - - - def perform_subset(self, src_ds, range_type, subset_rect, dst_rect, - rangesubset=None): - vrt = VRTBuilder(*subset_rect.size) - - input_bands = list(range_type) - - # list of band indices/names. defaults to all bands - if rangesubset: - subset_bands = rangesubset.get_band_indices(range_type, 1) - else: - subset_bands = xrange(1, len(range_type) + 1) - - for dst_index, src_index in enumerate(subset_bands, start=1): - input_band = input_bands[src_index-1] - vrt.add_band(input_band.data_type) - vrt.add_simple_source( - dst_index, src_ds, src_index, subset_rect, dst_rect - ) - - vrt.copy_metadata(src_ds) - vrt.copy_gcps(src_ds, subset_rect) - - return vrt.dataset - - - def encode(self, dataset, frmt, encoding_params): - options = () - if frmt == "image/tiff": - options = _get_gtiff_options(**encoding_params) - - args = [ ("%s=%s" % key, value) for key, value in options ] - - path = "/tmp/%s" % uuid4().hex - out_driver = gdal.GetDriverByName("GTiff") - return out_driver.CreateCopy(path, dataset, True, args), out_driver - - -def index_of(iterable, predicate, default=None, start=1): - for i, item in enumerate(iterable, start): - if predicate(item): - return i - return default - - -def temp_vsimem_filename(): - return "/vsimem/%s" % uuid4().hex - - -def _get_gtiff_options(compression=None, jpeg_quality=None, - predictor=None, interleave=None, tiling=False, - tilewidth=None, tileheight=None): - - logger.info("Applying GeoTIFF parameters.") - - if compression: - if compression.lower() == "huffman": - compression = "CCITTRLE" - yield ("COMPRESS", compression.upper()) - - if jpeg_quality is not None: - yield ("JPEG_QUALITY", str(jpeg_quality)) - - if predictor: - pr = ["NONE", "HORIZONTAL", "FLOATINGPOINT"].index(predictor.upper()) - if pr == -1: - raise ValueError("Invalid compression predictor '%s'." % predictor) - yield ("PREDICTOR", str(pr + 1)) - - if interleave: - yield ("INTERLEAVE", interleave) - - if tiling: - yield ("TILED", "YES") - if tilewidth is not None: - yield ("BLOCKXSIZE", str(tilewidth)) - if tileheight is not None: - yield ("BLOCKYSIZE", str(tileheight)) - - -class WCSConfigReader(config.Reader): - section = "services.ows.wcs" - maxsize = config.Option(type=int, default=None) diff -Nru eoxserver-0.4.0beta2/eoxserver/services/gml/v32/encoders.py eoxserver-0.3.2/eoxserver/services/gml/v32/encoders.py --- eoxserver-0.4.0beta2/eoxserver/services/gml/v32/encoders.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/gml/v32/encoders.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,147 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2014 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -from lxml.builder import ElementMaker - -from eoxserver.core.util.xmltools import NameSpace, NameSpaceMap, ns_xsi -from eoxserver.core.util.timetools import isoformat -from eoxserver.resources.coverages import crss - -# namespace declarations -ns_gml = NameSpace("http://www.opengis.net/gml/3.2", "gml") -ns_gmlcov = NameSpace("http://www.opengis.net/gmlcov/1.0", "gmlcov") -ns_om = NameSpace("http://www.opengis.net/om/2.0", "om") -ns_eop = NameSpace("http://www.opengis.net/eop/2.0", "eop") - -nsmap = NameSpaceMap(ns_gml, ns_gmlcov, ns_om, ns_eop) - -# Element factories -GML = ElementMaker(namespace=ns_gml.uri, nsmap=nsmap) -GMLCOV = ElementMaker(namespace=ns_gmlcov.uri, nsmap=nsmap) -OM = ElementMaker(namespace=ns_om.uri, nsmap=nsmap) -EOP = ElementMaker(namespace=ns_eop.uri, nsmap=nsmap) - - -class GML32Encoder(object): - def encode_linear_ring(self, ring, sr): - frmt = "%.3f %.3f" if sr.projected else "%.8f %.8f" - - swap = crss.getAxesSwapper(sr.srid) - pos_list = " ".join(frmt % swap(*point) for point in ring) - - return GML("LinearRing", - GML("posList", - pos_list - ) - ) - - def encode_polygon(self, polygon, base_id): - return GML("Polygon", - GML("exterior", - self.encode_linear_ring(polygon[0], polygon.srs) - ), - *(GML("interior", - self.encode_linear_ring(interior, polygon.srs) - ) for interior in polygon[1:]), - **{ns_gml("id"): "polygon_%s" % base_id} - ) - - def encode_multi_surface(self, geom, base_id): - if geom.geom_typeid in (6, 7): # MultiPolygon and GeometryCollection - polygons = [ - self.encode_polygon(polygon, "%s_%d" % (base_id, i+1)) - for i, polygon in enumerate(geom) - ] - elif geom.geom_typeid == 3: # Polygon - polygons = [self.encode_polygon(geom, base_id)] - - return GML("MultiSurface", - *[GML("surfaceMember", polygon) for polygon in polygons], - **{ns_gml("id"): "multisurface_%s" % base_id, - "srsName": "EPSG:%d" % geom.srid - } - ) - - def encode_time_period(self, begin_time, end_time, identifier): - return GML("TimePeriod", - GML("beginPosition", isoformat(begin_time)), - GML("endPosition", isoformat(end_time)), - **{ns_gml("id"): identifier} - ) - - def encode_time_instant(self, time, identifier): - return GML("TimeInstant", - GML("timePosition", isoformat(time)), - **{ns_gml("id"): identifier} - ) - -class EOP20Encoder(GML32Encoder): - def encode_footprint(self, footprint, eo_id): - return EOP("Footprint", - EOP("multiExtentOf", self.encode_multi_surface(footprint, eo_id)), - **{ns_gml("id"): "footprint_%s" % eo_id} - ) - - def encode_metadata_property(self, eo_id, contributing_datasets=None): - return EOP("metaDataProperty", - EOP("EarthObservationMetaData", - EOP("identifier", eo_id), - EOP("acquisitionType", "NOMINAL"), - EOP("status", "ARCHIVED"), - *([EOP("composedOf", contributing_datasets)] - if contributing_datasets else [] - ) - ) - ) - - def encode_earth_observation(self, eo_metadata, contributing_datasets=None, subset_polygon=None): - identifier = eo_metadata.identifier - begin_time = eo_metadata.begin_time - end_time = eo_metadata.end_time - result_time = eo_metadata.end_time - footprint = eo_metadata.footprint - - if subset_polygon is not None: - footprint = footprint.intersection(subset_polygon) - - - return EOP("EarthObservation", - OM("phenomenonTime", - self.encode_time_period(begin_time, end_time, "phen_time_%s" % identifier) - ), - OM("resultTime", - self.encode_time_instant(result_time, "res_time_%s" % identifier) - ), - OM("procedure"), - OM("observedProperty"), - OM("featureOfInterest", - self.encode_footprint(footprint, identifier) - ), - OM("result"), - self.encode_metadata_property(identifier, contributing_datasets), - **{ns_gml("id"): "eop_%s" % identifier} - ) diff -Nru eoxserver-0.4.0beta2/eoxserver/services/interfaces.py eoxserver-0.3.2/eoxserver/services/interfaces.py --- eoxserver-0.4.0beta2/eoxserver/services/interfaces.py 1970-01-01 00:00:00.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/interfaces.py 2013-12-10 14:57:00.000000000 +0000 @@ -0,0 +1,198 @@ +#------------------------------------------------------------------------------- +# $Id$ +# +# Project: EOxServer +# Authors: Stephan Krause +# Stephan Meissl +# +#------------------------------------------------------------------------------- +# Copyright (C) 2011 EOX IT Services GmbH +# +# 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 of this Software or works derived from this 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. +#------------------------------------------------------------------------------- + +""" +This module defines interfaces for service request handlers. + +EOxServer follows a cascaded approach for handling OWS requests: First, a +service handler takes in all requests for a specific service, e.g. WMS or WCS. +Second, the request gets passed on to the appropriate version handler. Last, +the actual operation handler for that request is invoked. + +This cascaded approach shall ensure that features that relate to every operation +of a service or service version (most importantly exception handling) can be +implemented centrally. +""" + +from eoxserver.core.interfaces import * +from eoxserver.core.registry import RegisteredInterface +from eoxserver.services.requests import OWSRequest, Response + +class RequestHandlerInterface(RegisteredInterface): + """ + This is the basic interface for OWS request handling. It is the parent + class of the other handler interfaces. The binding method is KVP. The + interface does not define any keys though, which is done by the child + classes. + + .. method:: handle(req) + + This method shall be called for handling the request. It expects an + :class:`~.OWSRequest` object as input ``req`` and shall return a + :class:`~.Response` object. + + """ + REGISTRY_CONF = { + "name": "Request Handler Interface", + "intf_id": "services.interfaces.RequestHandler", + "binding_method": "kvp", + "registry_keys": () + } + + handle = Method( + ObjectArg("req", arg_class=OWSRequest), + returns = ObjectArg("@return", arg_class=Response) + ) + +class ServiceHandlerInterface(RequestHandlerInterface): + """ + This interface inherits from :class:`RequestHandlerInterface`. It adds + no methods, but a registry key ``services.interfaces.service`` which + allows to bind to an implementation given the name of the service. + """ + REGISTRY_CONF = { + "name": "Service Handler Interface", + "intf_id": "services.interfaces.ServiceHandler", + "binding_method": "kvp", + "registry_keys": ( + "services.interfaces.service", + ) + } + +class VersionHandlerInterface(RequestHandlerInterface): + """ + This interface inherits from :class:`RequestHandlerInterface`. It adds + no methods, but the registry keys ``services.interfaces.service`` and + ``services.interface.version`` which allow to bind to an implementation + given the name of the service and the version descriptor. + """ + + REGISTRY_CONF = { + "name": "Service Handler Interface", + "intf_id": "services.interfaces.VersionHandler", + "binding_method": "kvp", + "registry_keys": ( + "services.interfaces.service", + "services.interfaces.version" + ) + } + +class OperationHandlerInterface(RequestHandlerInterface): + """ + This interface inherits from :class:`RequestHandlerInterface`. It adds + no methods, but the registry keys ``services.interfaces.service``, + ``services.interface.version`` and ``services.interfaces.operation`` which + allow to bind to an implementation given the name of the service, the + version descriptor and the operation name. + """ + + REGISTRY_CONF = { + "name": "Service Handler Interface", + "intf_id": "services.interfaces.OperationHandler", + "binding_method": "kvp", + "registry_keys": ( + "services.interfaces.service", + "services.interfaces.version", + "services.interfaces.operation" + ) + } + +class ExceptionHandlerInterface(RegisteredInterface): + """ + This interface is intended for exception handlers. These handlers shall + be invoked when an exception is raised during the processing of an OWS + request. + + .. method:: handleException(req, exception) + + This method shall handle an exception. It expects the original + :class:`~.OWSRequest` object ``req`` as well as the exception object as + input. The expected output is a :class:`~.Response` object which shall + contain an exception report and whose content will be sent to the client. + + In case the exception handler does not recognize a given type of exception + or cannot produce an appropriate exception report, the exception shall + be re-raised. + """ + REGISTRY_CONF = { + "name": "Exception Handler Interface", + "intf_id": "services.interfaces.ExceptionHandler", + "binding_method": "kvp", + "registry_keys": ( + "services.interfaces.exception_scheme", + ) + } + + handleException = Method( + ObjectArg("req", arg_class=OWSRequest), + ObjectArg("exception", arg_class=Exception), + returns = ObjectArg("@return", arg_class=Response) + ) + +class ExceptionEncoderInterface(RegisteredInterface): + """ + This interface is intended for encoding OWS exception reports. + + .. method:: encodeInvalidRequestException(exception) + + This method shall return an exception report for an + :class:`~.InvalidRequestException`. + + .. method:: encodeVersionNegotiationException(exception) + + This method shall return an exception report for a + :class:`~.VersionNegotiationException`. + + .. method:: encodeException(exception) + + This method shall return an exception report for any kind of exception. + """ + REGISTRY_CONF = { + "name": "OWS Exception Report XML Encoder Interface", + "intf_id": "services.interfaces.ExceptionEncoder", + "binding_method": "kvp", + "registry_keys": ( + "services.interfaces.exception_scheme", + ) + } + + encodeInvalidRequestException = Method( + ObjectArg("exception", arg_class=Exception), + returns = StringArg("@return") + ) + + encodeVersionNegotiationException = Method( + ObjectArg("exception", arg_class=Exception), + returns = StringArg("@return") + ) + + encodeException = Method( + ObjectArg("exception", arg_class=Exception), + returns = StringArg("@return") + ) diff -Nru eoxserver-0.4.0beta2/eoxserver/services/mapserver/connectors/multifile_connector.py eoxserver-0.3.2/eoxserver/services/mapserver/connectors/multifile_connector.py --- eoxserver-0.4.0beta2/eoxserver/services/mapserver/connectors/multifile_connector.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/mapserver/connectors/multifile_connector.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,86 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2011 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -from os.path import join -from uuid import uuid4 - -from eoxserver.core import Component, implements -from eoxserver.backends.access import connect -from eoxserver.contrib import vsi, vrt -from eoxserver.services.mapserver.interfaces import ConnectorInterface - - -class MultiFileConnector(Component): - """ Connects multiple files containing the various bands of the coverage - with the given layer. A temporary VRT file is used as abstraction for - the different band files. - """ - - implements(ConnectorInterface) - - def supports(self, data_items): - # TODO: better checks - return ( - len(data_items) > 1 - and all( - map(lambda d: d.semantic.startswith("bands"), data_items) - ) - ) - - def connect(self, coverage, data_items, layer, options): - - # TODO: implement - vrt_doc = vrt.VRT() - # TODO: configure vrt here - - path = join("/vsimem", uuid4().hex) - with vsi.open(path, "w+") as f: - vrt_doc.write(f) - - - # TODO!! - if layer.metadata.get("eoxs_wrap_dateline") == "true": - e = wrap_extent_around_dateline(coverage.extent, coverage.srid) - - vrt_path = join("/vsimem", uuid4().hex) - ds = gdal.Open(data) - vrt_ds = create_simple_vrt(ds, vrt_path) - size_x = ds.RasterXSize - size_y = ds.RasterYSize - - dx = abs(e[0] - e[2]) / size_x - dy = abs(e[1] - e[3]) / size_y - - vrt_ds.SetGeoTransform([e[0], dx, 0, e[3], 0, -dy]) - vrt_ds = None - - layer.data = vrt_path - - layer.data = path - - def disconnect(self, coverage, data_items, layer, options): - vsi.remove(layer.data) diff -Nru eoxserver-0.4.0beta2/eoxserver/services/mapserver/connectors/polygonmask_connector.py eoxserver-0.3.2/eoxserver/services/mapserver/connectors/polygonmask_connector.py --- eoxserver-0.4.0beta2/eoxserver/services/mapserver/connectors/polygonmask_connector.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/mapserver/connectors/polygonmask_connector.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,102 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2011 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -from eoxserver.core import Component, implements -from eoxserver.contrib import ogr -from eoxserver.contrib import mapserver as ms -from eoxserver.backends.access import connect -from eoxserver.services.mapserver.interfaces import ConnectorInterface - - -class PolygonMaskConnector(Component): - """ Connects polygon mask files to MapServer polygon layers. For some - purposes this can also be done via "reverse" polygons, where the actual - polygons are subtracted from the coverages footprint. - """ - - implements(ConnectorInterface) - - def supports(self, data_items): - num = len(data_items) - return ( - len(data_items) >= 1 - and len(filter( - lambda d: d.semantic.startswith("polygonmask"), data_items - )) == num - ) - - def connect(self, coverage, data_items, layer, options): - mask_item = data_items[0] - - try: - is_reversed = ( - layer.metadata.get("eoxs_geometry_reversed") == "true" - ) - except ms.MapServerError: - is_reversed = False - - # check if the geometry is "reversed" - if is_reversed: - # TODO: better use the coverages Extent? - geom_types = (ogr.wkbPolygon, ogr.wkbMultiPolygon) - output_polygon = ogr.Geometry(wkt=str(coverage.footprint.wkt)) - - for mask_item in data_items: - ds = ogr.Open(connect(mask_item)) - for i in range(ds.GetLayerCount()): - ogr_layer = ds.GetLayer(i) - if not ogr_layer: - continue - - feature = ogr_layer.GetNextFeature() - while feature: - # TODO: reproject if necessary - geometry = feature.GetGeometryRef() - if geometry.GetGeometryType() not in geom_types: - continue - if geometry: - output_polygon = output_polygon.Difference(geometry) - feature = ogr_layer.GetNextFeature() - - # since we have the geometry already in memory, add it to the layer - # as WKT - shape = ms.shapeObj.fromWKT(output_polygon.ExportToWkt()) - shape.initValues(1) - shape.setValue(0, coverage.identifier) - layer.addFeature(shape) - - else: - layer.connectiontype = ms.MS_OGR - layer.connection = connect(data_items[0]) - # TODO: more than one mask_item? - - layer.setProjection("EPSG:4326") - layer.setMetaData("ows_srs", "EPSG:4326") - layer.setMetaData("wms_srs", "EPSG:4326") - - def disconnect(self, coverage, data_items, layer, options): - pass diff -Nru eoxserver-0.4.0beta2/eoxserver/services/mapserver/connectors/simple_connector.py eoxserver-0.3.2/eoxserver/services/mapserver/connectors/simple_connector.py --- eoxserver-0.4.0beta2/eoxserver/services/mapserver/connectors/simple_connector.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/mapserver/connectors/simple_connector.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,85 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2011 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -from os.path import join -from uuid import uuid4 - -from eoxserver.core import Component, implements -from eoxserver.backends.access import connect -from eoxserver.contrib import vsi, gdal -from eoxserver.services.mapserver.interfaces import ConnectorInterface -from eoxserver.processing.gdal.vrt import create_simple_vrt -from eoxserver.processing.gdal import reftools -from eoxserver.resources.coverages.dateline import wrap_extent_around_dateline -from eoxserver.resources.coverages import models - - -class SimpleConnector(Component): - """ Connector for single file layers. - """ - implements(ConnectorInterface) - - def supports(self, data_items): - filtered = filter(lambda d: d.semantic.startswith("bands"), data_items) - return len(filtered) == 1 - - def connect(self, coverage, data_items, layer, options): - filtered = filter(lambda d: d.semantic.startswith("bands"), data_items) - data = connect(filtered[0]) - - if isinstance(coverage, models.ReferenceableDataset): - vrt_path = join("/vsimem", uuid4().hex) - reftools.create_rectified_vrt(data, vrt_path) - data = vrt_path - layer.setMetaData("eoxs_ref_data", data) - - if not layer.metadata.get("eoxs_wrap_dateline") == "true": - layer.data = data - else: - e = wrap_extent_around_dateline(coverage.extent, coverage.srid) - - vrt_path = join("/vsimem", uuid4().hex) - ds = gdal.Open(data) - vrt_ds = create_simple_vrt(ds, vrt_path) - size_x = ds.RasterXSize - size_y = ds.RasterYSize - - dx = abs(e[0] - e[2]) / size_x - dy = abs(e[1] - e[3]) / size_y - - vrt_ds.SetGeoTransform([e[0], dx, 0, e[3], 0, -dy]) - vrt_ds = None - - layer.data = vrt_path - - def disconnect(self, coverage, data_items, layer, options): - if layer.metadata.get("eoxs_wrap_dateline") == "true": - vsi.remove(layer.data) - - vrt_path = layer.metadata.get("eoxs_ref_data") - if vrt_path: - vsi.remove(vrt_path) diff -Nru eoxserver-0.4.0beta2/eoxserver/services/mapserver/connectors/tileindex_connector.py eoxserver-0.3.2/eoxserver/services/mapserver/connectors/tileindex_connector.py --- eoxserver-0.4.0beta2/eoxserver/services/mapserver/connectors/tileindex_connector.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/mapserver/connectors/tileindex_connector.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,53 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2011 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -import os.path - -from eoxserver.core import Component, implements -from eoxserver.backends.access import connect -from eoxserver.services.mapserver.interfaces import ConnectorInterface - - -class TileIndexConnector(Component): - """ Connects a tile index with the given layer. The tileitem is fixed to - "location". - """ - - implements(ConnectorInterface) - - def supports(self, data_items): - return ( - len(data_items) == 1 and data_items[0].semantic == "tileindex" - ) - - def connect(self, coverage, data_items, layer, options): - layer.tileindex = os.path.abspath(connect(data_items[0])) - layer.tileitem = "location" - - def disconnect(self, coverage, data_items, layer, options): - pass diff -Nru eoxserver-0.4.0beta2/eoxserver/services/mapserver/interfaces.py eoxserver-0.3.2/eoxserver/services/mapserver/interfaces.py --- eoxserver-0.4.0beta2/eoxserver/services/mapserver/interfaces.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/mapserver/interfaces.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,85 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2011 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -class ConnectorInterface(object): - """ Interface for connectors between `mapscript.layerObj` and associated - data. - """ - - def supports(self, data_items): - """ Returns `True` if the given `data_items` are supported and - `False` if not. - """ - - def connect(self, coverage, data_items, layer, options): - """ Connect a layer (a `mapscript.layerObj`) with the given data - items and coverage (a list of two-tuples: location and semantic). - """ - - def disconnect(self, coverage, data_items, layer, options): - """ Performs all necessary cleanup operations. - """ - - -class LayerFactoryInterface(object): - """ Interface for factories that create `mapscript.layerObj` objects for - coverages. - """ - - @property - def suffixes(self): - """ The suffixes associated with layers this factory produces. This is - used for "specialized" layers such as "bands" or "outlines" layers. - For factories that don't use this feature, it can be left out. - """ - - @property - def requires_connection(self): - """ Return whether or layers generated by this factory require to be - connected via a layer connector. - """ - - def generate(self, eo_object, group_layer, options): - """ Returns an iterable of `mapscript.layerObj` objects preconfigured - for the given EO object. This is easily done via the `yield` - statement. - """ - - def generate_group(self, name): - """ Returns a 'group layer' to be referenced by all other layers - generated by this factory. - """ - - -class StyleApplicatorInterface(object): - """ Interface for style applicators. - """ - - def apply(self, coverage, data_items, layer): - """ Apply all relevant styles. - """ diff -Nru eoxserver-0.4.0beta2/eoxserver/services/mapserver/wcs/base_renderer.py eoxserver-0.3.2/eoxserver/services/mapserver/wcs/base_renderer.py --- eoxserver-0.4.0beta2/eoxserver/services/mapserver/wcs/base_renderer.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/mapserver/wcs/base_renderer.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,207 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -from django.db.models import Q - -from eoxserver.core import Component -from eoxserver.core.config import get_eoxserver_config -from eoxserver.core.decoders import config, typelist -from eoxserver.contrib import mapserver as ms -from eoxserver.resources.coverages import crss -from eoxserver.resources.coverages.models import RectifiedStitchedMosaic -from eoxserver.resources.coverages.formats import getFormatRegistry - - -class WCSConfigReader(config.Reader): - section = "services.ows.wcs" - supported_formats = config.Option(type=typelist(str, ","), default=()) - maxsize = config.Option(type=int, default=None) - - section = "services.ows" - update_sequence = config.Option(default="0") - - -class BaseRenderer(Component): - abstract = True - - def create_map(self): - """ Helper function to create a WCS enabled MapServer mapObj. - """ - map_ = ms.mapObj() - map_.setMetaData("ows_enable_request", "*") - maxsize = WCSConfigReader(get_eoxserver_config()).maxsize - if maxsize is not None: - map_.maxsize = maxsize - map_.setMetaData("ows_updateSequence", - WCSConfigReader(get_eoxserver_config()).update_sequence - ) - return map_ - - def data_items_for_coverage(self, coverage): - """ Helper function to query all relevant data items for any raster data - from the database. - """ - return coverage.data_items.filter( - Q(semantic__startswith="bands") | Q(semantic="tileindex") - ) - - def layer_for_coverage(self, coverage, native_format, version=None): - """ Helper method to generate a WCS enabled MapServer layer for a given - coverage. - """ - range_type = coverage.range_type - bands = list(range_type) - - # create and configure layer - layer = ms.layerObj() - layer.name = coverage.identifier - layer.type = ms.MS_LAYER_RASTER - - layer.setProjection(coverage.spatial_reference.proj) - - extent = coverage.extent - size = coverage.size - resolution = ((extent[2] - extent[0]) / float(size[0]), - (extent[1] - extent[3]) / float(size[1])) - - layer.setExtent(*extent) - - ms.setMetaData(layer, { - "title": coverage.identifier, - "enable_request": "*" - }, namespace="ows") - - ms.setMetaData(layer, { - "label": coverage.identifier, - "extent": "%.10g %.10g %.10g %.10g" % extent, - "resolution": "%.10g %.10g" % resolution, - "size": "%d %d" % size, - "bandcount": str(len(bands)), - "interval": "%f %f" % bands[0].allowed_values, - "significant_figures": "%d" % bands[0].significant_figures, - "rangeset_name": range_type.name, - "rangeset_label": range_type.name, - "imagemode": ms.gdalconst_to_imagemode_string(bands[0].data_type), - "formats": " ".join([ - f.wcs10name if version.startswith("1.0") else f.mimeType - for f in self.get_wcs_formats()] - ) - }, namespace="wcs") - - if version is None or version.startswith("2.0"): - ms.setMetaData(layer, { - "band_names": " ".join([band.name for band in bands]), - }, namespace="wcs") - else: - ms.setMetaData(layer, { - "rangeset_axes": ",".join(band.name for band in bands), - }, namespace="wcs") - - if native_format: - if version.startswith("1.0"): - native_format = next(( - x.wcs10name for x in self.get_wcs_formats() - if x.mimeType == native_format), native_format - ) - ms.setMetaData(layer, { - "native_format": native_format, - "nativeformat": native_format - }, namespace="wcs") - - native_crs = "EPSG:%d" % coverage.spatial_reference.srid - all_crss = crss.getSupportedCRS_WCS(format_function=crss.asShortCode) - if native_crs in all_crss: - all_crss.remove(native_crs) - - # setting the coverages CRS as the first one is important! - all_crss.insert(0, native_crs) - - supported_crss = " ".join(all_crss) - layer.setMetaData("ows_srs", supported_crss) - layer.setMetaData("wcs_srs", supported_crss) - - for band in bands: - ms.setMetaData(layer, { - "band_description": band.description, - "band_definition": band.definition, - "band_uom": band.uom, - }, namespace=band.name) - - # For MS WCS 1.x interface - ms.setMetaData(layer, { - "label": band.name, - "interval": "%d %d" % band.allowed_values - }, namespace="wcs_%s" % band.name) - - nilvalues = " ".join( - str(nil_value.value) for nil_value in bands[0].nil_value_set - ) - nilvalues_reasons = " ".join( - nil_value.reason for nil_value in bands[0].nil_value_set - ) - if nilvalues: - ms.setMetaData(layer, { - "nilvalues": nilvalues, - "nilvalues_reasons": nilvalues_reasons - }, namespace="wcs") - - return layer - - def get_native_format(self, coverage, data_items): - if issubclass(coverage.real_type, RectifiedStitchedMosaic): - # use the default format for RectifiedStitchedMosaics - return getFormatRegistry().getDefaultNativeFormat().wcs10name - - if len(data_items) == 1: - return data_items[0].format - - return None - - def find_param(self, params, name, default=None): - for key, value in params: - if key == name: - return value - return default - - def get_wcs_formats(self): - return getFormatRegistry().getSupportedFormatsWCS() - - def get_all_outputformats(self, use_mime=True): - outputformats = [] - for frmt in self.get_wcs_formats(): - of = ms.outputFormatObj(frmt.driver, "custom") - of.name = frmt.mimeType if use_mime else frmt.wcs10name - of.mimetype = frmt.mimeType - of.extension = frmt.defaultExt - outputformats.append(of) - return outputformats - - -def is_format_supported(mime_type): - reader = WCSConfigReader(get_eoxserver_config()) - return mime_type in reader.supported_formats diff -Nru eoxserver-0.4.0beta2/eoxserver/services/mapserver/wcs/capabilities_renderer.py eoxserver-0.3.2/eoxserver/services/mapserver/wcs/capabilities_renderer.py --- eoxserver-0.4.0beta2/eoxserver/services/mapserver/wcs/capabilities_renderer.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/mapserver/wcs/capabilities_renderer.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,121 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2011 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -from eoxserver.core import Component, implements -from eoxserver.core.config import get_eoxserver_config -from eoxserver.core.util.timetools import isoformat -from eoxserver.contrib.mapserver import create_request, Map, Layer -from eoxserver.resources.coverages import crss -from eoxserver.services.mapserver.wcs.base_renderer import BaseRenderer -from eoxserver.services.ows.common.config import CapabilitiesConfigReader -from eoxserver.services.ows.wcs.interfaces import ( - WCSCapabilitiesRendererInterface -) -from eoxserver.services.ows.version import Version -from eoxserver.services.result import result_set_from_raw_data, get_content_type -from eoxserver.services.urls import get_http_service_url - - -class MapServerWCSCapabilitiesRenderer(BaseRenderer): - """ WCS Capabilities renderer implementation using MapServer. - """ - implements(WCSCapabilitiesRendererInterface) - - versions = (Version(1, 0), Version(1, 1)) - - def supports(self, params): - return params.version in self.versions - - def render(self, params): - conf = CapabilitiesConfigReader(get_eoxserver_config()) - - http_service_url = get_http_service_url(params.http_request) - - map_ = Map() - map_.setMetaData({ - "enable_request": "*", - "onlineresource": http_service_url, - "service_onlineresource": conf.onlineresource, - "updateSequence": conf.update_sequence, - "name": conf.name, - "title": conf.title, - "label": conf.title, - "abstract": conf.abstract, - "accessconstraints": conf.access_constraints, - "addresstype": "", - "address": conf.delivery_point, - "stateorprovince": conf.administrative_area, - "city": conf.city, - "postcode": conf.postal_code, - "country": conf.country, - "contactelectronicmailaddress": conf.electronic_mail_address, - "contactfacsimiletelephone": conf.phone_facsimile, - "contactvoicetelephone": conf.phone_voice, - "contactperson": conf.individual_name, - "contactorganization": conf.provider_name, - "contactposition": conf.position_name, - "role": conf.role, - "hoursofservice": conf.hours_of_service, - "contactinstructions": conf.contact_instructions, - "fees": conf.fees, - "keywordlist": ",".join(conf.keywords), - "formats": " ".join([f.wcs10name for f in self.get_wcs_formats()]), - "srs": " ".join(crss.getSupportedCRS_WCS(format_function=crss.asShortCode)), - }, namespace="ows") - map_.setProjection("EPSG:4326") - - for outputformat in self.get_all_outputformats(False): - map_.appendOutputFormat(outputformat) - - for coverage in params.coverages: - layer = Layer(coverage.identifier) - - layer.setProjection(coverage.spatial_reference.proj) - extent = coverage.extent - size = coverage.size - resolution = ((extent[2] - extent[0]) / float(size[0]), - (extent[1] - extent[3]) / float(size[1])) - - layer.setExtent(*extent) - layer.setMetaData({ - "title": coverage.identifier, - "label": coverage.identifier, - "extent": "%.10g %.10g %.10g %.10g" % extent, - "resolution": "%.10g %.10g" % resolution, - "size": "%d %d" % size, - "formats": " ".join([f.wcs10name for f in self.get_wcs_formats()]), - "srs": " ".join(crss.getSupportedCRS_WCS(format_function=crss.asShortCode)), - }, namespace="wcs") - - map_.insertLayer(layer) - - request = create_request(params) - request.setParameter("version", params.version) - raw_result = map_.dispatch(request) - result = result_set_from_raw_data(raw_result) - return result diff -Nru eoxserver-0.4.0beta2/eoxserver/services/mapserver/wcs/coverage_description_renderer.py eoxserver-0.3.2/eoxserver/services/mapserver/wcs/coverage_description_renderer.py --- eoxserver-0.4.0beta2/eoxserver/services/mapserver/wcs/coverage_description_renderer.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/mapserver/wcs/coverage_description_renderer.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,88 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -from eoxserver.core import implements -from eoxserver.contrib import mapserver as ms -from eoxserver.resources.coverages import models -from eoxserver.services.mapserver.wcs.base_renderer import BaseRenderer -from eoxserver.services.ows.version import Version -from eoxserver.services.exceptions import NoSuchCoverageException -from eoxserver.services.ows.wcs.interfaces import ( - WCSCoverageDescriptionRendererInterface -) -from eoxserver.services.result import result_set_from_raw_data - - -class CoverageDescriptionMapServerRenderer(BaseRenderer): - """ A coverage description renderer implementation using mapserver. - """ - - implements(WCSCoverageDescriptionRendererInterface) - - versions = (Version(1, 1), Version(1, 0)) - handles = ( - models.RectifiedDataset, models.RectifiedStitchedMosaic, - models.ReferenceableDataset - ) - - def supports(self, params): - return ( - params.version in self.versions - and all( - map( - lambda c: issubclass(c.real_type, self.handles), - params.coverages - ) - ) - ) - - def render(self, params): - map_ = self.create_map() - - use_name = (params.version == Version(1, 0)) - - for coverage in params.coverages: - - # ReferenceableDatasets are not supported in WCS < 2.0 - if issubclass(coverage.real_type, models.ReferenceableDataset): - raise NoSuchCoverageException((coverage.identifier,)) - - data_items = self.data_items_for_coverage(coverage) - native_format = self.get_native_format(coverage, data_items) - layer = self.layer_for_coverage( - coverage, native_format, params.version - ) - map_.insertLayer(layer) - - for outputformat in self.get_all_outputformats(not use_name): - map_.appendOutputFormat(outputformat) - - request = ms.create_request(params) - raw_result = ms.dispatch(map_, request) - result = result_set_from_raw_data(raw_result) - return result diff -Nru eoxserver-0.4.0beta2/eoxserver/services/mapserver/wcs/coverage_renderer.py eoxserver-0.3.2/eoxserver/services/mapserver/wcs/coverage_renderer.py --- eoxserver-0.4.0beta2/eoxserver/services/mapserver/wcs/coverage_renderer.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/mapserver/wcs/coverage_renderer.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,364 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -from datetime import datetime -from urllib import unquote -import logging - -from lxml import etree - -from eoxserver.core import implements, ExtensionPoint -from eoxserver.contrib import mapserver as ms -from eoxserver.resources.coverages import models, crss -from eoxserver.resources.coverages.formats import getFormatRegistry -from eoxserver.services.exceptions import NoSuchCoverageException -from eoxserver.services.ows.wcs.interfaces import WCSCoverageRendererInterface -from eoxserver.services.ows.wcs.v20.encoders import WCS20EOXMLEncoder -from eoxserver.services.ows.wcs.v20.util import ( - ScaleSize, ScaleExtent, ScaleAxis -) -from eoxserver.services.mapserver.interfaces import ( - ConnectorInterface, LayerFactoryInterface -) -from eoxserver.services.mapserver.wcs.base_renderer import ( - BaseRenderer, is_format_supported -) -from eoxserver.services.ows.version import Version -from eoxserver.services.result import result_set_from_raw_data, ResultBuffer -from eoxserver.services.exceptions import ( - RenderException, OperationNotSupportedException, - InterpolationMethodNotSupportedException, InvalidOutputCrsException -) - - -logger = logging.getLogger(__name__) - -INTERPOLATION_TRANS = { - "nearest-neighbour": "NEAREST", - "linear": "BILINEAR", - "bilinear": "BILINEAR", - "cubic": "BICUBIC", - "average": "AVERAGE" -} - - -class RectifiedCoverageMapServerRenderer(BaseRenderer): - """ A coverage renderer for rectified coverages. Uses mapserver to process - the request. - """ - - implements(WCSCoverageRendererInterface) - - # ReferenceableDatasets are not handled in WCS >= 2.0 - versions_full = (Version(1, 1), Version(1, 0)) - versions_partly = (Version(2, 0),) - versions = versions_full + versions_partly - - handles_full = ( - models.RectifiedDataset, - models.RectifiedStitchedMosaic, - models.ReferenceableDataset - ) - - handles_partly = (models.RectifiedDataset, models.RectifiedStitchedMosaic) - handles = handles_full + handles_partly - - connectors = ExtensionPoint(ConnectorInterface) - layer_factories = ExtensionPoint(LayerFactoryInterface) - - def supports(self, params): - return ( - (params.version in self.versions_full - and issubclass(params.coverage.real_type, self.handles_full)) - or - (params.version in self.versions_partly - and issubclass(params.coverage.real_type, self.handles_partly)) - ) - - def render(self, params): - # get coverage related stuff - coverage = params.coverage - - # ReferenceableDataset are not supported in WCS < 2.0 - if issubclass(coverage.real_type, models.ReferenceableDataset): - raise NoSuchCoverageException((coverage.identifier,)) - - data_items = self.data_items_for_coverage(coverage) - - range_type = coverage.range_type - bands = list(range_type) - - subsets = params.subsets - - if subsets: - subsets.srid # this automatically checks the validity - - # create and configure map object - map_ = self.create_map() - - # configure outputformat - native_format = self.get_native_format(coverage, data_items) - if get_format_by_mime(native_format) is None: - native_format = "image/tiff" - - frmt = params.format or native_format - - if frmt is None: - raise RenderException("Format could not be determined", "format") - - mime_type, frmt = split_format(frmt) - - imagemode = ms.gdalconst_to_imagemode(bands[0].data_type) - time_stamp = datetime.now().strftime("%Y%m%d%H%M%S") - basename = "%s_%s" % (coverage.identifier, time_stamp) - of = create_outputformat( - mime_type, frmt, imagemode, basename, - getattr(params, "encoding_params", {}) - ) - - map_.appendOutputFormat(of) - map_.setOutputFormat(of) - - # TODO: use layer factory here - layer = self.layer_for_coverage(coverage, native_format, params.version) - - map_.insertLayer(layer) - - for connector in self.connectors: - if connector.supports(data_items): - break - else: - raise OperationNotSupportedException( - "Could not find applicable layer connector.", "coverage" - ) - - try: - connector.connect(coverage, data_items, layer, {}) - # create request object and dispatch it against the map - request = ms.create_request( - self.translate_params(params, range_type) - ) - request.setParameter("format", mime_type) - raw_result = ms.dispatch(map_, request) - - finally: - # perform any required layer related cleanup - connector.disconnect(coverage, data_items, layer, {}) - - result_set = result_set_from_raw_data(raw_result) - - if params.version == Version(2, 0): - if getattr(params, "mediatype", None) in ("multipart/mixed", "multipart/related"): - encoder = WCS20EOXMLEncoder() - is_mosaic = issubclass( - coverage.real_type, models.RectifiedStitchedMosaic - ) - - if not is_mosaic: - tree = encoder.alter_rectified_dataset( - coverage, getattr(params, "http_request", None), - etree.parse(result_set[0].data_file).getroot(), - subsets.bounding_polygon(coverage) if subsets else None - ) - else: - tree = encoder.alter_rectified_stitched_mosaic( - coverage.cast(), getattr(params, "http_request", None), - etree.parse(result_set[0].data_file).getroot(), - subsets.bounding_polygon(coverage) if subsets else None - ) - - result_set[0] = ResultBuffer( - encoder.serialize(tree), - encoder.content_type - ) - - # "default" response - return result_set - - def translate_params(self, params, range_type): - """ "Translate" parameters to be understandable by mapserver. - """ - if params.version.startswith("2.0"): - for key, value in params: - if key == "interpolation": - interpolation = INTERPOLATION_TRANS.get(value) - if not interpolation: - raise InterpolationMethodNotSupportedException( - "Interpolation method '%s' is not supported." - % value - ) - yield key, value - - else: - yield key, value - - rangesubset = params.rangesubset - if rangesubset: - yield "rangesubset", ",".join( - map(str, rangesubset.get_band_indices(range_type, 1)) - ) - - # TODO: this only works in newer MapServer implementations - # (since 6.4?). - SCALE_AVAILABLE = ms.msGetVersionInt() > 60401 - scalefactor = params.scalefactor - if scalefactor is not None: - if SCALE_AVAILABLE: - yield "scalefactor", str(scalefactor) - else: - raise RenderException( - "'ScaleFactor' is not supported by MapServer in the " - "current version.", "scalefactor" - ) - - for scale in params.scales: - scaleaxes = [] - if isinstance(scale, ScaleSize): - yield "size", "%s(%d)" % (scale.axis, scale.size) - elif isinstance(scale, ScaleExtent): - yield "size", "%s(%d)" % (scale.axis, scale.high-scale.low) - elif isinstance(scale, ScaleAxis): - if SCALE_AVAILABLE: - scaleaxes.append(scale) - else: - raise RenderException( - "'ScaleAxes' is not supported by MapServer in the " - "current version.", "scaleaxes" - ) - - if scaleaxes: - yield "scaleaxes", ",".join( - "%s(%f)" % (scale.axis, scale.value) - for scale in scaleaxes - ) - - if params.outputcrs is not None: - srid = crss.parseEPSGCode(params.outputcrs, - (crss.fromURL, crss.fromURN, crss.fromShortCode) - ) - if srid is None: - raise InvalidOutputCrsException( - "Failed to extract an EPSG code from the OutputCRS URI " - "'%s'." % params.outputcrs - ) - yield "outputcrs", params.outputcrs - - else: - for key, value in params: - yield key, value - - -def split_format(frmt): - parts = unquote(frmt).split(";") - mime_type = parts[0] - options = map( - lambda kv: map(lambda i: i.strip(), kv.split("=")), parts[1:] - ) - return mime_type, options - - -def create_outputformat(mime_type, options, imagemode, basename, parameters): - """ Returns a ``mapscript.outputFormatObj`` for the given format name and - imagemode. - """ - - reg_format = get_format_by_mime(mime_type) - - if not reg_format: - raise RenderException( - "Unsupported output format '%s'." % mime_type, "format" - ) - - outputformat = ms.outputFormatObj(reg_format.driver, "custom") - outputformat.name = reg_format.wcs10name - outputformat.mimetype = reg_format.mimeType - outputformat.extension = reg_format.defaultExt - outputformat.imagemode = imagemode - - #for key, value in options: - # outputformat.setOption(str(key), str(value)) - - if mime_type == "image/tiff": - _apply_gtiff(outputformat, **parameters) - - - filename = basename + reg_format.defaultExt - outputformat.setOption("FILENAME", str(filename)) - - return outputformat - - -def _apply_gtiff(outputformat, compression=None, jpeg_quality=None, - predictor=None, interleave=None, tiling=False, - tilewidth=None, tileheight=None): - - logger.info("Applying GeoTIFF parameters.") - - if compression: - if compression.lower() == "huffman": - compression = "CCITTRLE" - outputformat.setOption("COMPRESS", str(compression.upper())) - - if jpeg_quality is not None: - outputformat.setOption("JPEG_QUALITY", str(jpeg_quality)) - - if predictor: - pr = ["NONE", "HORIZONTAL", "FLOATINGPOINT"].index(predictor.upper()) - if pr == -1: - raise ValueError("Invalid compression predictor '%s'." % predictor) - outputformat.setOption("PREDICTOR", str(pr + 1)) - - if interleave: - outputformat.setOption("INTERLEAVE", str(interleave.upper())) - - if tiling: - outputformat.setOption("TILED", "YES") - if tilewidth is not None: - outputformat.setOption("BLOCKXSIZE", str(tilewidth)) - if tileheight is not None: - outputformat.setOption("BLOCKYSIZE", str(tileheight)) - - -def get_format_by_mime(mime_type): - """ Convenience function to return an enabled format descriptior for the - given mime type or WCS 1.0 format name. Returns ``None``, if none - applies. - """ - - registry = getFormatRegistry() - reg_format = registry.getFormatByMIME(mime_type) - - if not reg_format: - wcs10_frmts = registry.getFormatsByWCS10Name(mime_type) - if wcs10_frmts: - reg_format = wcs10_frmts[0] - - if reg_format and not is_format_supported(reg_format.mimeType): - return None - - return reg_format diff -Nru eoxserver-0.4.0beta2/eoxserver/services/mapserver/wms/capabilities_renderer.py eoxserver-0.3.2/eoxserver/services/mapserver/wms/capabilities_renderer.py --- eoxserver-0.4.0beta2/eoxserver/services/mapserver/wms/capabilities_renderer.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/mapserver/wms/capabilities_renderer.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,207 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2011 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -from itertools import chain - -from eoxserver.core import Component, implements, ExtensionPoint -from eoxserver.core.config import get_eoxserver_config -from eoxserver.core.util.timetools import isoformat -from eoxserver.contrib.mapserver import create_request, Map, Layer, Class, Style -from eoxserver.resources.coverages import crss, models -from eoxserver.resources.coverages.formats import getFormatRegistry -from eoxserver.services.ows.common.config import CapabilitiesConfigReader -from eoxserver.services.ows.wms.interfaces import ( - WMSCapabilitiesRendererInterface -) -from eoxserver.services.mapserver.interfaces import LayerFactoryInterface -from eoxserver.services.result import result_set_from_raw_data, get_content_type -from eoxserver.services.urls import get_http_service_url - - -class MapServerWMSCapabilitiesRenderer(Component): - """ WMS Capabilities renderer implementation using MapServer. - """ - implements(WMSCapabilitiesRendererInterface) - - layer_factories = ExtensionPoint(LayerFactoryInterface) - - @property - def suffixes(self): - return list( - chain(*[factory.suffixes for factory in self.layer_factories]) - ) - - def render(self, collections, coverages, request_values, request): - conf = CapabilitiesConfigReader(get_eoxserver_config()) - - suffixes = self.suffixes - - http_service_url = get_http_service_url(request) - - map_ = Map() - map_.setMetaData({ - "enable_request": "*", - "onlineresource": http_service_url, - "service_onlineresource": conf.onlineresource, - "updateSequence": conf.update_sequence, - "name": conf.name, - "title": conf.title, - "abstract": conf.abstract, - "accessconstraints": conf.access_constraints, - "addresstype": "postal", - "address": conf.delivery_point, - "stateorprovince": conf.administrative_area, - "city": conf.city, - "postcode": conf.postal_code, - "country": conf.country, - "contactelectronicmailaddress": conf.electronic_mail_address, - "contactfacsimiletelephone": conf.phone_facsimile, - "contactvoicetelephone": conf.phone_voice, - "contactperson": conf.individual_name, - "contactorganization": conf.provider_name, - "contactposition": conf.position_name, - "fees": conf.fees, - "keywordlist": ",".join(conf.keywords), - "srs": " ".join(crss.getSupportedCRS_WCS(format_function=crss.asShortCode)), - }, namespace="ows") - map_.setProjection("EPSG:4326") - map_.setMetaData({ - "getmap_formatlist": ",".join([f.mimeType for f in self.get_wms_formats()]), - "getfeatureinfo_formatlist": "text/html,application/vnd.ogc.gml,text/plain", - }, namespace="wms") - - map_extent = None - - for collection in collections: - group_name = None - - # calculate extent and timextent for every collection - extent = collection.extent_wgs84 - # save overall map extent - map_extent = self.join_extents(map_extent, extent) - - eo_objects = collection.eo_objects.filter( - begin_time__isnull=False, end_time__isnull=False - ) - timeextent = ",".join( - map( - lambda o: ( - "/".join( - map(isoformat, o.time_extent) - ) + "/PT1S" - ), eo_objects - ) - ) - - if len(suffixes) > 1: - # create group layer, if there is more than one suffix for this - # collection - group_name = collection.identifier + "_group" - group_layer = Layer(group_name) - group_layer.setMetaData({ - "title": group_name, - "abstract": group_name, - "extent": " ".join(map(str, extent)), - }, namespace="wms") - - minx, miny, maxx, maxy = extent - group_layer.setExtent(minx, miny, maxx, maxy) - - # add default style - default_class = Class("default") - default_style= Style("default") - default_class.insertStyle(default_style) - group_layer.insertClass(default_class) - - map_.insertLayer(group_layer) - - for suffix in suffixes: - layer_name = collection.identifier + (suffix or "") - layer = Layer(layer_name) - if group_name: - layer.setMetaData({ - "layer_group": "/" + group_name - }, namespace="wms") - - layer.setMetaData({ - "title": layer_name, - "abstract": layer_name, - "extent": " ".join(map(str, extent)), - "timeextent": timeextent, - }, namespace="wms") - map_.insertLayer(layer) - - for coverage in coverages: - extent = coverage.extent_wgs84 - minx, miny, maxx, maxy = extent - # save overall map extent - map_extent = self.join_extents(map_extent, extent) - - layer_name = coverage.identifier - layer = Layer(layer_name) - layer.setMetaData({ - "title": layer_name, - "abstract": layer_name, - "extent": " ".join(map(str, extent)), - }, namespace="wms") - minx, miny, maxx, maxy = extent - layer.setExtent(minx, miny, maxx, maxy) - - map_.insertLayer(layer) - - # set the map_extent to a reasonable default value - # in case there is no coverage or collection - if map_extent is None : - map_extent = ( 0.0, 0.0, 1.0, 1.0 ) - - map_minx, map_miny, map_maxx, map_maxy = map_extent - map_.setExtent(map_minx, map_miny, map_maxx, map_maxy) - - request = create_request(request_values) - raw_result = map_.dispatch(request) - result = result_set_from_raw_data(raw_result) - return result, get_content_type(result) - - def get_wms_formats(self): - return getFormatRegistry().getSupportedFormatsWMS() - - def join_extents(self, e1=None, e2=None): - if e1 and e2: - e1_minx, e1_miny, e1_maxx, e1_maxy = e1 - e2_minx, e2_miny, e2_maxx, e2_maxy = e2 - return ( - min(e1_minx, e2_minx), - min(e1_miny, e2_miny), - max(e1_maxx, e2_maxx), - max(e1_maxy, e2_maxy) - ) - elif e1: - return e1 - elif e2: - return e2 - else: - return None diff -Nru eoxserver-0.4.0beta2/eoxserver/services/mapserver/wms/feature_info_renderer.py eoxserver-0.3.2/eoxserver/services/mapserver/wms/feature_info_renderer.py --- eoxserver-0.4.0beta2/eoxserver/services/mapserver/wms/feature_info_renderer.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/mapserver/wms/feature_info_renderer.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,121 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2011 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -from eoxserver.core import implements -from eoxserver.core.config import get_eoxserver_config -from eoxserver.core.decoders import xml -from eoxserver.contrib import mapserver as ms -from eoxserver.resources.coverages import models -from eoxserver.services.ows.common.config import CapabilitiesConfigReader -from eoxserver.services.mapserver.wms.util import MapServerWMSBaseComponent -from eoxserver.services.ows.wms.interfaces import ( - WMSFeatureInfoRendererInterface -) -from eoxserver.services.ows.wcs.v20.encoders import WCS20EOXMLEncoder -from eoxserver.services.result import ( - result_set_from_raw_data, get_content_type, ResultBuffer -) -from eoxserver.services.urls import get_http_service_url - -class MapServerWMSFeatureInfoRenderer(MapServerWMSBaseComponent): - """ A WMS feature info renderer using MapServer. - """ - implements(WMSFeatureInfoRendererInterface) - - - def render(self, layer_groups, request_values, request, **options): - config = CapabilitiesConfigReader(get_eoxserver_config()) - http_service_url = get_http_service_url(request) - map_ = ms.Map() - map_.setMetaData({ - "enable_request": "*", - "onlineresource": http_service_url, - }, namespace="ows") - - map_.setMetaData("wms_getfeatureinfo_formatlist", "text/html") - map_.setProjection("EPSG:4326") - - session = self.setup_map(layer_groups, map_, options) - - # check if the required format is EO O&M - frmt = pop_param(request_values, "info_format") - use_eoom = False - if frmt in ("application/xml", "text/xml"): - request_values.append(("info_format", "application/vnd.ogc.gml")) - use_eoom = True - else: - request_values.append(("info_format", frmt)) - - with session: - request = ms.create_request(request_values) - raw_result = map_.dispatch(request) - result = result_set_from_raw_data(raw_result) - - if not use_eoom: - # just return the response - return result, get_content_type(result) - else: - # do a postprocessing step and get all identifiers in order - # to encode them with EO O&M - decoder = GMLFeatureDecoder(result[0].data_file.read()) - identifiers = decoder.identifiers - coverages = models.Coverage.objects.filter( - identifier__in=identifiers - ) - - # sort the result with the returned order of coverages - lookup_table = dict((c.identifier, c) for c in coverages) - coverages = [ - lookup_table[identifier] for identifier in identifiers - ] - - # encode the coverages with the EO O&M - encoder = WCS20EOXMLEncoder() - - return [ - ResultBuffer( - encoder.serialize( - encoder.encode_coverage_descriptions(coverages) - ), encoder.content_type - ) - ], encoder.content_type - - - -def pop_param(request_values, name, default=None): - """ Helper to pop one param from a key-value list - """ - for param_name, value in request_values: - if param_name.lower() == name: - request_values.remove((param_name, value)) - return value - return default - - -class GMLFeatureDecoder(xml.Decoder): - identifiers = xml.Parameter("//identifier/text()", num="*") diff -Nru eoxserver-0.4.0beta2/eoxserver/services/mapserver/wms/layerfactories/base.py eoxserver-0.3.2/eoxserver/services/mapserver/wms/layerfactories/base.py --- eoxserver-0.4.0beta2/eoxserver/services/mapserver/wms/layerfactories/base.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/mapserver/wms/layerfactories/base.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,256 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# Martin Paces -# -#------------------------------------------------------------------------------- -# Copyright (C) 2011-2014 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -import os.path - -from django.conf import settings - -from eoxserver.core import Component, implements -from eoxserver.contrib import mapserver as ms -from eoxserver.resources.coverages import models, crss -from eoxserver.services.mapserver.interfaces import LayerFactoryInterface -from eoxserver.services import models as service_models - -from eoxserver.resources.coverages.dateline import ( - extent_crosses_dateline, wrap_extent_around_dateline -) - -#------------------------------------------------------------------------------- - -class BaseStyleMixIn(object): - STYLES = ( - ("red", 255, 0, 0), - ("green", 0, 128, 0), - ("blue", 0, 0, 255), - ("white", 255, 255, 255), - ("black", 0, 0, 0), - ("yellow", 255, 255, 0), - ("orange", 255, 165, 0), - ("magenta", 255, 0, 255), - ("cyan", 0, 255, 255), - ("brown", 165, 42, 42) - ) - - DEFAULT_STYLE = "red" - - - def apply_styles(self, layer, fill=False): - # add style info - for name, r, g, b in self.STYLES: - cls = ms.classObj() - style = ms.styleObj() - style.outlinecolor = ms.colorObj(r, g, b) - if fill: - style.color = ms.colorObj(r, g, b) - cls.insertStyle(style) - cls.group = name - - layer.insertClass(cls) - - layer.classgroup = self.DEFAULT_STYLE - - -class OffsiteColorMixIn(object): - def offsite_color_from_range_type(self, range_type, band_indices=None): - """ Helper function to create an offsite color for a given range type - and optionally band indices. - """ - - if band_indices == None: - if len(range_type) == 1: - band_indices = [0, 0, 0] - elif len(range_type) >=3: - band_indices = [0, 1, 2] - else: - # no offsite color possible - return None - - if len(band_indices) != 3: - raise ValueError( - "Wrong number of band indices to calculate offsite color." - ) - - values = [] - for index in band_indices: - band = range_type[index] - nil_value_set = band.nil_value_set - if nil_value_set and len(nil_value_set) > 0: - values.append(nil_value_set[0].value) - else: - return None - - return ms.colorObj(*values) - - -class PolygonLayerMixIn(object): - def _create_polygon_layer(self, name): - layer = ms.layerObj() - layer.name = name - layer.type = ms.MS_LAYER_POLYGON - - self.apply_styles(layer) - - srid = 4326 - layer.setProjection(crss.asProj4Str(srid)) - layer.setMetaData("ows_srs", crss.asShortCode(srid)) - layer.setMetaData("wms_srs", crss.asShortCode(srid)) - - layer.dump = True - - layer.header = os.path.join(settings.PROJECT_DIR, "conf", "outline_template_header.html") - layer.template = os.path.join(settings.PROJECT_DIR, "conf", "outline_template_dataset.html") - layer.footer = os.path.join(settings.PROJECT_DIR, "conf", "outline_template_footer.html") - - layer.setMetaData("gml_include_items", "all") - layer.setMetaData("wms_include_items", "all") - - layer.addProcessing("ITEMS=identifier") - - layer.offsite = ms.colorObj(0, 0, 0) - - return layer - - -class PlainLayerMixIn(object): - - def _create_layer(self, coverage, name, extent=None, group=None, wrapped=False): - layer = ms.layerObj() - layer.name = name - layer.type = ms.MS_LAYER_RASTER - if extent: - layer.setMetaData("wms_extent", "%f %f %f %f" % extent) - layer.setExtent(*extent) - - #layer.setMetaData( - # "wms_enable_request", "getcapabilities getmap getfeatureinfo" - #) - - if wrapped: - # set the info for the connector to wrap this layer around the dateline - layer.setMetaData("eoxs_wrap_dateline", "true") - - self._set_projection(layer, coverage.spatial_reference) - if group: - layer.group = group - - return layer - - def get_render_options(self, coverage): - try: - return service_models.WMSRenderOptions.objects.get( - coverage=coverage - ) - except service_models.WMSRenderOptions.DoesNotExist: - return None - - def set_render_options(self, layer, offsite=None, options=None): - if offsite: - layer.offsite = offsite - if options: - red = options.default_red - green = options.default_green - blue = options.default_blue - alpha = options.default_alpha - - if red is not None and green is not None and blue is not None: - bands = "%d,%d,%d" % (red, green, blue) - if alpha is not None: - bands += alpha - layer.setProcessingKey("BANDS", bands) - - if options.scale_auto: - layer.setProcessingKey("SCALE", "AUTO") - elif options.scale_min is not None and options.scale_max is not None: - layer.setProcessingKey( - "SCALE", "%d,%d" % (options.scale_min, options.scale_max) - ) - - if options.resampling: - layer.setProcessingKey("RESAMPLE", str(options.resampling)) - - def _set_projection(self, layer, sr): - short_epsg = "EPSG:%d" % sr.srid - layer.setProjection(sr.proj) - layer.setMetaData("ows_srs", short_epsg) - layer.setMetaData("wms_srs", short_epsg) - - -class AbstractLayerFactory(PlainLayerMixIn, Component): - implements(LayerFactoryInterface) - abstract = True - - def generate(self, eo_object, group_layer, suffix, options): - raise NotImplementedError - - -class BaseCoverageLayerFactory(OffsiteColorMixIn, PlainLayerMixIn, Component): - implements(LayerFactoryInterface) - # NOTE: Technically, this class is not abstract but we need it labeled - # as an abstract class to allow inheritance while avoiding multiple - # component's imports. - abstract = True - - def generate(self, eo_object, group_layer, suffix, options): - coverage = eo_object.cast() - extent = coverage.extent - srid = coverage.srid - - data_items = coverage.data_items.all() - range_type = coverage.range_type - - offsite = self.offsite_color_from_range_type(range_type) - options = self.get_render_options(coverage) - - if extent_crosses_dateline(extent, srid): - identifier = coverage.identifier - wrapped_extent = wrap_extent_around_dateline(extent, srid) - layer = self._create_layer( - coverage, identifier + "_unwrapped", extent, identifier - ) - - self.set_render_options(layer, offsite, options) - self.set_render_options(layer, offsite, options) - yield layer, data_items - wrapped_layer = self._create_layer( - coverage, identifier + "_wrapped", wrapped_extent, identifier, True - ) - self.set_render_options(wrapped_layer, offsite, options) - yield wrapped_layer, data_items - else: - layer = self._create_layer( - coverage, coverage.identifier, extent - ) - self.set_render_options(layer, offsite, options) - yield layer, data_items - - - def generate_group(self, name): - return ms.Layer(name) - - diff -Nru eoxserver-0.4.0beta2/eoxserver/services/mapserver/wms/layerfactories/colorized_mask_layer_factory.py eoxserver-0.3.2/eoxserver/services/mapserver/wms/layerfactories/colorized_mask_layer_factory.py --- eoxserver-0.4.0beta2/eoxserver/services/mapserver/wms/layerfactories/colorized_mask_layer_factory.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/mapserver/wms/layerfactories/colorized_mask_layer_factory.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,115 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -import logging - -from eoxserver.core.config import get_eoxserver_config -from eoxserver.core.decoders import config, typelist -from eoxserver.contrib import mapserver as ms -from eoxserver.services.mapserver.wms.layerfactories.base import ( - BaseStyleMixIn, AbstractLayerFactory -) - - -logger = logging.getLogger(__name__) - - -class ColorizedMaskLayerFactory(BaseStyleMixIn, AbstractLayerFactory): - - @property - def suffixes(self): - return ["_%s" % mask for mask in self.enabled_masks] - - @property - def enabled_masks(self): - decoder = EnabledMasksConfigReader(get_eoxserver_config()) - return decoder.mask_names - - def generate(self, eo_object, group_layer, suffix, options): - mask_name = suffix[1:] - coverage = eo_object.cast() - layer_name = "%s%s" % (coverage.identifier, suffix) - - if mask_name not in self.enabled_masks: - return - - mask_items = coverage.data_items.filter( - semantic="polygonmask[%s]" % mask_name - ) - - # externaly enforced group or multiple groupped masks - if group_layer or len(mask_items) > 1 : - - # check if the external group provided - if not create a new one - if not group_layer : - group_layer = self.generate_group(coverage.identifier + suffix) - yield group_layer, () - - # append to a comma separated list - _append = lambda l,v: "%s,%s"%(l,v) if l else v - - # handle the groupped layers - for i, mask_item in enumerate(mask_items): - layer = self.create_polygon_layer( - coverage, "%s_%d" % (layer_name, i) - ) - group_layer.connection = _append( - group_layer.connection, layer.name - ) - - yield layer, (mask_item,) - - # only one mask used directly as a layer - elif len(mask_items) == 1 : - layer = self.create_polygon_layer(coverage, layer_name) - self.apply_styles(layer, fill=True) - yield layer, (mask_items[0],) - - - def generate_group(self, name): - layer = ms.layerObj() - layer.name = name - layer.type = ms.MS_LAYER_POLYGON - layer.connectiontype = ms.MS_UNION - layer.connection = "" - self.apply_styles(layer, fill=True) - return layer - - - def create_polygon_layer(self, coverage, name): - layer = self._create_layer(coverage, name, coverage.extent) - self._set_projection(layer, coverage.spatial_reference) - layer.type = ms.MS_LAYER_POLYGON - layer.dump = True - layer.offsite = ms.colorObj(0, 0, 0) - return layer - - -class EnabledMasksConfigReader(config.Reader): - section = "services.ows.wms" - mask_names = config.Option(type=typelist(str, ","), default=[]) diff -Nru eoxserver-0.4.0beta2/eoxserver/services/mapserver/wms/layerfactories/coverage_bands_layer_factory.py eoxserver-0.3.2/eoxserver/services/mapserver/wms/layerfactories/coverage_bands_layer_factory.py --- eoxserver-0.4.0beta2/eoxserver/services/mapserver/wms/layerfactories/coverage_bands_layer_factory.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/mapserver/wms/layerfactories/coverage_bands_layer_factory.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,100 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2011 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -import os.path - -from django.conf import settings - -from eoxserver.core import Component, implements -from eoxserver.contrib.mapserver import ( - Layer, MS_LAYER_POLYGON, shapeObj, classObj, styleObj, colorObj -) -from eoxserver.resources.coverages import models -from eoxserver.services.mapserver.interfaces import LayerFactoryInterface -from eoxserver.services.mapserver.wms.layerfactories.base import ( - AbstractLayerFactory, OffsiteColorMixIn -) - - -class CoverageBandsLayerFactory(OffsiteColorMixIn, AbstractLayerFactory): - handles = (models.RectifiedDataset, models.RectifiedStitchedMosaic,) - # TODO: ReferenceableDatasets - suffixes = ("_bands",) - requires_connection = True - - def generate(self, eo_object, group_layer, suffix, options): - name = eo_object.identifier + "_bands" - layer = Layer(name) - layer.setMetaData("ows_title", name) - layer.setMetaData("wms_label", name) - layer.addProcessing("CLOSE_CONNECTION=CLOSE") - - coverage = eo_object.cast() - range_type = coverage.range_type - - req_bands = options["bands"] - band_indices = [] - bands = [] - - for req_band in req_bands: - if isinstance(req_band, int): - band_indices.append(req_band + 1) - - bands.append(range_type[req_band]) - else: - for i, band in enumerate(range_type): - if band.name == req_band: - band_indices.append(i + 1) - bands.append(band) - break - else: - raise Exception( - "Coverage '%s' does not have a band with name '%s'." - ) - - if len(req_bands) in (3, 4): - indices_str = ",".join(map(str, band_indices)) - offsite_indices = map(lambda v: v-1, band_indices[:3]) - elif len(req_bands) == 1: - indices_str = ",".join(map(str, band_indices * 3)) - v = band_indices[0] - 1 - offsite_indices = [v, v, v] - else: - raise Exception("Invalid number of bands requested.") - - offsite = self.offsite_color_from_range_type( - range_type, offsite_indices - ) - options = self.get_render_options(coverage) - self.set_render_options(layer, offsite, options) - - layer.setProcessingKey("BANDS", indices_str) - yield (layer, coverage.data_items.all()) - - def generate_group(self, name): - return Layer(name) diff -Nru eoxserver-0.4.0beta2/eoxserver/services/mapserver/wms/layerfactories/coverage_layer_factory.py eoxserver-0.3.2/eoxserver/services/mapserver/wms/layerfactories/coverage_layer_factory.py --- eoxserver-0.4.0beta2/eoxserver/services/mapserver/wms/layerfactories/coverage_layer_factory.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/mapserver/wms/layerfactories/coverage_layer_factory.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,38 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# Martin Paces -# -#------------------------------------------------------------------------------- -# Copyright (C) 2011-2014 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -from eoxserver.resources.coverages import models -from eoxserver.services.mapserver.wms.layerfactories.base import ( - BaseCoverageLayerFactory -) - -class CoverageLayerFactory(BaseCoverageLayerFactory): - handles = (models.RectifiedDataset, models.RectifiedStitchedMosaic, - models.ReferenceableDataset) - suffixes = (None,) - requires_connection = True diff -Nru eoxserver-0.4.0beta2/eoxserver/services/mapserver/wms/layerfactories/coverage_masked_outlines_layer_factory.py eoxserver-0.3.2/eoxserver/services/mapserver/wms/layerfactories/coverage_masked_outlines_layer_factory.py --- eoxserver-0.4.0beta2/eoxserver/services/mapserver/wms/layerfactories/coverage_masked_outlines_layer_factory.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/mapserver/wms/layerfactories/coverage_masked_outlines_layer_factory.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,74 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2011 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -from eoxserver.core import Component, implements -from eoxserver.contrib import mapserver as ms -from eoxserver.resources.coverages import models -from eoxserver.services.mapserver.interfaces import LayerFactoryInterface -from eoxserver.services.mapserver.wms.layerfactories.base import ( - AbstractLayerFactory, BaseStyleMixIn, PolygonLayerMixIn -) - - -class CoverageMaskedOutlinesLayerFactory(BaseStyleMixIn, PolygonLayerMixIn, AbstractLayerFactory): - handles = (models.RectifiedDataset, models.ReferenceableDataset, - models.RectifiedStitchedMosaic,) - suffixes = ("_masked_outlines",) - requires_connection = False - - - def generate(self, eo_object, group_layer, suffix, options): - # don't generate any layers, but add the footprint as feature to the - # group layer - - if group_layer: - layer = group_layer - else: - layer = self._create_polygon_layer( - eo_object.identifier + "_masked_outlines" - ) - - coverage = eo_object.cast() - layer.setMetaData("eoxs_geometry_reversed", "true") - - mask_items = coverage.data_items.filter( - semantic__startswith="polygonmask" - ) - - # use the whole footprint if no mask is given - if not len(mask_items): - shape = ms.shapeObj.fromWKT(eo_object.footprint.wkt) - shape.initValues(1) - shape.setValue(0, eo_object.identifier) - layer.addFeature(shape) - - yield layer, mask_items - - - def generate_group(self, name): - return self._create_polygon_layer(name) diff -Nru eoxserver-0.4.0beta2/eoxserver/services/mapserver/wms/layerfactories/coverage_mask_layer_factory.py eoxserver-0.3.2/eoxserver/services/mapserver/wms/layerfactories/coverage_mask_layer_factory.py --- eoxserver-0.4.0beta2/eoxserver/services/mapserver/wms/layerfactories/coverage_mask_layer_factory.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/mapserver/wms/layerfactories/coverage_mask_layer_factory.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,126 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# Martin Paces -# -#------------------------------------------------------------------------------- -# Copyright (C) 2011-2014 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -from eoxserver.contrib import mapserver as ms -from eoxserver.resources.coverages import models - -from eoxserver.services.mapserver.wms.layerfactories.base import ( - BaseCoverageLayerFactory -) - -class CoverageMaskedLayerFactory(BaseCoverageLayerFactory): - handles = (models.RectifiedDataset, models.RectifiedStitchedMosaic) - suffixes = ("_masked",) - requires_connection = True - - def generate(self, eo_object, group_layer, suffix, options): - name = eo_object.identifier + suffix - mask_layer_name = eo_object.identifier + "__mask__" - - #--------------------------------------------------------------------- - # handle the mask layers - - # get the applicable sematics - mask_semantics = ("polygonmask",) - mask_items = eo_object.data_items.all() - for mask_semantic in mask_semantics: - mask_items = mask_items.filter(semantic__startswith=mask_semantic) - - # layer creating closure - def _create_mask_polygon_layer(name): - mask_layer = ms.layerObj() - mask_layer.name = name - mask_layer.type = ms.MS_LAYER_POLYGON - - mask_layer.setMetaData("eoxs_geometry_reversed", "true") - - cls = ms.classObj(mask_layer) - style = ms.styleObj(cls) - style.color.setRGB(0, 0, 0) - return mask_layer - - # multiple masks shall be grouped by a group layer - if len(mask_items) > 1: - - # more than one mask, requires a mask group layer - mask_layer = Layer(mask_layer_name) - - yield (mask_group_layer, ()) - - # generate mask layers - for i, mask_item in enumerate(mask_items): - - mask_sublayer = _create_mask_polygon_layer( - "%s%2.2d"%(mask_layer_name,i) - ) - mask_sublayer.group = mask_layer.name - - yield (mask_sublayer, (mask_item,)) - - - # single mask shall be used directly as a "group" layer - elif len(mask_items) == 1 : - - mask_layer = _create_mask_polygon_layer(mask_layer_name) - - yield (mask_layer, (mask_items[0],)) - - - # no mask at all - else: - - mask_layer = None - - #--------------------------------------------------------------------- - # handle the image layers - - super_items = super(CoverageMaskedLayerFactory, self).generate( - eo_object, group_layer, suffix, options - ) - - for layer, data_items in super_items: - - # if we do have a mask, reference it in the layer - if mask_layer: - layer.mask = mask_layer.name - - # fix the layer name by appending the right suffix - layer.name = layer.name + suffix - if layer.group: - layer.group = layer.group + suffix - - # "re-yield" the layer and its items - yield (layer, data_items) - - - def generate_group(self, name): - layer = ms.layerObj() - layer.name = name - layer.type = ms.MS_LAYER_RASTER - return layer - diff -Nru eoxserver-0.4.0beta2/eoxserver/services/mapserver/wms/layerfactories/coverage_outlines_layer_factory.py eoxserver-0.3.2/eoxserver/services/mapserver/wms/layerfactories/coverage_outlines_layer_factory.py --- eoxserver-0.4.0beta2/eoxserver/services/mapserver/wms/layerfactories/coverage_outlines_layer_factory.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/mapserver/wms/layerfactories/coverage_outlines_layer_factory.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,76 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2011 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -from eoxserver.core import Component, implements -from eoxserver.contrib.mapserver import ( - Layer, MS_LAYER_POLYGON, shapeObj, classObj, styleObj, colorObj -) -from eoxserver.resources.coverages import models -from eoxserver.services.mapserver.interfaces import LayerFactoryInterface -from eoxserver.services.mapserver.wms.layerfactories.base import ( - AbstractLayerFactory, BaseStyleMixIn, PolygonLayerMixIn -) - - -class CoverageOutlinesLayerFactory(BaseStyleMixIn, PolygonLayerMixIn, AbstractLayerFactory): - handles = (models.RectifiedDataset, models.ReferenceableDataset, - models.RectifiedStitchedMosaic,) - suffixes = ("_outlines",) - requires_connection = False - - - def generate(self, eo_object, group_layer, suffix, options): - # don't generate any layers, but add the footprint as feature to the - # group layer - - if group_layer: - layer = group_layer - else: - layer = self._create_polygon_layer( - eo_object.identifier + "_outlines" - ) - - shape = shapeObj.fromWKT(eo_object.footprint.wkt) - shape.initValues(1) - shape.setValue(0, eo_object.identifier) - layer.addFeature(shape) - - if not group_layer: - yield layer, () - - - def generate_group(self, name): - layer = self._create_polygon_layer(name) - - # Dummy feature, or else empty groups will produce errors - shape = shapeObj() - shape.initValues(1) - shape.setValue(0, "dummy") - layer.addFeature(shape) - - return layer diff -Nru eoxserver-0.4.0beta2/eoxserver/services/mapserver/wms/legendgraphic_renderer.py eoxserver-0.3.2/eoxserver/services/mapserver/wms/legendgraphic_renderer.py --- eoxserver-0.4.0beta2/eoxserver/services/mapserver/wms/legendgraphic_renderer.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/mapserver/wms/legendgraphic_renderer.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,39 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2011 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -from eoxserver.core import implements -from eoxserver.services.mapserver.wms.util import MapServerWMSBaseComponent -from eoxserver.services.ows.wms.interfaces import ( - WMSLegendGraphicRendererInterface -) - - -class MapServerWMSLegendGraphicRenderer(MapServerWMSBaseComponent): - """ A WMS feature info renderer using MapServer. - """ - implements(WMSLegendGraphicRendererInterface) diff -Nru eoxserver-0.4.0beta2/eoxserver/services/mapserver/wms/map_renderer.py eoxserver-0.3.2/eoxserver/services/mapserver/wms/map_renderer.py --- eoxserver-0.4.0beta2/eoxserver/services/mapserver/wms/map_renderer.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/mapserver/wms/map_renderer.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,37 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2011 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -from eoxserver.core import implements -from eoxserver.services.ows.wms.interfaces import WMSMapRendererInterface -from eoxserver.services.mapserver.wms.util import MapServerWMSBaseComponent - - -class MapServerWMSMapRenderer(MapServerWMSBaseComponent): - """ A WMS map renderer using MapServer. - """ - implements(WMSMapRendererInterface) diff -Nru eoxserver-0.4.0beta2/eoxserver/services/mapserver/wms/styleapplicators/sld.py eoxserver-0.3.2/eoxserver/services/mapserver/wms/styleapplicators/sld.py --- eoxserver-0.4.0beta2/eoxserver/services/mapserver/wms/styleapplicators/sld.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/mapserver/wms/styleapplicators/sld.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,56 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -import logging - -try: - from eoxserver.contrib.mapserver import MS_VERSION_NUM -except ImportError: - MS_VERSION_NUM = 0 - -from eoxserver.core import Component, implements -from eoxserver.backends.access import connect -from eoxserver.services.mapserver.interfaces import StyleApplicatorInterface - - -logger = logging.getLogger(__name__) - - -# Versions smaller than 6.2 result in a segfault in this operation. -if MS_VERSION_NUM >= 60200: - class SLDStyleApplicator(Component): - implements(StyleApplicatorInterface) - - def apply(self, coverage, data_items, layer): - sld_items = filter(lambda d: ( - d.semantic.startswith("style") and d.format.upper() == "SLD" - ), data_items) - - for sld_item in sld_items: - sld_filename = connect(sld_item) - with open(sld_filename) as f: - layer.map.applySLD(f.read()) diff -Nru eoxserver-0.4.0beta2/eoxserver/services/mapserver/wms/util.py eoxserver-0.3.2/eoxserver/services/mapserver/wms/util.py --- eoxserver-0.4.0beta2/eoxserver/services/mapserver/wms/util.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/mapserver/wms/util.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,226 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2011 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -import logging -from itertools import chain - -from django.utils.datastructures import SortedDict - -from eoxserver.core import Component, ExtensionPoint -from eoxserver.core.config import get_eoxserver_config -from eoxserver.contrib import mapserver as ms -from eoxserver.resources.coverages.crss import CRSsConfigReader -from eoxserver.services.mapserver.interfaces import ( - ConnectorInterface, LayerFactoryInterface, StyleApplicatorInterface -) -from eoxserver.services.result import result_set_from_raw_data, get_content_type -from eoxserver.services.exceptions import RenderException -from eoxserver.services.ows.wms.exceptions import InvalidFormat - -logger = logging.getLogger(__name__) - - -class MapServerWMSBaseComponent(Component): - """ Base class for various WMS render components using MapServer. - """ - - connectors = ExtensionPoint(ConnectorInterface) - layer_factories = ExtensionPoint(LayerFactoryInterface) - style_applicators = ExtensionPoint(StyleApplicatorInterface) - - def render(self, layer_groups, request_values, **options): - map_ = ms.Map() - map_.setMetaData("ows_enable_request", "*") - map_.setProjection("EPSG:4326") - map_.imagecolor.setRGB(0, 0, 0) - - # set supported CRSs - decoder = CRSsConfigReader(get_eoxserver_config()) - crss_string = " ".join( - map(lambda crs: "EPSG:%d" % crs, decoder.supported_crss_wms) - ) - map_.setMetaData("ows_srs", crss_string) - map_.setMetaData("wms_srs", crss_string) - - self.check_parameters(map_, request_values) - - session = self.setup_map(layer_groups, map_, options) - - with session: - request = ms.create_request(request_values) - raw_result = map_.dispatch(request) - - result = result_set_from_raw_data(raw_result) - return result, get_content_type(result) - - def check_parameters(self, map_, request_values): - for key, value in request_values: - if key.lower() == "format": - if not map_.getOutputFormatByName(value): - raise InvalidFormat(value) - break - else: - raise RenderException("Missing 'format' parameter") - - @property - def suffixes(self): - return list( - chain(*[factory.suffixes for factory in self.layer_factories]) - ) - - def get_connector(self, data_items): - for connector in self.connectors: - if connector.supports(data_items): - return connector - return None - - def get_layer_factory(self, suffix): - result = None - for factory in self.layer_factories: - if suffix in factory.suffixes: - if result: - pass # TODO - #raise Exception("Found") - result = factory - return result - return result - - def setup_map(self, layer_selection, map_, options): - group_layers = SortedDict() - session = ConnectorSession(options) - - # set up group layers before any "real" layers - for collections, _, name, suffix in tuple(layer_selection.walk()): - if not collections: - continue - - # get a factory for the given suffix - factory = self.get_layer_factory(suffix) - if not factory: - # raise or pass? - continue - - # get the groups name, which is the name of the collection + the - # suffix - group_name = collections[-1].identifier + (suffix or "") - - # generate a group layer - group_layer = factory.generate_group(group_name) - group_layers[group_name] = group_layer - - # set up the actual layers for each coverage - for collections, coverage, name, suffix in layer_selection.walk(): - # get a factory for the given coverage and suffix - factory = self.get_layer_factory(suffix) - - group_layer = None - group_name = None - - if collections: - group_name = collections[-1].identifier + (suffix or "") - group_layer = group_layers.get(group_name) - - if not coverage: - # add an empty layer to not produce errors out of bounds. - if name: - tmp_layer = ms.layerObj() - tmp_layer.name = (name + suffix) if suffix else name - layers_and_data_items = ((tmp_layer, ()),) - else: - layers_and_data_items = () - - elif not factory: - tmp_layer = ms.layerObj() - tmp_layer.name = name - layers_and_data_items = ((tmp_layer, ()),) - else: - data_items = coverage.data_items.all() - coverage.cached_data_items = data_items - layers_and_data_items = tuple(factory.generate( - coverage, group_layer, suffix, options - )) - - for layer, data_items in layers_and_data_items: - connector = self.get_connector(data_items) - - if group_name: - layer.setMetaData("wms_layer_group", "/" + group_name) - - session.add(connector, coverage, data_items, layer) - - coverage_layers = [layer for _, layer, _ in session.coverage_layers] - for layer in chain(group_layers.values(), coverage_layers): - old_layer = map_.getLayerByName(layer.name) - if old_layer: - # remove the old layer and reinsert the new one, to - # raise the layer to the top. - # TODO: find a more efficient way to do this - map_.removeLayer(old_layer.index) - map_.insertLayer(layer) - - # apply any styles - # TODO: move this to map/legendgraphic renderer only? - for coverage, layer, data_items in session.coverage_layers: - for applicator in self.style_applicators: - applicator.apply(coverage, data_items, layer) - - return session - - def get_empty_layers(self, name): - layer = ms.layerObj() - layer.name = name - layer.setMetaData("wms_enable_request", "getmap") - return (layer,) - - -class ConnectorSession(object): - """ Helper class to be used in `with` statements. Allows connecting and - disconnecting all added layers with the given data items. - """ - def __init__(self, options=None): - self.item_list = [] - self.options = options or {} - - def add(self, connector, coverage, data_items, layer): - self.item_list.append( - (connector, coverage, layer, data_items) - ) - - def __enter__(self): - for connector, coverage, layer, data_items in self.item_list: - if connector: - connector.connect(coverage, data_items, layer, self.options) - - def __exit__(self, *args, **kwargs): - for connector, coverage, layer, data_items in self.item_list: - if connector: - connector.disconnect(coverage, data_items, layer, self.options) - - @property - def coverage_layers(self): - return map(lambda it: (it[1], it[2], it[3]), self.item_list) diff -Nru eoxserver-0.4.0beta2/eoxserver/services/mapserver.py eoxserver-0.3.2/eoxserver/services/mapserver.py --- eoxserver-0.4.0beta2/eoxserver/services/mapserver.py 1970-01-01 00:00:00.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/mapserver.py 2013-12-10 14:57:00.000000000 +0000 @@ -0,0 +1,446 @@ +#------------------------------------------------------------------------------- +# $Id$ +# +# Project: EOxServer +# Authors: Stephan Krause +# Stephan Meissl +# +#------------------------------------------------------------------------------- +# Copyright (C) 2011 EOX IT Services GmbH +# +# 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 of this Software or works derived from this 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. +#------------------------------------------------------------------------------- + +""" +This module contains the abstract base classes for request handling. +""" + +import logging +import os.path +from cgi import escape +import time + +from django.conf import settings +import mapscript + +from eoxserver.core.interfaces import Method, ObjectArg, ListArg +from eoxserver.core.registry import RegisteredInterface +from eoxserver.core.exceptions import InternalError +from eoxserver.contrib.gdal import GDT_Byte, GDT_Int16, GDT_UInt16, GDT_Float32 +from eoxserver.services.base import BaseRequestHandler +from eoxserver.services.requests import OWSRequest, Response +from eoxserver.services.exceptions import InvalidRequestException +from eoxserver.core.util.multiparttools import mpUnpack, mpPack, capitalize +from eoxserver.core.util.multiparttools import getMimeType, getMultipartBoundary + + +logger = logging.getLogger(__name__) + +#------------------------------------------------------------------------------- +# utilities + +# content-type parsing +is_xml = lambda ct : getMimeType(ct) in ("text/xml","application/xml","application/gml+xml") +is_multipart = lambda ct : getMimeType(ct).startswith("multipart/") + +# capilatize header names +_headcap = lambda p : ( capitalize(p[0]) , p[1] ) +headcap = lambda h : dict( map( _headcap , h.items() ) ) + +#------------------------------------------------------------------------------- + +class MapServerRequest(OWSRequest): + """ + This class inherits from :class:`~.OWSRequest`. + + The constructor expects a single argument ``req`` which is expected to + contain an :class:`~.OWSRequest` instance. The parameters and decoder + will be taken from that instance. + + :class:`MapServerRequest` objects add two properties: first a ``map`` + property which contains a :class:`mapscript.mapObj`, and second an + ``ows_req`` property which contains a :class:`mapscript.OWSRequest` + object. These properties are not configured at the beginning. + """ + def __init__(self, req): + super(MapServerRequest, self).__init__(req.http_req, params=req.params, decoder=req.decoder) + + self.version = req.version + + self.map = mapscript.mapObj() + self.ows_req = mapscript.OWSRequest() + +class MapServerResponse(Response): + """ + This class inherits from :class:`~.Response`. It adds methods to handle + with response data obtained from MapServer, including methods for + multipart messages. + + The constructor takes several arguments. In ``ms_response``, the response + buffer as returned by MapServer is expected. The ``ms_content_type`` + argument shall be set to the MIME type of the response content. + ``ms_status`` shall contain the MapServer status as returned by the + call to :meth:`mapscript.mapObj.OWSDispatch`. ``headers`` and ``status`` + are optional and have the same meaning as in :class:`~.Response`. + """ + + def __init__(self, ms_response, ms_content_type, ms_status, headers={}, status=None): + super(MapServerResponse, self).__init__(content=ms_response, content_type=ms_content_type, headers=headers, status=status) + self.ms_status = ms_status + + self.ms_response_data = None + self.ms_response_data_headers = {} + self.ms_response_xml = None + self.ms_response_xml_headers = {} + + def splitResponse(self): + """ + This method splits a multipart response into its different parts. + + The XML part is stored in the ``ms_response_xml`` property of the + object. The coverage data is stored in the ``ms_response_data`` + property of the object. The headers of the parts are stored in + the ``ms_response_xml_headers`` and ``ms_response_data_headers`` + properties respectively. + """ + + if is_multipart( self.content_type ) : + + # extract multipart boundary + boundary = getMultipartBoundary( self.content_type ) + + for headers,offset,size in mpUnpack(self.content,boundary,capitalize=True) : + + if is_xml( headers['Content-Type'] ) : + self.ms_response_xml = self.content[offset:(offset+size)] + self.ms_response_xml_headers = headers + else : + self.ms_response_data = self.content[offset:(offset+size)] + self.ms_response_data_headers = headers + + else : # single part payload + + headers = headcap( self.headers ) + headers['Content-Type'] = self.content_type + + if is_xml( self.content_type ) : + self.ms_response_xml = self.content + self.ms_response_xml_headers = headers + else : + self.ms_response_data = self.content + self.ms_response_data_headers = headers + + + def getProcessedResponse(self, response_xml, headers_xml=None, boundary="wcs", subtype="mixed"): + """ + This method returns a :class:`~.Response` object that contains the + coverage data generated by the original MapServer call and the + XML data contained in the ``response_xml`` argument. + + The ``headers_xml`` parameter may contain a dictionary of headers + to be tagged on the XML part of the multipart response. The + ``boundary`` argument shall contain the boundary string used for + delimiting the different parts of the message and defaults to ``wcs``. + The ``subtype`` argument relates to the second part of the MIME type + statement and defaults to ``mixed`` for a complete MIME type of + ``multipart/mixed``. + """ + if self.ms_response_data is not None: # mutipart response + + if headers_xml is None: headers_xml = self.ms_response_xml_headers + + # normalize header content + headers_xml = headcap( headers_xml ) + headers_data = headcap( self.ms_response_data_headers ) + + # prepare multipart package + parts = [ + ( headers_xml.items() , response_xml ) , + ( headers_data.items() , self.ms_response_data ) , + ] + + content_type = "multipart/%s; boundary=%s"%(subtype,boundary) + + return Response(mpPack(parts,boundary),content_type,{},self.getStatus()) + + else : # pure XML response - currently not in use - possibly harmfull as there is no way to test it! + + if headers_xml is None: headers_xml = self.headers + + return Response(response_xml,self.content_type,headcap(headers_xml),self.getStatus()) + + def getContentType(self): + """ + Returns the content type of the response. + """ + if "Content-type" in self.headers: #MP: Is the 'Content-type' case correct? + return self.headers["Content-type"] + else: + return self.content_type # TODO: headers for multipart messages + + def getStatus(self): + """ + Returns the HTTP status code of the response. + """ + if self.status is None: + if self.ms_status is not None and self.ms_status == 0: + return 200 + else: + return 400 + else: + return self.status + +class MapServerLayerGeneratorInterface(RegisteredInterface): + """ + This interface is not in use. + """ + REGISTRY_CONF = { + "name": "MapServer Layer Generator Interface", + "intf_id": "services.mapserver.MapServerLayerGeneratorInterface", + "binding_method": "kvp", + "registry_keys": ( + "services.mapserver.service", + "services.mapserver.version", + "services.mapserver.eo_object_type", + ) + } + + configure = Method( + ObjectArg("ms_req", arg_class=MapServerRequest), + ObjectArg("eo_object"), + returns=ObjectArg("@return", arg_class=mapscript.layerObj) + ) + +class MapServerDataConnectorInterface(RegisteredInterface): + """ + This interface is intended for objects that configure the input data for + a MapServer layer. The basic rationale for this is that there are at + least three different types of data sources that need differentt + treatment: + + * data stored in single plain files + * tiled data with references in a tile index (a shape file) + * rasdaman arrays + + Others might be added in the course of development of EOxServer. + + .. method:: congigure(layer, eo_object) + + This method takes a :class:`mapscript.layerObj` object and an ``eo_object`` + as input and configures the MapServer layer according to the type of + data package used by the ``eo_object`` (RectifiedDataset, + ReferenceableDataset or RectifiedStitchedMosaic). + """ + REGISTRY_CONF = { + "name": "MapServer Data Connector Interface", + "intf_id": "services.mapserver.MapServerDataConnectorInterface", + "binding_method": "kvp", + "registry_keys": ( + "services.mapserver.data_structure_type", + ) + } + + configure = Method( + ObjectArg("layer", arg_class=mapscript.layerObj), + ObjectArg("eo_object"), + ListArg("filter_exprs", default=None), + returns=ObjectArg("@return", arg_class=mapscript.layerObj) + ) + +class MapServerOperationHandler(BaseRequestHandler): + """ + MapServerOperationHandler serves as parent class for all operations + involving calls to MapServer. It is not an abstract class, but implements + the most basic functionality, i.e. simply passing on a request to + MapServer as it comes in. + + This class implements a workflow for request handling that + involves calls to MapServer using its Python binding (mapscript). + Requests are processed in six steps: + + * retrieve coverage information (:meth:`createCoverages` method) + * configure a MapServer ``OWSRequest`` object with + parameters from the request (:meth:`configureRequest` method) + * configure a MapServer ``mapObj`` object with + parameters from the request and the config + (:meth:`configureMapObj` method) + * add layers to the MapServer ``mapObj`` (:meth:`addLayers` method) + * dispatch the request, i.e. let MapServer actually do its + work; return the result (:meth:`dispatch` method) + * postprocess the MapServer response (:meth:`postprocess` method) + + Child classes may override the configureRequest, configureMap, + postprocess and postprocessFault methods in order to customize + functionality. If possible, the handle and dispatch methods should + not be overridden. + """ + + PARAM_SCHEMA = { + "service": {"xml_location": "/@service", "xml_type": "string", "kvp_key": "service", "kvp_type": "string"}, + "version": {"xml_location": "/@version", "xml_type": "string", "kvp_key": "version", "kvp_type": "string"}, + "operation": {"xml_location": "/", "xml_type": "localName", "kvp_key": "request", "kvp_type": "string"} + } + + def __init__(self): + super(MapServerOperationHandler, self).__init__() + + self.req = None + self.map = mapscript.mapObj(os.path.join(settings.PROJECT_DIR, "conf", "template.map")) + self.ows_req = mapscript.OWSRequest() + + self.temp_files = [] + + def _addTempFile(self, temp_file_path): + self.temp_files.append(temp_file_path) + + def _setParameter(self, key, value): + self.ows_req.setParameter(key, value) + + def _addParameter(self, key, value): + self.ows_req.addParameter(key, value) + + def configureRequest(self): + """ + This method configures the ``ms_req.ows_req`` object (an + instance of ``mapscript.OWSRequest``) with the parameters + passed in with the user request. This method can be overridden + in order to change the treatment of parameters. + """ + if self.req.getParamType() == "kvp": + self.ows_req.type = mapscript.MS_GET_REQUEST + for key, values in self.req.getParams().items(): + if len(values) == 1: + self._setParameter(key.lower(), escape(values[0])) + else: + c = 0 + for value in values: + # addParameter() available in MapServer >= 6.2 + # https://github.com/mapserver/mapserver/issues/3973 + try: + self._addParameter(key.lower(), escape(value)) + # Workaround for MapServer 6.0 + except AttributeError: + if c == 0: + new_key = key.lower() + else: + new_key = "%s_%d" % (key.lower(), c) + self._setParameter(new_key, escape(value)) + c += 1 + while "%s_%d" % (key.lower(), c) in self.req.getParams(): + c += 1 + + self._setParameter("version", self.req.getVersion()) + elif self.req.getParamType() == "xml": + self.ows_req.type = mapscript.MS_POST_REQUEST + self.ows_req.postrequest = self.req.getParams() + else: + raise Exception("Unknown parameter type '%s'." % self.req.getParamType()) + + def dispatch(self): + """ + This method actually executes the MapServer request by calling + ``ms_req.map.OWSDispatch()``. This method should not be + overridden by child classes. + """ + + logger.debug("MapServer: Installing stdout to buffer.") + mapscript.msIO_installStdoutToBuffer() + + try: + logger.debug("MapServer: Dispatching.") + ts = time.time() + # Execute the OWS request by mapserver, obtain the status in + # dispatch_status (0 is OK) + dispatch_status = self.map.OWSDispatch(self.ows_req) + te = time.time() + logger.debug("MapServer: Dispatch took %f seconds." % (te - ts)) + except Exception, e: + raise InvalidRequestException( + str(e), + "NoApplicableCode", + None + ) + + logger.debug("MapServer: Retrieving content-type.") + try: + content_type = mapscript.msIO_stripStdoutBufferContentType() + mapscript.msIO_stripStdoutBufferContentHeaders() + + except mapscript.MapServerError: + # degenerate response. Manually split headers from content + complete_result = mapscript.msIO_getStdoutBufferBytes() + parts = complete_result.split("\r\n") + result = parts[-1] + headers = parts[:-1] + + for header in headers: + if header.lower().startswith("content-type"): + content_type = header[14:] + break + else: + content_type = None + + else: + logger.debug("MapServer: Retrieving stdout buffer bytes.") + result = mapscript.msIO_getStdoutBufferBytes() + + logger.debug("MapServer: Performing MapServer cleanup.") + # Workaround for MapServer issue #4369 + msversion = mapscript.msGetVersionInt() + if msversion < 60004 or (msversion < 60200 and msversion >= 60100): + mapscript.msCleanup() + else: + mapscript.msIO_resetHandlers() + + return MapServerResponse(result, content_type, dispatch_status) + + def cleanup(self): + for temp_file in self.temp_files: + try: + os.remove(temp_file) + except: + logger.warning("Could not remove temporary file '%s'" % temp_file) + +def gdalconst_to_imagemode(const): + """ + This function translates a GDAL data type constant as defined in the + :mod:`gdalconst` module to a MapScript image mode constant. + """ + if const == GDT_Byte: + return mapscript.MS_IMAGEMODE_BYTE + elif const == GDT_Int16 or const == GDT_UInt16: + return mapscript.MS_IMAGEMODE_INT16 + elif const == GDT_Float32: + return mapscript.MS_IMAGEMODE_FLOAT32 + else: + raise InternalError("MapServer is not capable to process the datatype %d" % const) + +def gdalconst_to_imagemode_string(const): + """ + This function translates a GDAL data type constant as defined in the + :mod:`gdalconst` module to a string as used in the MapServer map file + to denote an image mode. + """ + if const == GDT_Byte: + return "BYTE" + elif const == GDT_Int16 or const == GDT_UInt16: + return "INT16" + elif const == GDT_Float32: + return "FLOAT32" + diff -Nru eoxserver-0.4.0beta2/eoxserver/services/models.py eoxserver-0.3.2/eoxserver/services/models.py --- eoxserver-0.4.0beta2/eoxserver/services/models.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/models.py 2013-12-10 14:57:00.000000000 +0000 @@ -1,16 +1,18 @@ #------------------------------------------------------------------------------- +# $Id$ # # Project: EOxServer -# Authors: Fabian Schindler +# Authors: Stephan Krause +# Stephan Meissl # #------------------------------------------------------------------------------- -# Copyright (C) 2014 EOX IT Services GmbH +# Copyright (C) 2011 EOX IT Services GmbH # # 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 +# 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 @@ -24,25 +26,3 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. #------------------------------------------------------------------------------- - - -from django.contrib.gis.db import models -from eoxserver.resources.coverages import models as coverage_models - - -class WMSRenderOptions(models.Model): - """ Additional options for rendering coverages via WMS. - """ - - coverage = models.OneToOneField(coverage_models.Coverage) - - default_red = models.PositiveIntegerField(null=True, default=None) - default_green = models.PositiveIntegerField(null=True, default=None) - default_blue = models.PositiveIntegerField(null=True, default=None) - default_alpha = models.PositiveIntegerField(null=True, default=None) - - resampling = models.CharField(null=True, blank=True, max_length=16) - - scale_auto = models.BooleanField(default=False) - scale_min = models.PositiveIntegerField(null=True) - scale_max = models.PositiveIntegerField(null=True) diff -Nru eoxserver-0.4.0beta2/eoxserver/services/native/wcs/capabilities_renderer.py eoxserver-0.3.2/eoxserver/services/native/wcs/capabilities_renderer.py --- eoxserver-0.4.0beta2/eoxserver/services/native/wcs/capabilities_renderer.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/native/wcs/capabilities_renderer.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,68 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -from django.conf import settings - -from eoxserver.core import Component, implements -from eoxserver.services.result import ResultBuffer -from eoxserver.services.ows.version import Version -from eoxserver.services.ows.wcs.interfaces import ( - WCSCapabilitiesRendererInterface -) -from eoxserver.services.ows.wcs.v20.encoders import WCS20CapabilitiesXMLEncoder - - -class NativeWCS20CapabilitiesRenderer(Component): - implements(WCSCapabilitiesRendererInterface) - - versions = (Version(2, 0),) - - def supports(self, params): - if params.version not in self.versions: - return False - - if params.accept_formats and "text/xml" not in params.accept_formats: - return False - - # TODO: accept_languages? - return True - - def render(self, params): - encoder = WCS20CapabilitiesXMLEncoder() - return [ - ResultBuffer( - encoder.serialize( - encoder.encode_capabilities( - params.sections or ("all"), params.coverages, - getattr(params, "dataset_series", ()), - params.http_request - ), pretty_print=settings.DEBUG - ), - encoder.content_type - ) - ] diff -Nru eoxserver-0.4.0beta2/eoxserver/services/native/wcs/coverage_description_renderer.py eoxserver-0.3.2/eoxserver/services/native/wcs/coverage_description_renderer.py --- eoxserver-0.4.0beta2/eoxserver/services/native/wcs/coverage_description_renderer.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/native/wcs/coverage_description_renderer.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,62 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -from django.conf import settings - -from eoxserver.core import Component, implements -from eoxserver.services.result import ResultBuffer -from eoxserver.services.ows.version import Version -from eoxserver.services.ows.wcs.v20.encoders import WCS20EOXMLEncoder -from eoxserver.services.ows.wcs.interfaces import ( - WCSCoverageDescriptionRendererInterface -) - - -class NativeWCS20CoverageDescriptionRenderer(Component): - """ Coverage description renderer for WCS 2.0 using the EO application - profile. - """ - - implements(WCSCoverageDescriptionRendererInterface) - - versions = (Version(2, 0),) - - def supports(self, params): - return params.version in self.versions - - def render(self, params): - encoder = WCS20EOXMLEncoder() - return [ - ResultBuffer( - encoder.serialize( - encoder.encode_coverage_descriptions(params.coverages), - pretty_print=settings.DEBUG - ), - encoder.content_type - ) - ] diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ogc.py eoxserver-0.3.2/eoxserver/services/ogc.py --- eoxserver-0.4.0beta2/eoxserver/services/ogc.py 1970-01-01 00:00:00.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ogc.py 2013-12-10 14:57:00.000000000 +0000 @@ -0,0 +1,122 @@ +#------------------------------------------------------------------------------- +# $Id$ +# +# Project: EOxServer +# Authors: Stephan Krause +# Stephan Meissl +# +#------------------------------------------------------------------------------- +# Copyright (C) 2011 EOX IT Services GmbH +# +# 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 of this Software or works derived from this 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. +#------------------------------------------------------------------------------- + +""" +This module contains old style exception handlers that use the OGC +namespace for exception reports (prior to OWS Common). +""" + +from eoxserver.core.util.xmltools import XMLEncoder +from eoxserver.services.exceptions import InvalidRequestException +from eoxserver.services.base import BaseExceptionHandler +from eoxserver.services.interfaces import ( + ExceptionHandlerInterface, ExceptionEncoderInterface +) + +class OGCExceptionHandler(BaseExceptionHandler): + """ + Handler class for the OGC namespace. + """ + REGISTRY_CONF = { + "name": "OGC Namespace Exception Handler", + "impl_id": "services.ogc.OGCExceptionHandler", + "registry_values": { + "services.interfaces.exception_scheme": "ogc" + } + } + + def _filterExceptions(self, exception): + if not isinstance(exception, InvalidRequestException): + raise + + def _getEncoder(self): + return OGCExceptionEncoder(self.schemas) + + def _getContentType(self, exception): + return "application/vnd.ogc.se_xml" + +OGCExceptionHandlerImplementation = ExceptionHandlerInterface.implement(OGCExceptionHandler) + +class OGCExceptionEncoder(XMLEncoder): + """ + Encoder class for OGC namespace exception reports. + """ + REGISTRY_CONF = { + "name": "OGC Namespace Exception Report Encoder", + "impl_id": "services.ogc.OGCExceptionEncoder", + "registry_values": { + "services.interfaces.exception_scheme": "ogc" + } + } + + def _initializeNamespaces(self): + return { + "ogc": "http://www.opengis.net/ogc", + "xsi": "http://www.w3.org/2001/XMLSchema-instance" + } + + def encodeExceptionReport(self, exception_text, exception_code, locator=None): + if locator is None: + element = self._makeElement("ogc", "ServiceExceptionReport", [ + ("", "@version", "1.2.0"), + ("ogc", "ServiceException", [ + ("", "@code", exception_code), + ("", "@@", exception_text) + ]) + ]) + else: + element = self._makeElement("ogc", "ServiceExceptionReport", [ + ("", "@version", "1.2.0"), + ("ogc", "ServiceException", [ + ("", "@code", exception_code), + ("", "@locator", locator), + ("", "@@", exception_text) + ]) + ]) + + if self.schemas is not None: + schemas_location = " ".join(["%s %s"%(ns, location) for ns, location in self.schemas.iteritems()]) + element.setAttributeNS(self.ns_dict["xsi"], "%s:%s" % ("xsi", "schemaLocation"), schemas_location) + + return element + + def encodeInvalidRequestException(self, exception): + return self.encodeExceptionReport( + exception.msg, + exception.error_code, + exception.locator + ) + + def encodeVersionNegotiationException(self, exception): + return "" # TODO: check against OWS Common + + def encodeException(self, exception): + return self.encodeExceptionReport("Internal Server Error", "NoApplicableCode") + +OGCExceptionEncoderImplementation = ExceptionEncoderInterface.implement(OGCExceptionEncoder) diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/common/config.py eoxserver-0.3.2/eoxserver/services/ows/common/config.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/common/config.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/common/config.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,67 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2011 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -from eoxserver.core.decoders import config, typelist, Choice - - -class CapabilitiesConfigReader(config.Reader): - section = "services.ows" - - update_sequence = config.Option(default="0") - name = config.Option(default="None") - title = config.Option(default="None") - abstract = config.Option(default="None") - keywords = config.Option(type=typelist(str, ","), default=[]) - fees = config.Option(default="None") - access_constraints = config.Option(default="None") - provider_name = config.Option(default="None") - provider_site = config.Option(default="None") - individual_name = config.Option(default="None") - position_name = config.Option(default="None") - phone_voice = config.Option(default="None") - phone_facsimile = config.Option(default="None") - delivery_point = config.Option(default="None") - city = config.Option(default="None") - administrative_area = config.Option(default="None") - postal_code = config.Option(default="None") - country = config.Option(default="None") - electronic_mail_address = config.Option(default="None") - onlineresource = config.Option(default="None") - hours_of_service = config.Option(default="None") - contact_instructions = config.Option(default="None") - role = config.Option(default="None") - - http_service_url = Choice( - config.Option("http_service_url", section="services.owscommon", required=True), - config.Option("http_service_url", section="services.ows", required=True), - ) - - -class WCSEOConfigReader(config.Reader): - section = "services.ows.wcs20" - paging_count_default = config.Option(type=int, default=None) diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/common/v11/encoders.py eoxserver-0.3.2/eoxserver/services/ows/common/v11/encoders.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/common/v11/encoders.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/common/v11/encoders.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,58 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -from lxml.builder import ElementMaker - -from eoxserver.core.util.xmltools import XMLEncoder, NameSpace, NameSpaceMap - - -ns_xlink = NameSpace("http://www.w3.org/1999/xlink", "xlink") -ns_ows = NameSpace("http://www.opengis.net/ows/1.1", "ows", "http://schemas.opengis.net/ows/1.1.0/owsExceptionReport.xsd") -ns_xml = NameSpace("http://www.w3.org/XML/1998/namespace", "xml") - -nsmap = NameSpaceMap(ns_ows) -OWS = ElementMaker(namespace=ns_ows.uri, nsmap=nsmap) - - -class OWS11ExceptionXMLEncoder(XMLEncoder): - def encode_exception(self, message, version, code, locator=None): - exception_attributes = { - "exceptionCode": code - } - - if locator: - exception_attributes["locator"] = locator - - exception_text = (OWS("ExceptionText", message),) if message else () - - return OWS("ExceptionReport", - OWS("Exception", *exception_text, **exception_attributes - ), version=version, **{ns_xml("lang"): "en"} - ) - - def get_schema_locations(self): - return nsmap.schema_locations diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/common/v20/encoders.py eoxserver-0.3.2/eoxserver/services/ows/common/v20/encoders.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/common/v20/encoders.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/common/v20/encoders.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,69 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2011 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -from lxml.builder import ElementMaker - -from eoxserver.core.util.xmltools import XMLEncoder, NameSpace, NameSpaceMap - - -ns_xlink = NameSpace("http://www.w3.org/1999/xlink", "xlink") -ns_ows = NameSpace("http://www.opengis.net/ows/2.0", "ows", "http://schemas.opengis.net/ows/2.0/owsAll.xsd") -ns_xml = NameSpace("http://www.w3.org/XML/1998/namespace", "xml") - -nsmap = NameSpaceMap(ns_ows) -OWS = ElementMaker(namespace=ns_ows.uri, nsmap=nsmap) - - -class OWS20Encoder(XMLEncoder): - def encode_reference(self, node_name, href, reftype="simple"): - - attributes = {ns_xlink("href"): href} - if reftype: - attributes[ns_xlink("type")] = reftype - - return OWS(node_name, **attributes) - - -class OWS20ExceptionXMLEncoder(XMLEncoder): - def encode_exception(self, message, version, code, locator=None): - exception_attributes = { - "exceptionCode": str(code) - } - - if locator: - exception_attributes["locator"] = str(locator) - - exception_text = (OWS("ExceptionText", message),) if message else () - - return OWS("ExceptionReport", - OWS("Exception", *exception_text, **exception_attributes - ), version=version, **{ns_xml("lang"): "en"} - ) - - def get_schema_locations(self): - return nsmap.schema_locations diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/common/v20/exceptionhandler.py eoxserver-0.3.2/eoxserver/services/ows/common/v20/exceptionhandler.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/common/v20/exceptionhandler.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/common/v20/exceptionhandler.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,53 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -from eoxserver.services.ows.common.v20.encoders import OWS20ExceptionXMLEncoder - - -class OWS20ExceptionHandler(object): - """ A Fallback exception handler. This class does on purpose not implement - the ExceptionHandlerInterface and must be instantiated manually. - """ - - def handle_exception(self, request, exception): - message = str(exception) - version = "2.0.0" - # NOTE: An existing 'code' attribute can be set to None value!!! - code = getattr(exception, "code", None) or "NoApplicableCode" - locator = getattr(exception, "locator", None) - status_code = 400 - - encoder = OWS20ExceptionXMLEncoder() - - return ( - encoder.serialize( - encoder.encode_exception(message, version, code, locator) - ), - encoder.content_type, - status_code - ) - diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/component.py eoxserver-0.3.2/eoxserver/services/ows/component.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/component.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/component.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,243 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2011 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -import itertools -from functools import partial - -from eoxserver.core import Component, ExtensionPoint, env - -from eoxserver.services.ows.interfaces import * -from eoxserver.services.ows.decoders import get_decoder -from eoxserver.services.exceptions import ( - ServiceNotSupportedException, VersionNotSupportedException, - VersionNegotiationException, OperationNotSupportedException -) -from eoxserver.services.ows.common.v20.exceptionhandler import ( - OWS20ExceptionHandler -) - - -class ServiceComponent(Component): - service_handlers = ExtensionPoint(ServiceHandlerInterface) - exception_handlers = ExtensionPoint(ExceptionHandlerInterface) - - get_service_handlers = ExtensionPoint(GetServiceHandlerInterface) - post_service_handlers = ExtensionPoint(PostServiceHandlerInterface) - - version_negotiation_handlers = ExtensionPoint(VersionNegotiationInterface) - - def __init__(self, *args, **kwargs): - super(ServiceComponent, self).__init__(*args, **kwargs) - - def query_service_handler(self, request): - """ Tries to find the correct service handler for a given request. The - request ``method`` can either be "POST" (in which case the request body - is parsed as XML) or "GET" (in which case the request is parsed - as "KVP"). - - If necessary a version negotiation is conducted, following OWS - guidelines. - - :param request: a :class:`Django HttpRequest ` - object - :returns: the request handler component for the given request - :raises ServiceNotSupportedException: if the service is not supported - by any component - :raises VersionNotSupportedException: if the specified version is not - supported - :raises OperationNotSupportedException: if the specified request - operation is not supported - """ - - decoder = get_decoder(request) - if request.method == "GET": - handlers = self.get_service_handlers - elif request.method == "POST": - handlers = self.post_service_handlers - else: - handlers = self.service_handlers - - version = decoder.version - if version is None: - accepted_versions = decoder.acceptversions - handlers = filter_handlers( - handlers, decoder.service, accepted_versions, decoder.request - ) - return self.version_negotiation(handlers, accepted_versions) - - # check that the service is supported - handlers = filter( - partial(handler_supports_service, service=decoder.service), handlers - ) - if not handlers: - raise ServiceNotSupportedException(decoder.service) - - # check that the required version is enabled - handlers_ = filter( - lambda h: decoder.version in h.versions, handlers - ) - if not handlers_: - # old style version negotiation shall always return capabilities - if decoder.request == "GETCAPABILITIES": - handlers = [sorted( - filter( - lambda h: decoder.request == h.request.upper(), handlers - ), key=lambda h: max(h.versions), reverse=True - )[0]] - else: - raise VersionNotSupportedException( - decoder.service, decoder.version - ) - else: - handlers = handlers_ - - # check that the required operation is supported and sort by the highest - # version supported in descending manner - handlers = sorted( - filter( - lambda h: decoder.request == h.request.upper(), handlers - ), key=lambda h: max(h.versions), reverse=True - ) - - if not handlers: - operation = decoder.request - raise OperationNotSupportedException( - "Operation '%s' is not supported." % operation, operation - ) - - # return the handler with the highest version - return handlers[0] - - def query_service_handlers(self, service=None, versions=None, request=None, - method=None): - """ Query the service handler components, filtering optionally by - ``service``, ``versions``, ``request`` or ``method``. - """ - method = method.upper() if method is not None else None - - if method == "GET": - handlers = self.get_service_handlers - elif method == "POST": - handlers = self.post_service_handlers - elif method is None: - handlers = self.service_handlers - else: - return [] - - handlers = filter_handlers(handlers, service, versions, request) - return sort_handlers(handlers) - - def query_exception_handler(self, request): - try: - decoder = get_decoder(request) - handlers = self.exception_handlers - handlers = sorted( - filter( - partial(handler_supports_service, service=decoder.service), - self.exception_handlers - ), - key=lambda h: max(h.versions), reverse=True - ) - - # try to get the correctly versioned exception handler - if decoder.version: - for handler in handlers: - if decoder.version in handler.versions: - return handler - else: - # return the exception handler with the highest version, - # if one is available - return handlers[0] - except: - # swallow any exception here, because we *really* need a handler - # to correctly show the exception. - pass - - # last resort fallback is a plain OWS exception handler - return OWS20ExceptionHandler() - - def version_negotiation(self, handlers, accepted_versions=None): - version_to_handler = {} - for handler in handlers: - for version in handler.versions: - version_to_handler.setdefault(version, handler) - - available_versions = sorted(version_to_handler.keys(), reverse=True) - if not available_versions: - raise VersionNegotiationException() - - if not accepted_versions: - return version_to_handler[available_versions[0]] - - combinations = itertools.product(accepted_versions, available_versions) - for accepted_version, available_version in combinations: - if accepted_version == available_version: - return version_to_handler[available_version] - - raise VersionNegotiationException() - - -def filter_handlers(handlers, service=None, versions=None, request=None): - """ Utility function to filter the given OWS service handlers by their - attributes 'service', 'versions' and 'request'. - """ - - service = service.upper() if service is not None else None - request = request.upper() if request is not None else None - - if service: - handlers = filter( - partial(handler_supports_service, service=service), handlers - ) - - if request: - handlers = filter(lambda h: h.request.upper() == request, handlers) - - if versions: - handlers = [ - handler for handler in handlers - if any(version in handler.versions for version in versions) - ] - - return handlers - - -def sort_handlers(handlers, ascending=True): - return sorted( - handlers, key=lambda h: getattr(h, "index", 100000), - reverse=not ascending - ) - - -def handler_supports_service(handler, service=None): - """ Convenience method to check whether or not a handler supports a service. - """ - if isinstance(handler.service, basestring): - return handler.service.upper() == service - else: - return service in handler.service diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/decoders.py eoxserver-0.3.2/eoxserver/services/ows/decoders.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/decoders.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/decoders.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,71 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -from eoxserver.core.decoders import kvp, xml, upper, typelist -from eoxserver.core.util.xmltools import NameSpace, NameSpaceMap -from eoxserver.services.ows.version import parse_version_string - - -ns_xlink = NameSpace("http://www.w3.org/1999/xlink", "xlink") -ns_ows10 = NameSpace("http://www.opengis.net/ows/1.0", "ows10") -ns_ows11 = NameSpace("http://www.opengis.net/ows/1.1", "ows11") -ns_ows20 = NameSpace("http://www.opengis.net/ows/2.0", "ows20") - -nsmap = NameSpaceMap(ns_ows10, ns_ows11, ns_ows20) - - -def get_decoder(request): - """ Convenience function to return the right OWS Common request deocder for - the given `django.http.HttpRequest`. - """ - if request.method == "GET": - return OWSCommonKVPDecoder(request.GET) - elif request.method == "POST": - # TODO: this may also be in a different format. - return OWSCommonXMLDecoder(request.body) - - -class OWSCommonKVPDecoder(kvp.Decoder): - service = kvp.Parameter("service", type=upper, num="?") - version = kvp.Parameter("version", type=parse_version_string, num="?") - request = kvp.Parameter("request", type=upper) - acceptversions = kvp.Parameter(type=typelist(parse_version_string, ","), num="?") - - -class OWSCommonXMLDecoder(xml.Decoder): - service = xml.Parameter("@service", type=upper, num="?") - version = xml.Parameter("@version", type=parse_version_string, num="?") - request = xml.Parameter("local-name()", type=upper) - acceptversions = xml.Parameter( - "ows10:AcceptVersions/ows10:Version/text() " - "| ows11:AcceptVersions/ows11:Version/text() " - "| ows20:AcceptVersions/ows20:Version/text()", - type=parse_version_string, num="*" - ) - - namespaces = nsmap diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/interfaces.py eoxserver-0.3.2/eoxserver/services/ows/interfaces.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/interfaces.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/interfaces.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,113 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2011 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -class ServiceHandlerInterface(object): - """ Interface for OWS Service handlers. - """ - - @property - def service(self): - """ The name of the supported service in uppercase letters. This can - also be an iterable, if the handler shall support more than one - service specifier. - Some service specifications demand that the service parameter can be - omitted for certain requests. In this case this property can alse be - ``None`` or contain ``None``. - """ - - @property - def versions(self): - """ An iterable of all supported versions as strings. - """ - - @property - def request(self): - """ The supported request method. - """ - - def handle(self, request): - """ The main handling method. Takes a `django.http.Request` object as - single parameter. - """ - - @property - def constraints(self): - """ Optional property to return a dict with constraints for default - values. - """ - - @property - def index(self): - """ Optional. The index this service handler shall have when being - reported in a capabilities document. - """ - - -class ExceptionHandlerInterface(object): - """ Interface for OWS exception handlers. - """ - - @property - def service(self): - """ The name of the supported service in uppercase letters. This can - also be an iterable, if the handler shall support more than one - service specifier. - Some service specifications demand that the service parameter can be - omitted for certain requests. In this case this property can alse be - ``None`` or contain ``None``. - """ - - @property - def versions(self): - """ An iterable of all supported versions as strings. - """ - - @property - def request(self): - """ The supported request method. - """ - - def handle_exception(self, request, exception): - """ The main exception handling method. Parameters are an object of the - `django.http.Request` type and the raised exception. - """ - - -class GetServiceHandlerInterface(ServiceHandlerInterface): - """ Interface for service handlers that support HTTP GET requests. - """ - - -class PostServiceHandlerInterface(ServiceHandlerInterface): - """ Interface for service handlers that support HTTP POST requests. - """ - - -class VersionNegotiationInterface(ServiceHandlerInterface): - """ Interface for handlers that contribute to version negotiation. - """ diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/version.py eoxserver-0.3.2/eoxserver/services/ows/version.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/version.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/version.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,133 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -from functools import wraps -try: - from functools import total_ordering -except ImportError: - from eoxserver.core.util.functools import total_ordering - - -__all__ = ["parse_version_string", "Version"] - - -def parse_version_string(version_string): - """ Convenience function to parse a version from a string. - """ - return Version(*map(int, version_string.split("."))) - - -def convert_to_version(f): - """ Decorator to automatically parse a ``Version`` from a string. - """ - - @wraps(f) - def wrapper(self, other): - if isinstance(other, Version): - return f(self, other) - elif isinstance(other, basestring): - return f(self, parse_version_string(other)) - try: - return f(self, Version(*other)) - except TypeError: - pass - raise TypeError("Cannot convert '%s' to version" % type(other),__name__) - return wrapper - - -@total_ordering -class Version(object): - """ Abstraction for OWS versions. Must be in the form 'x.y(.z)', where all - components must be positive integers or zero. The last component may be - unspecified (None). - - Versions can be compared with other versions. Strings and tuples of the - correct layout are also compareable. - - Versions are compared by the "major" and the "minor" number. Only if - both versions provide a "revision" it is taken into account. So Versions - "1.0" and "1.0.1" are considered equal! - """ - - def __init__(self, major, minor, revision=None): - try: - assert(isinstance(major, int) and major >= 0) - assert(isinstance(minor, int) and minor >= 0) - assert(revision is None - or (isinstance(revision, int) and revision >= 0)) - except AssertionError: - raise ValueError("Invalid version components supplied.") - - if revision is None: - self._values = (major, minor) - else: - self._values = (major, minor, revision) - - - @property - def major(self): - return self._values[0] - - @property - def minor(self): - return self._values[1] - - @property - def revision(self): - try: - return self._values[2] - except IndexError: - return None - - - @convert_to_version - def __eq__(self, other): - for self_v, other_v in zip(self._values, other._values): - if self_v != other_v: - return False - return True - - - @convert_to_version - def __lt__(self, other): - for self_v, other_v in zip(self._values, other._values): - if self_v < other_v: - return True - elif self_v > other_v: - return False - return False - - - def __str__(self): - return ".".join(map(str, self._values)) - - - def __repr__(self): - return '<%s.%s ("%s") instance at 0x%x>' % ( - __name__, type(self).__name__, str(self), id(self) - ) diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/basehandlers.py eoxserver-0.3.2/eoxserver/services/ows/wcs/basehandlers.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/basehandlers.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wcs/basehandlers.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,268 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -"""\ -This module contains a set of handler base classes which shall help to implement -a specific handler. Interface methods need to be overridden in order to work, -default methods can be overidden. -""" - -from eoxserver.core import ExtensionPoint -from eoxserver.resources.coverages import models -from eoxserver.services.result import to_http_response -from eoxserver.services.ows.wcs.parameters import WCSCapabilitiesRenderParams -from eoxserver.services.exceptions import ( - NoSuchCoverageException, OperationNotSupportedException -) -from eoxserver.services.ows.wcs.interfaces import ( - WCSCoverageDescriptionRendererInterface, WCSCoverageRendererInterface, - WCSCapabilitiesRendererInterface -) - - -class WCSGetCapabilitiesHandlerBase(object): - """ Base for Coverage description handlers. - """ - - service = "WCS" - request = "GetCapabilities" - - index = 0 - - renderers = ExtensionPoint(WCSCapabilitiesRendererInterface) - - def get_decoder(self, request): - """ Interface method to get the correct decoder for this request. - """ - - def lookup_coverages(self, decoder): - """ Default implementation of the coverage lookup. Simply returns all - coverages in no specific order. - """ - return models.Coverage.objects.filter(visible=True) \ - .order_by("identifier") - - def get_params(self, coverages, decoder): - """ Default method to return a render params object from the given - coverages/decoder. - """ - - return WCSCapabilitiesRenderParams(coverages, - getattr(decoder, "version", None), - getattr(decoder, "sections", None), - getattr(decoder, "acceptlanguages", None), - getattr(decoder, "acceptformats", None), - getattr(decoder, "updatesequence", None), - ) - - def get_renderer(self, params): - """ Default implementation for a renderer retrieval. - """ - for renderer in self.renderers: - if renderer.supports(params): - return renderer - - raise OperationNotSupportedException( - "No Capabilities renderer found for the given parameters.", - self.request - ) - - def to_http_response(self, result_set): - """ Default result to response conversion method. - """ - return to_http_response(result_set) - - - def handle(self, request): - """ Default handler method. - """ - - # parse the parameters - decoder = self.get_decoder(request) - - # get the coverages - coverages = self.lookup_coverages(decoder) - - # create the render params - params = self.get_params(coverages, decoder) - params.http_request = request - - # get the renderer - renderer = self.get_renderer(params) - - # dispatch the renderer and return the response - result_set = renderer.render(params) - return self.to_http_response(result_set) - - -class WCSDescribeCoverageHandlerBase(object): - """ Base for Coverage description handlers. - """ - - service = "WCS" - request = "DescribeCoverage" - - index = 1 - - renderers = ExtensionPoint(WCSCoverageDescriptionRendererInterface) - - def get_decoder(self, request): - """ Interface method to get the correct decoder for this request. - """ - - def lookup_coverages(self, decoder): - """ Default implementation of the coverage lookup. Returns a sorted list - of coverage models according to the decoders `coverage_ids` - attribute. Raises a `NoSuchCoverageException` if any of the given - IDs was not found in the database. - """ - ids = decoder.coverage_ids - coverages = sorted( - models.Coverage.objects.filter(identifier__in=ids), - key=(lambda coverage: ids.index(coverage.identifier)) - ) - - # check correct number - if len(coverages) < len(ids): - available_ids = set([coverage.identifier for coverage in coverages]) - raise NoSuchCoverageException(set(ids) - available_ids) - - return coverages - - def get_params(self, coverages, decoder): - """ Interface method to return a render params object from the given - coverages/decoder. - """ - - def get_renderer(self, params): - """ Default implementation for a renderer retrieval. - """ - for renderer in self.renderers: - if renderer.supports(params): - return renderer - - raise OperationNotSupportedException( - "No suitable coverage description renderer found.", - self.request - ) - - def to_http_response(self, result_set): - """ Default result to response conversion method. - """ - return to_http_response(result_set) - - - def handle(self, request): - """ Default request handling method implementation. - """ - # parse the parameters - decoder = self.get_decoder(request) - - # lookup the coverages - coverages = self.lookup_coverages(decoder) - - # create the render parameters - params = self.get_params(coverages, decoder) - - # find the correct renderer - renderer = self.get_renderer(params) - - # render and return the response - result_set = renderer.render(params) - return self.to_http_response(result_set) - - -class WCSGetCoverageHandlerBase(object): - """ Base for get coverage handlers. - """ - - service = "WCS" - request = "GetCoverage" - - index = 10 - - renderers = ExtensionPoint(WCSCoverageRendererInterface) - - def get_decoder(self, request): - """ Interface method to get the correct decoder for this request. - """ - - def lookup_coverage(self, decoder): - """ Default implementation of the coverage lookup. Returns the coverage - model for the given request decoder or raises an exception if it is - not found. - """ - coverage_id = decoder.coverage_id - - try: - coverage = models.Coverage.objects.get(identifier=coverage_id) - except models.Coverage.DoesNotExist: - raise NoSuchCoverageException((coverage_id,)) - - return coverage - - def get_params(self, coverages, decoder, request): - """ Interface method to return a render params object from the given - coverages/decoder. - """ - - def get_renderer(self, params): - """ Default implementation for a renderer retrieval. - """ - for renderer in self.renderers: - if renderer.supports(params): - return renderer - - raise OperationNotSupportedException( - "No renderer found for coverage '%s'." % params.coverage, - self.request - ) - - def to_http_response(self, result_set): - """ Default result to response conversion method. - """ - return to_http_response(result_set) - - def handle(self, request): - """ Default handling method implementation. - """ - - # parse the request - decoder = self.get_decoder(request) - - # get the coverage model - coverage = self.lookup_coverage(decoder) - - # create the render params - params = self.get_params(coverage, decoder, request) - - # get the renderer - renderer = self.get_renderer(params) - - # render the coverage and return the response - result_set = renderer.render(params) - return self.to_http_response(result_set) diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/common.py eoxserver-0.3.2/eoxserver/services/ows/wcs/common.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/common.py 1970-01-01 00:00:00.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wcs/common.py 2013-12-10 14:57:00.000000000 +0000 @@ -0,0 +1,331 @@ +#------------------------------------------------------------------------------- +# $Id$ +# +# Project: EOxServer +# Authors: Stephan Krause +# Stephan Meissl +# +#------------------------------------------------------------------------------- +# Copyright (C) 2011 EOX IT Services GmbH +# +# 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 of this Software or works derived from this 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. +#------------------------------------------------------------------------------- + +""" +This module contains handlers and functions commonly used by the different WCS +version implementations. +""" + +import datetime +from urllib import unquote + +import mapscript + +from eoxserver.core.exceptions import InternalError + +from eoxserver.services.owscommon import OWSCommonConfigReader +from eoxserver.services.mapserver import ( + MapServerOperationHandler, gdalconst_to_imagemode +) +from eoxserver.services.exceptions import InvalidRequestException + +from eoxserver.resources.coverages.formats import getFormatRegistry +from eoxserver.resources.coverages import crss + +_stripDot = lambda s : s[1:] if s[0] == '.' else s + +class WCSCommonHandler(MapServerOperationHandler): + """ + This class provides the common operation handler for handling WCS + operation requests using MapServer. It inherits from + :class:`~.MapServerOperationHandler`. + + The class implements a handling chain: + + * first, the request parameters are validated using :meth:`validateParams` + * then, the coverage(s) the request relates to are retrieved using + :meth:`createCoverages` + * then, the :class:`mapscript.OWSRequest` and :class:`mapscript.mapObj` + instances are configured using :meth:`configureRequest` and + :meth:`configureMapObj` + * then the layers are added using :meth:`addLayers` + * then the request is carried out by MapServer using :meth:`dispatch` + * finally, postprocessing steps on the response retrieved from MapServer + can be performed using :meth:`postprocess` + """ + def __init__(self): + super(WCSCommonHandler, self).__init__() + + self.coverages = [] + + def _processRequest(self, req): + self.req = req + self.req.setSchema(self.PARAM_SCHEMA) + + try: + self.validateParams() + self.createCoverages() + self.configureRequest() + self.configureMapObj() + self.addLayers() + response = self.postprocess(self.dispatch()) + finally: + self.cleanup() + + return response + + def validateParams(self): + """ + This method is intended to validate the parameters. It has to be + overridden by child classes. + """ + pass + + def createCoverages(self): + """ + This method creates coverages, i.e. it adds coverage objects to + the ``coverages`` list of the handler. It has to be overridden by + child classes. + """ + pass + + def configureMapObj(self): + """ + This method configures the ``map`` property of the handler (an + instance of :class:`mapscript.mapObj`) with parameters from the + config. This method can be overridden in order to implement more + sophisticated behaviour. + """ + + self.map.setMetaData("ows_onlineresource", OWSCommonConfigReader().getHTTPServiceURL() + "?") + self.map.setMetaData("wcs_label", "EOxServer WCS") + + def addLayers(self): + """ + This method adds layers to the :class:`mapscript.mapObj` stored by the + handler. By default it inserts a layer for every coverage. The layers + are retrieved by calls to :meth:`getMapServerLayer`. + """ + for coverage in self.coverages: + self.map.insertLayer(self.getMapServerLayer(coverage)) + + def getMapServerLayer(self, coverage): + """ + This method creates and returns a :class:`mapscript.layerObj` instance + and configures it according to the metadata stored in the ``coverage`` + object. + """ + layer = mapscript.layerObj() + + layer.name = coverage.getCoverageId() + layer.setMetaData("ows_title", coverage.getCoverageId()) + layer.status = mapscript.MS_ON + + for key, value in coverage.getLayerMetadata(): + layer.setMetaData(key, value) + + extent = coverage.getExtent() + size = coverage.getSize() + rangetype = coverage.getRangeType() + resolution = ((extent[2]-extent[0]) / float(size[0]), + (extent[1]-extent[3]) / float(size[1])) + + layer.setMetaData("wcs_extent", "%.10g %.10g %.10g %.10g" % extent) + layer.setMetaData("wcs_resolution", "%.10g %.10g" % resolution) + layer.setMetaData("wcs_size", "%d %d" % size) + + layer.type = mapscript.MS_LAYER_RASTER + layer.dump = mapscript.MS_TRUE + layer.setConnectionType(mapscript.MS_RASTER, '') + + layer.setMetaData("wcs_label", coverage.getCoverageId()) + + layer.setExtent(*coverage.getExtent()) + + return layer + + def postprocess(self, resp): + """ + This method postprocesses a :class:`~.MapServerResponse` object + ``resp``. By default the response is returned unchanged. The method + can be overridden by child classes. + """ + return resp + +def getMSOutputFormatsAll( coverage = None ) : + """ Setup all the supported MapServer output formats. + When the coverage parameter is provided than the + range type is used to setup format's image mode.""" + + # set image mode + if coverage is not None : + # set image mode based on the coverage's range type + im = gdalconst_to_imagemode( coverage.getRangeType().data_type ) + else : + # default + im = None + + # retrieve the format registry + FormatRegistry = getFormatRegistry() + + # get the supported formats + sfs = FormatRegistry.getSupportedFormatsWCS() + + #prepare list of output formats + ofs = [] + + for sf in sfs : + + # output format definition + of = mapscript.outputFormatObj( sf.driver, "custom" ) + of.name = sf.mimeType + of.mimetype = sf.mimeType + of.extension = _stripDot( sf.defaultExt ) + if im is not None : + of.imagemode = im + + ofs.append( of ) + + return ofs + +def getWCSNativeFormat( source_mime ): + + # retrieve the format registry + FormatRegistry = getFormatRegistry() + + # get the coverage's source format + source_format = FormatRegistry.getFormatByMIME( source_mime ) + + # map the source format to the native one + native_format = FormatRegistry.mapSourceToNativeWCS20( source_format ) + + #return native format mime + return native_format + + +def getMSWCSNativeFormat( source_mime ): + return getWCSNativeFormat( source_mime ).mimeType + +def getMSWCS10NativeFormat( source_mime ): + return getWCSNativeFormat( source_mime ).wcs10name + +def getMSWCSSRSMD(): + """ get the space separated list of CRS EPSG codes to be passed + to MapScript setMedata("wcs_srs",...) method """ + return " ".join(crss.getSupportedCRS_WCS(format_function=crss.asShortCode)) + +def getMSWCSFormatMD(): + """ get the space separated list of supported formats to be passed + to MapScript setMedata("wcs_formats",...) method """ + + # retrieve the format registry + FormatRegistry = getFormatRegistry() + + # get format record + frm = FormatRegistry.getSupportedFormatsWCS() + + return " ".join( map( lambda f : f.mimeType , frm ) ) + +def getMSWCS10FormatMD(): + """ get the space separated list of supported formats to be passed + to MapScript setMedata("wcs_formats",...) method """ + + # retrieve the format registry + FormatRegistry = getFormatRegistry() + + # get format record + frm = FormatRegistry.getSupportedFormatsWCS() + + return " ".join( map( lambda f : f.wcs10name , frm ) ) + +def getMSOutputFormat(format_param, coverage): + + #split MIME type to base MIME and format specific options + mime_type, format_options = parse_format_param(format_param) + + # get coverage range type + rangetype = coverage.getRangeType() + + # retrieve the format registry + FormatRegistry = getFormatRegistry() + + # get format record + frm = FormatRegistry.getFormatByMIME( mime_type ) + + # check also WCS 1.0 name (this time 'mime_type' is not really a MIME-type) + if frm is None : + frms = FormatRegistry.getFormatsByWCS10Name( mime_type ) + frm = frms[0] if len( frms ) > 0 else None + + # check that the format is among the supported formats + if ( frm not in FormatRegistry.getSupportedFormatsWCS() ) : + sf = map( lambda f : f.mimeType , FormatRegistry.getSupportedFormatsWCS() ) + raise InvalidRequestException( + "Unsupported format '%s'!" % ( mime_type ) , + "InvalidParameterValue", "format" ) + + # check the driver + if frm.driver.partition("/")[0] != "GDAL" : + raise InternalError( "Unsupported format backend \"%s\"!" % frm.driver.partition("/")[0] ) + + # output format definition + output_format = mapscript.outputFormatObj( frm.driver, "custom" ) + output_format.mimetype = frm.mimeType + output_format.extension = _stripDot( frm.defaultExt ) + output_format.imagemode = gdalconst_to_imagemode( rangetype.data_type ) + + # format specific options + for fo in format_options: + key, value = map( lambda s : str(s.strip()) , fo.split("=") ) + output_format.setOption( key, value ) + + # set the response filename + + time_stamp = datetime.datetime.now().strftime("%Y%m%d%H%M%S") + + filename = "%s_%s%s" % ( coverage.getCoverageId(), time_stamp, frm.defaultExt ) + + output_format.setOption( "FILENAME", str(filename) ) + + return output_format + + +def parse_format_param(format_param): + """ + This utility function is used to parse a MIME type expression + ``format_param`` into its parts. It returns a tuple + ``(mime_type, format_options)`` which contains the mime type as a string + as well as a list of format options. The input is expected as a MIME type + like string of the form:: + + /[;=[;...]] + + This is used for an EOxServer specific extension of the WCS format parameter + which allows to tag additional format creation options such as compression + and others to format expressions, e.g.:: + + image/tiff;compression=LZW + """ + parts = unquote(format_param).split(";") + + mime_type = parts[0] + + format_options = parts[1:] + + return (mime_type, format_options) diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/encoders.py eoxserver-0.3.2/eoxserver/services/ows/wcs/encoders.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/encoders.py 1970-01-01 00:00:00.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wcs/encoders.py 2013-12-10 14:57:00.000000000 +0000 @@ -0,0 +1,1125 @@ +#------------------------------------------------------------------------------- +# $Id$ +# +# Project: EOxServer +# Authors: Stephan Krause +# Stephan Meissl +# Martin Paces +# +#------------------------------------------------------------------------------- +# Copyright (C) 2011 EOX IT Services GmbH +# +# 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 of this Software or works derived from this 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. +#------------------------------------------------------------------------------- + +""" +This module contains XML encoders for WCS metadata based on GML, EO O&M, +GMLCOV, WCS 2.0 and EO-WCS. +""" + + +from datetime import datetime +from xml.dom import minidom + +from django.contrib.gis.geos import Polygon + +from eoxserver.core.util.xmltools import XMLEncoder +from eoxserver.core.util.timetools import isotime +from eoxserver.processing.mosaic import MosaicContribution + +from eoxserver.resources.coverages.formats import getFormatRegistry +from eoxserver.resources.coverages import crss + +# TODO make precision adjustable (3 decimal digits for projected axes) + +PPREC1=("%.8f","%.3f") +PPREC2=("%s %s"%(PPREC1[0],PPREC1[0]),"%s %s"%(PPREC1[1],PPREC1[1])) + +def _getUnitLabelAndFormat( epsg ) : + """ auxiliary function """ + + is_projected = crss.isProjected( epsg ) + is_reversed = crss.hasSwappedAxes( epsg ) + + if is_projected : + axes = "y x" if is_reversed else "x y" + unit = "m m" + else: + axes = "lat long" if is_reversed else "long lat" + unit = "deg deg" + + return unit , axes , PPREC2[is_projected] , is_reversed , is_projected + + +class GMLEncoder(XMLEncoder): + """ + This encoder provides methods for encoding basic GML objects. + + Note that the axis order for the input point coordinates used in + geometry representations is always + (x, y) or (lon, lat). The axis order in the output coordinates on the + other hand will be the order as mandated by the EPSG definition of + the respective spatial reference system. This may be (y, x) for some + projected CRSes (e.g. EPSG:3035, the European Lambert Azimuthal Equal + Area projection used for many datasets covering Europe) and (lat,lon) + for most geographic CRSes including EPSG:4326 (WGS 84). + """ + def _initializeNamespaces(self): + return { + "gml": "http://www.opengis.net/gml/3.2" + } + + def encodeLinearRing(self, ring, srid): + """ + Returns a :mod:`xml.dom.minidom` element containing the GML + representation of a linear ring. The ``ring`` argument is + expected to be a list of tuples which represent 2-D point coordinates + with (x,y)/(lon,lat) axis order. The ``srid`` argument shall contain the + EPSG ID of the spatial reference system as an integer. + """ + + floatFormat = PPREC2[ crss.isProjected(srid) ] + + # get axes swapping function + swap = crss.getAxesSwapper( srid ) + + pos_list = " ".join([ floatFormat%swap(*point) for point in ring]) + + return self._makeElement( + "gml", "LinearRing", [ + ("gml", "posList", pos_list) + ] + ) + + def encodePolygon(self, poly, base_id): + """ + This method returns a :mod:`xml.dom.minidom` element containing the GML + representation of a polygon. The ``poly`` argument is expected to be a + GeoDjango :class:`~django.contrib.gis.geos.Polygon` or + :class:`~django.contrib.gis.geos.GEOSGeometry` object containing a + polygon. The ``base_id`` string is used to generate the required gml:id + attributes on different elements of the polygon encoding. + """ + ext_element = self.encodeLinearRing(poly[0], poly.srid) + + if len(poly) > 1: + int_elements = [("gml", "interior", [(self.encodeLinearRing(ring, poly.srid),)]) for ring in poly[1:]] + else: + int_elements = [] + + sub_elements = [ + ("@gml", "id", "polygon_%s" % base_id), + ("gml", "exterior", [(ext_element,)]) + ] + sub_elements.extend(int_elements) + + return self._makeElement( + "gml", "Polygon", sub_elements + ) + + def encodeMultiPolygon(self, geom, base_id): + """ + This method returns a :mod:`xml.dom.minidom` element containing the GML + represenation of a multipolygon. The ``geom`` argument is expected to be + a GeoDjango :class:`~django.contrib.gis.geos.GEOSGeometry` object. The + ``base_id`` string is used to generate the required gml:id attributes + on different elements of the multipolygon encoding. + """ + if geom.geom_type in ("MultiPolygon", "GeometryCollection"): + polygons = [self.encodePolygon(geom[c], "%s_%d" % (base_id, c+1)) for c in range(0, geom.num_geom)] + elif geom.geom_type == "Polygon": + polygons = [self.encodePolygon(geom, base_id)] + + + + sub_elements = [ + ("@gml", "id", "multisurface_%s" % base_id), + ("@", "srsName", "EPSG:4326") + ] + sub_elements.extend([ + ("gml", "surfaceMember", [ + (poly_element,) + ]) for poly_element in polygons + ]) + + return self._makeElement( + "gml", "MultiSurface", sub_elements + ) + +class EOPEncoder(GMLEncoder): + """ + This encoder implements some encodings of EO O&M. It inherits from + :class:`GMLEncoder`. + """ + def _initializeNamespaces(self): + ns_dict = super(EOPEncoder, self)._initializeNamespaces() + ns_dict.update({ + "om": "http://www.opengis.net/om/2.0", + "eop": "http://www.opengis.net/eop/2.0" + }) + return ns_dict + + def encodeFootprint(self, footprint, eo_id): + """ + Returns a :mod:`xml.dom.minidom` element containing the EO O&M + representation of a footprint. The ``footprint`` argument shall contain + a GeoDjango :class:`~django.contrib.gis.geos.GEOSGeometry` object + containing the footprint as a polygon or multipolygon. The ``eo_id`` + argument is passed on to the GML encoder as a base ID for generating + required gml:id attributes. + """ + return self._makeElement( + "eop", "Footprint", [ + ("@gml", "id", "footprint_%s" % eo_id), + ("eop", "multiExtentOf", [ + (self.encodeMultiPolygon(footprint, eo_id),) + ]) + ] + ) + + def encodeMetadataProperty(self, eo_id, contributing_datasets=None): + """ + This method returns a :mod:`xml.dom.minidom` element containing the + EO O&M representation of an eop:metaDataProperty element. + + The ``eo_id`` element is reported in the eop:identifier element. If + provided, a list of ``contributing_datasets`` descriptions will be + included in the eop:composedOf element. + """ + sub_elements = [ + ("eop", "identifier", eo_id), + ("eop", "acquisitionType", "NOMINAL"), # TODO + ("eop", "status", "ARCHIVED") # TODO + ] + + if contributing_datasets is not None: + sub_elements.append( + ("eop", "composedOf", contributing_datasets) + ) + + return self._makeElement( + "eop", "metaDataProperty", [ + ("eop", "EarthObservationMetaData", sub_elements) + ] + ) + + def encodeEarthObservation(self, eo_metadata, contributing_datasets=None, poly=None): + """ + This method returns a :mod:`xml.dom.minidom` element containing the + EO O&M representation of an Earth Observation. It takes an + ``eo_metadata`` object as an input that implements the + :class:`~.EOMetadataInterface`. + + Note that the return value is only a minimal encoding with the + mandatory elements. + """ + eo_id = eo_metadata.getEOID() + begin_time_iso = isotime(eo_metadata.getBeginTime()) + end_time_iso = isotime(eo_metadata.getEndTime()) + result_time_iso = isotime(eo_metadata.getEndTime()) # TODO isotime(datetime.now()) + + footprint = None + if eo_metadata.getType() == "eo.rect_stitched_mosaic": + for ds in eo_metadata.getDatasets(): + if footprint is None: + footprint = ds.getFootprint() + else: + footprint = ds.getFootprint().union(footprint) + + else: + footprint = eo_metadata.getFootprint() + + if poly is not None: + footprint = footprint.intersection(poly) + + return self._makeElement( + "eop", "EarthObservation", [ + ("@gml", "id", "eop_%s" % eo_id), + ("om", "phenomenonTime", [ + ("gml", "TimePeriod", [ + ("@gml", "id", "phen_time_%s" % eo_id), + ("gml", "beginPosition", begin_time_iso), + ("gml", "endPosition", end_time_iso) + ]) + ]), + ("om", "resultTime", [ + ("gml", "TimeInstant", [ + ("@gml", "id", "res_time_%s" % eo_id), + ("gml", "timePosition", result_time_iso) + ]) + ]), + ("om", "procedure", []), + ("om", "observedProperty", []), + ("om", "featureOfInterest", [ + (self.encodeFootprint(footprint, eo_id),) + ]), + ("om", "result", []), + (self.encodeMetadataProperty(eo_id, contributing_datasets),) + ] + ) + +class CoverageGML10Encoder(XMLEncoder): + """ + This encoder provides methods for obtaining GMLCOV 1.0 compliant XML + encodings of coverage descriptions. + """ + def _initializeNamespaces(self): + return { + "gml": "http://www.opengis.net/gml/3.2", + "gmlcov": "http://www.opengis.net/gmlcov/1.0", + "swe": "http://www.opengis.net/swe/2.0" + } + + def _getGMLId(self, id): + if str(id)[0].isdigit(): + return "gmlid_%s" % str(id) + else: + return id + + def encodeDomainSet(self, coverage): + """ + This method returns a :mod:`xml.dom.minidom` element containing the + GMLCOV represenation of the domain set for rectified or referenceable + coverages. The ``coverage`` argument is expected to implement + :class:`~.EOCoverageInterface`. + + The domain set can be represented by either a referenceable or + a rectified grid; :meth:`encodeReferenceableGrid` or + :meth:`encodeRectifiedGrid` are called accordingly. + """ + if coverage.getType() == "eo.ref_dataset": + return self._makeElement("gml", "domainSet", [ + (self.encodeReferenceableGrid( coverage.getSize(), + coverage.getSRID(), + "%s_grid" % coverage.getCoverageId() + ),) + ]) + else: + return self._makeElement("gml", "domainSet", [ + (self.encodeRectifiedGrid( coverage.getSize(), + coverage.getExtent(), coverage.getSRID(), + "%s_grid" % coverage.getCoverageId() + ),) + ]) + + def encodeSubsetDomainSet(self, coverage, srid, size, extent): + """ + This method returns a :mod:`xml.dom.minidom` element containing the + GMLCOV representation of a domain set for subsets of rectified or + referenceable coverages. Whereas :meth:`encodeDomainSet` + computes the grid metadata based on the spatial reference system, extent + and pixel size of the whole coverage, this method can be customized + with parameters related to a subset of the coverage. + + The method expects four parameters: ``coverage`` shall be an object + implementing :class:`~.EOCoverageInterface`; ``srid`` shall be the + EPSG ID of the subset CRS (which does not have to be the same as the + coverage CRS); ``size`` shall be a 2-tuple of width and height of the + subset; finally the ``extent`` shall be represented by a 4-tuple + ``(minx, miny, maxx, maxy)``. + + """ + if coverage.getType() == "eo.ref_dataset": + return self._makeElement("gml", "domainSet", [ + (self.encodeReferenceableGrid( size, srid, + "%s_grid" % coverage.getCoverageId() + ),) + ]) + else: + return self._makeElement("gml", "domainSet", [ + (self.encodeRectifiedGrid( size, extent, srid, + "%s_grid" % coverage.getCoverageId() + ),) + ]) + + + def encodeRectifiedGrid(self, size, (minx, miny, maxx, maxy), srid, id): + """ + This method returns a :mod:`xml.dom.minidom` element containing the + GMLCOV representation of a rectified grid. It expects + four parameters as input: ``size`` shall be a 2-tuple of width and + height of the subset; the extent shall be represented by a 4-tuple + ``(minx, miny, maxx, maxy)``; the ``srid`` shall contain the EPSG ID + of the spatial reference system; finally, the ``id`` string is used to + generate gml:id attributes on certain elements that require it. + """ + + axesUnits, axesLabels, floatFormat , axesReversed , crsProjected = \ + _getUnitLabelAndFormat( srid ) + + # get axes swapping function + swap = crss.getAxesSwapper( srid , axesReversed ) + + origin = floatFormat % swap( minx, maxy ) + x_offsets = floatFormat % swap( ( maxx - minx )/float( size[0] ) , 0 ) + y_offsets = floatFormat % swap( 0 , ( miny - maxy )/float( size[1] ) ) + + grid_element = self._makeElement("gml", "RectifiedGrid", [ + ("", "@dimension", 2), + ("@gml", "id", self._getGMLId(id)), + ("gml", "limits", [ + ("gml", "GridEnvelope", [ + ("gml", "low", "0 0"), + ("gml", "high", "%d %d" % (size[0]-1, size[1]-1)) + ]) + ]), + ("gml", "axisLabels", axesLabels) + ]) + + grid_element.appendChild(self._makeElement("gml", "origin", [ + ("gml", "Point", [ + ("", "@srsName", crss.asURL(srid) ), + ("@gml", "id", self._getGMLId("%s_origin" % id)), + ("gml", "pos", origin) + ]) + ])) + + grid_element.appendChild(self._makeElement("gml", "offsetVector", [ + ("", "@srsName", "http://www.opengis.net/def/crs/EPSG/0/%s" % srid), + ("", "@@", x_offsets) + ])) + grid_element.appendChild(self._makeElement("gml", "offsetVector", [ + ("", "@srsName", "http://www.opengis.net/def/crs/EPSG/0/%s" % srid), + ("", "@@", y_offsets) + ])) + + return grid_element + + + def encodeReferenceableGrid(self, size, srid, id): + """ + This method returns a :mod:`xml.dom.minidom` element containig the + GMLCOV representation of a referenceable grid. It expects + three parameters: ``size`` is a 2-tuple of width and height of the + grid, the ``srid`` is the EPSG ID of the spatial reference system + and the ``id`` string is used to generate gml:id attributes on + elements that require it. + + Note that the return value is a gml:ReferenceableGrid element that + actually does not exist in the GML standard. + + The reason is that EOxServer geo-references datasets using ground + control points (GCPs) provided with the dataset. With the current GML + implementations of gml:AbstractReferenceableGrid it is not possible + to specify only the GCPs in the description of the grid. You'd have to + calculate and encode the coordinates of every grid point instead. This + would blow up the XML descriptions of typical satellite scenes to + several 100 MB - which is clearly impractical. + + The current implementation returns a gml:RectifiedGrid pseudo-element + that is based on the gml:AbstractGrid structure and has about the + following structure:: + + + + + 0 0 + 999 999 + + + lon lat + + """ + axesUnits, axesLabels, floatFormat , axesReversed , crsProjected = \ + _getUnitLabelAndFormat( srid ) + + grid_element = self._makeElement("gml", "ReferenceableGrid", [ + ("", "@dimension", 2), + ("@gml", "id", self._getGMLId(id)), + ("gml", "limits", [ + ("gml", "GridEnvelope", [ + ("gml", "low", "0 0"), + ("gml", "high", "%d %d" % (size[0]-1, size[1]-1)) + ]) + ]), + ("gml", "axisLabels", axesLabels) + ]) + + return grid_element + + + def encodeBoundedBy(self, (minx, miny, maxx, maxy), srid = 4326 ): + """ + This method returns a :mod:`xml.dom.minidom` element representing the + gml:boundedBy element. It expects the extent as a 4-tuple + ``(minx, miny, maxx, maxy)``. The ``srid`` parameter is optional and + represents the EPSG ID of the spatial reference system as an integer; + default is 4326. + """ + axesUnits, axesLabels, floatFormat , axesReversed , crsProjected = \ + _getUnitLabelAndFormat( srid ) + + # get axes swapping function + swap = crss.getAxesSwapper( srid , axesReversed ) + + return self._makeElement("gml", "boundedBy", [ + ("gml", "Envelope", [ + ("", "@srsName", crss.asURL( srid ) ), + ("", "@axisLabels", axesLabels ), + ("", "@uomLabels", axesUnits ), + ("", "@srsDimension", 2), + ("gml", "lowerCorner", floatFormat % swap(minx, miny) ), + ("gml", "upperCorner", floatFormat % swap(maxx, maxy) ) + ]) + ]) + + def encodeRangeType(self, coverage): + """ + This method returns the range type XML encoding based on GMLCOV and + SWE Common as an :mod:`xml.dom.minidom` element. The ``coverage`` + parameter shall implement :class:`~.EOCoverageInterface`. + """ + range_type = coverage.getRangeType() + + return self._makeElement("gmlcov", "rangeType", [ + ("swe", "DataRecord", [ + (self.encodeRangeTypeField(range_type, band),) + for band in range_type.bands + ]) + ]) + + def encodeRangeTypeField(self, range_type, band): + """ + This method returns the the encoding of a SWE Common field as an + :mod:`xml.dom.minidom` element. This XML structure represents a band in + terms of typical EO data. The ``range_type`` parameter shall be a + :class:`~.RangeType` object, the ``band`` parameter a :class:`~.Band` + object. + """ + return self._makeElement("swe", "field", [ + ("", "@name", band.name), + ("swe", "Quantity", [ + ("", "@definition", band.definition), + ("swe", "description", band.description), +# TODO: Not in sweCommon anymore +# ("swe", "name", band.name), + ("swe", "nilValues", [(self.encodeNilValue(nil_value),) for nil_value in band.nil_values]), + ("swe", "uom", [ + ("", "@code", band.uom) + ]), + ("swe", "constraint", [ + ("swe", "AllowedValues", [ + ("swe", "interval", "%s %s" % range_type.getAllowedValues()), + ("swe", "significantFigures", range_type.getSignificantFigures()) + ]) + ]) + ]) + ]) + + def encodeNilValue(self, nil_value): + """ + This method returns the SWE Common encoding of a nil value as an + :mod:`xml.dom.minidom` element; the input parameter shall be of type + :class:`~.NilValue`. + """ + return self._makeElement("swe", "NilValues", [ + ("swe", "nilValue", [ + ("", "@reason", nil_value.reason), + ("", "@@", nil_value.value) + ]) + ]) + + +class WCS20Encoder(CoverageGML10Encoder): + """ + This encoder class provides methods for generating XML needed by WCS 2.0. + It inherits from :class:`CoverageGML10Encoder`. + """ + def _initializeNamespaces(self): + ns_dict = super(WCS20Encoder, self)._initializeNamespaces() + ns_dict.update({ + "wcs": "http://www.opengis.net/wcs/2.0", + "xsi": "http://www.w3.org/2001/XMLSchema-instance" + }) + return ns_dict + + def encodeExtension(self): + """ + Returns an empty wcs:Extension element as an :mod:`xml.dom.minidom` + element. + """ + return self._makeElement("wcs", "Extension", []) + + def encodeCoverageDescription(self, coverage): + """ + Returns a :mod:`xml.dom.minidom` element representing a coverage + description. The method expects one parameter, ``coverage``, which + shall implement the :class:`~.EOCoverageInterface`. + """ + return self._makeElement("wcs", "CoverageDescription", [ + ("@gml", "id", self._getGMLId(coverage.getCoverageId())), + (self.encodeBoundedBy(coverage.getWGS84Extent()),), + ("wcs", "CoverageId", coverage.getCoverageId()), + (self.encodeDomainSet(coverage),), + (self.encodeRangeType(coverage),), + ("wcs", "ServiceParameters", [ + ("wcs", "CoverageSubtype", coverage.getCoverageSubtype()), + ]) + ]) + + def encodeCoverageDescriptions(self, coverages, is_root=False): + """ + Returns a :mod:`xml.dom.minidom` element representing a + wcs:CoverageDescriptions element. The ``coverages`` argument shall be + a list of objects implementing :class:`~.EOCoverageInterface` whereas + the optional ``is_root`` flag indicates that the element will be the + document root and thus should include an xsi:schemaLocation attribute + pointing to the EO-WCS schema; it defaults to ``False``. + """ + if is_root: + sub_nodes = [("@xsi", "schemaLocation", "http://www.opengis.net/wcseo/1.0 http://schemas.opengis.net/wcseo/1.0/wcsEOAll.xsd")] + else: + sub_nodes = [] + + if coverages is not None and len(coverages) != 0: + sub_nodes.extend([(self.encodeCoverageDescription(coverage),) for coverage in coverages]) + + return self._makeElement("wcs", "CoverageDescriptions", sub_nodes) + + +class WCS20EOAPEncoder(WCS20Encoder): + """ + This encoder provides methods for generating EO-WCS compliant XML + descriptions. + """ + def _initializeNamespaces(self): + ns_dict = super(WCS20EOAPEncoder, self)._initializeNamespaces() + ns_dict.update({ + "ows": "http://www.opengis.net/ows/2.0", + "crs": "http://www.opengis.net/wcs/service-extension/crs/1.0", + "wcseo": "http://www.opengis.net/wcseo/1.0", + "xlink": "http://www.w3.org/1999/xlink" + }) + return ns_dict + + def encodeContributingDatasets(self, coverage, poly=None): + """ + This method returns a list of :mod:`xml.dom.minidom` elements containing + wcseo:dataset descriptions of contributing datasets. This is used for + coverage descriptions of Rectified Stitched Mosaics. The ``coverage`` + parameter shall refer to a :class:`~.RectifiedStitchedMosaicWrapper` + object. The optional ``poly`` argument may contain a GeoDjango + :class:`~django.contrib.gis.geos.GEOSGeometry` object describing the + polygon. If it is provided, the set of contributing datasets will be + restricted to those intersecting the given polygon. + """ + eop_encoder = EOPEncoder() + + contributions = MosaicContribution.getContributions(coverage, poly) + + return [ + (self._makeElement( + "wcseo", "dataset", [ + ("wcs", "CoverageId", contribution.dataset.getCoverageId()), + ("wcseo", "contributingFootprint", eop_encoder.encodeFootprint(contribution.contributing_footprint, contribution.dataset.getEOID())) + ] + ),) + for contribution in contributions + ] + + def encodeEOMetadata(self, coverage, req=None, include_composed_of=False, poly=None): # TODO include_composed_of and poly are currently ignored + """ + This method returns a :mod:`xml.dom.minidom` element containing the + EO Metadata description of a coverage as needed for EO-WCS descriptions. + The method requires one argument, ``coverage``, that shall implement + :class:`~.EOCoverageInterface`. + + Moreover, a :class:`~.OWSRequest` object ``req`` can be provided. If it + is present, a wcseo:lineage element that describes the request + arguments will be added to the metadata description. + + The ``include_composed_of`` and ``poly`` arguments are ignored at the + moment. + """ + + poly_intersection = None + if poly is not None: + poly_intersection = coverage.getFootprint().intersection(poly) + + eop_encoder = EOPEncoder() + + if coverage.getEOGML(): + dom = minidom.parseString(coverage.getEOGML()) + earth_observation = dom.documentElement + if poly_intersection is not None: + new_footprint = eop_encoder.encodeFootprint(poly_intersection, coverage.getEOID()) + old_footprint = dom.getElementsByTagNameNS(eop_encoder.ns_dict["eop"], "Footprint")[0] + old_footprint.parentNode.replaceChild(new_footprint, old_footprint) + else: + earth_observation = eop_encoder.encodeEarthObservation(coverage, poly=poly_intersection) + + if req is None: + lineage = None + else: + if req.getParamType() == "kvp": + lineage = self._makeElement( + "wcseo", "lineage", [ + ("wcseo", "referenceGetCoverage", [ + ("ows", "Reference", [ + ("@xlink", "href", req.http_req.build_absolute_uri().replace("&", "&")) + ]) + ]), + ("gml", "timePosition", isotime(datetime.now())) + ] + ) + elif req.getParamType() == "xml": + post_dom = minidom.parseString(req.params) + post_xml = post_dom.documentElement + + lineage = self._makeElement( + "wcseo", "lineage", [ + ("wcseo", "referenceGetCoverage", [ + ("ows", "ServiceReference", [ + ("@xlink", "href", req.http_req.build_absolute_uri()), + ("ows", "RequestMessage", post_xml) + ]) + ]), + ("gml", "timePosition", isotime(datetime.now())) + ] + ) + else: + lineage = None + + if lineage is None: + return self._makeElement("gmlcov", "metadata", [ + ("gmlcov", "Extension", [ + ("wcseo", "EOMetadata", [ + (earth_observation,), + ]), + ]), + ]) + else: + return self._makeElement("gmlcov", "metadata", [ + ("gmlcov", "Extension", [ + ("wcseo", "EOMetadata", [ + (earth_observation,), + (lineage,) + ]), + ]), + ]) + + def encodeContents(self): + """ + Returns an empty wcs:Contents element as :mod:`xml.dom.minidom` + element. + """ + return self._makeElement("wcs", "Contents", []) + + def encodeCoverageSummary(self, coverage): + """ + This method returns a wcs:CoverageSummary element as + :mod:`xml.dom.minidom` element. It expects a ``coverage`` object + implementing :class:`~.EOCoverageInterface` as input. + """ + return self._makeElement("wcs", "CoverageSummary", [ + ("wcs", "CoverageId", coverage.getCoverageId()), + ("wcs", "CoverageSubtype", coverage.getEOCoverageSubtype()), + ]) + + def encodeCoverageDescription(self, coverage, is_root=False): + """ + This method returns a wcs:CoverageDescription element including + EO Metadata as :mod:`xml.dom.minidom` element. It expects one + mandatory argument, ``coverage``, which shall implement + :class:`~.EOCoverageInterface`. The optional ``is_root`` flag indicates + whether the returned element will be the document root of the + response. If yes, a xsi:schemaLocation attribute pointing to the + EO-WCS schema will be added to the root element. It defaults to + ``False``. + """ + + # retrieve the format registry + FormatRegistry = getFormatRegistry() + + # get the coverage's source format + source_mime = coverage.getData().getSourceFormat() + source_format = FormatRegistry.getFormatByMIME( source_mime ) + + # map the source format to the native one + native_format = FormatRegistry.mapSourceToNativeWCS20( source_format ) + + if is_root: + sub_nodes = [("@xsi", "schemaLocation", "http://www.opengis.net/wcseo/1.0 http://schemas.opengis.net/wcseo/1.0/wcsEOAll.xsd")] + else: + sub_nodes = [] + + sub_nodes.extend([ + ("@gml", "id", self._getGMLId(coverage.getCoverageId())), + (self.encodeBoundedBy(coverage.getExtent(),coverage.getSRID()),), + ("wcs", "CoverageId", coverage.getCoverageId()), + (self.encodeEOMetadata(coverage),), + (self.encodeDomainSet(coverage),), + (self.encodeRangeType(coverage),), + ("wcs", "ServiceParameters", [ + ("wcs", "CoverageSubtype", coverage.getEOCoverageSubtype()), + ("wcs", "nativeFormat" , native_format.mimeType ) + ]) + ]) + return self._makeElement("wcs", "CoverageDescription", sub_nodes) + + #TODO: remove once fully supported by mapserver + def encodeSupportedCRSs( self ) : + """ + This method returns list of :mod:`xml.dom.minidom` elements containing + the supported CRSes for a service. The CRSes are retrieved using + :func:`eoxserver.resources.coverages.crss.getSupportedCRS_WCS`. They + are encoded as crsSupported elements in the namespace of the WCS 2.0 + CRS extension. + """ + + # get list of supported CRSes + supported_crss = crss.getSupportedCRS_WCS(format_function=crss.asURL) + + el = [] + + for sc in supported_crss : + + el.append( self._makeElement( "crs" , "crsSupported" , sc ) ) + + return el + + def encodeRangeSet( self , reference , mimeType ) : + """ + This method returns a :mod:`xml.dom.minidom` element containing a + reference to the range set of the coverage. The ``reference`` parameter + shall refer to the file part of a multipart message. The ``mime_type`` + shall contain the MIME type of the delivered coverage. + """ + return self._makeElement("gml", "rangeSet", + [( "gml","File" , + [("gml","rangeParameters", + [( "@xlink" , "arcrole" , "fileReference" ), + ( "@xlink" , "href" , reference ), + ( "@xlink" , "role" , mimeType ), + ]), + ("gml","fileReference",reference), + ("gml","fileStructure",[]), + ("gml","mimeType",mimeType), + ]), + ]) + + def encodeReferenceableDataset( self , coverage , reference , mimeType , is_root = False , subset = None ) : + """ + This method returns the description of a Referenceable Dataset as a + :mod:`xml.dom.minidom` element. It expects three input arguments: + ``coverage`` shall be a :class:`~.ReferenceableDatasetWrapper` instance; + ``reference`` shall be a string containing a reference to the + coverage data; ``mime_type`` shall be a string containing the MIME type + of the coverage data. + + The ``is_root`` flag indicates that the returned element is the + document root and an xsi:schemaLocation attribute pointing to the + EO-WCS schemas shall be added. It defaults to ``False``. The + ``subset`` argument is optional. In case it is provided it indicates + that the description relates to a subset of the dataset only and thus + the metadata (domain set) shall be changed accordingly. It is expected + to be a 4-tuple of ``(srid, size, extent, footprint)``. The ``srid`` + represents the integer EPSG ID of the CRS description. The ``size`` + contains a 2-tuple of width and height. The ``extent`` is a 4-tuple + of ``(minx, miny, maxx, maxy)``; the coordinates shall be expressed + in the CRS denoted by ``srid``. The ``footprint`` part is not used. + """ + + # handle subset + dst_srid = coverage.getSRID() + + if subset is None : + # whole area - no subset + domain = self.encodeDomainSet(coverage) + eomd = self.encodeEOMetadata(coverage) + dst_extent = coverage.getExtent() + + else : + + # subset is given + _srid, size, _extent, footprint = subset + + domain = self.encodeSubsetDomainSet(coverage, _srid, size, _extent) + eomd = self.encodeEOMetadata(coverage, poly=footprint) + + # get the WGS84 extent + poly = Polygon.from_bbox(_extent) + poly.srid = _srid + poly.transform(dst_srid) + dst_extent = poly.extent + + sub_nodes = [] + + if is_root: + sub_nodes.append( ("@xsi", "schemaLocation", "http://www.opengis.net/wcseo/1.0 http://schemas.opengis.net/wcseo/1.0/wcsEOAll.xsd") ) + + sub_nodes.extend([ + ("@gml", "id", self._getGMLId(coverage.getCoverageId())), + (self.encodeBoundedBy(dst_extent,dst_srid),),(domain,), + (self.encodeRangeSet( reference , mimeType ),), + (self.encodeRangeType(coverage),),(eomd,),]) + + return self._makeElement("wcseo", "ReferenceableDataset", sub_nodes) + + + def encodeSubsetCoverageDescription(self, coverage, srid, size, extent, footprint, is_root=False): + """ + This method returns a :mod:`xml.dom.minidom` element containing a + coverage description for a subset of a coverage according to WCS 2.0. + The ``coverage`` parameter shall implement + :class:`~.EOCoverageInterface`. The ``srid`` shall contain the + integer EPSG ID of the output (subset) CRS. The ``size`` parameter + shall be a 2-tuple of width and height. The ``extent`` shall be a + 4-tuple of ``(minx, miny, maxx, maxy)`` expressed in the CRS described + by ``srid``. The ``footprint`` argument shall be a GeoDjango + :class:`~django.contrib.gis.geos.GEOSGeometry` object containing a + polygon. The ``is_root`` flag indicates whether the resulting + wcs:CoverageDescription element is the document root of the response. + In that case a xsi:schemaLocation attribute pointing to the EO-WCS + schema will be added. It defaults to ``False``. + """ + poly = Polygon.from_bbox(extent) + poly.srid = srid + poly.transform(4326) + + wgs84_extent = poly.extent + + if is_root: + sub_nodes = [("@xsi", "schemaLocation", "http://www.opengis.net/wcseo/1.0 http://schemas.opengis.net/wcseo/1.0/wcsEOAll.xsd")] + else: + sub_nodes = [] + sub_nodes.extend([ + ("@xsi", "schemaLocation", "http://www.opengis.net/wcseo/1.0 http://schemas.opengis.net/wcseo/1.0/wcsEOAll.xsd"), + ("@gml", "id", self._getGMLId(coverage.getCoverageId())), + (self.encodeBoundedBy(wgs84_extent),), + ("wcs", "CoverageId", coverage.getCoverageId()), + (self.encodeEOMetadata(coverage, poly=footprint),), + (self.encodeSubsetDomainSet(coverage, srid, size, extent),), + (self.encodeRangeType(coverage),), + ("wcs", "ServiceParameters", [ + ("wcs", "CoverageSubtype", coverage.getEOCoverageSubtype()), + ]) + ]) + return self._makeElement("wcs", "CoverageDescription", sub_nodes) + + def encodeDatasetSeriesDescription(self, dataset_series): + """ + This method returns a :mod:`xml.dom.minidom` element representing + a Dataset Series description. The method expects a + :class:`~.DatasetSeriesWrapper` object ``dataset_series`` as its + only input. + """ + return self._makeElement("wcseo", "DatasetSeriesDescription", [ + ("@gml", "id", self._getGMLId(dataset_series.getEOID())), + (self.encodeBoundedBy(dataset_series.getWGS84Extent()),), + ("wcseo", "DatasetSeriesId", dataset_series.getEOID()), + (self.encodeTimePeriod(dataset_series),), +# ("wcseo", "ServiceParameters", [ +# TODO: Include all referenced EO Coverages: +# ("wcseo", "rectifiedDataset", datasetseries.getCoverageSubtype()), +# ("wcseo", "referenceableDataset", datasetseries.getCoverageSubtype()), +# ("wcseo", "rectifiedStitchedMosaic", datasetseries.getCoverageSubtype()), +# ("wcseo", "referenceableStitchedMosaic", datasetseries.getCoverageSubtype()), +# ]) + ]) + + def encodeDatasetSeriesDescriptions(self, datasetseriess): + """ + This method returns a wcs:DatasetSeriesDescriptions element as a + :mod:`xml.dom.minidom` element. The element contains the + descriptions of a list of Dataset Series contained in the + ``datasetseriess`` parameter. + """ + if datasetseriess is not None and len(datasetseriess) != 0: + sub_nodes = [(self.encodeDatasetSeriesDescription(datasetseries),) for datasetseries in datasetseriess] + else: + sub_nodes = [] + return self._makeElement("wcseo", "DatasetSeriesDescriptions", sub_nodes) + + def encodeEOCoverageSetDescription(self, datasetseriess, coverages, numberMatched=None, numberReturned=None): + """ + This method returns a wcseo:EOCoverageSetDescription element (the + response to a EO-WCS DescribeEOCoverageSet request) as a + :mod:`xml.dom.minidom` element. + + ``datasetseriess`` shall be a list of :class:`~.DatasetSeriesWrapper` + objects. The ``coverages`` argument shall be a list of objects + implementing :class:`~.EOCoverageInterface`. The optional + ``numberMatched`` and ``numberReturned`` arguments are used in + responses for pagination. + """ + if numberMatched is None: + numberMatched = len(coverages) + if numberReturned is None: + numberReturned = len(coverages) + + root_element = self._makeElement("wcseo", "EOCoverageSetDescription", [ + ("@xsi", "schemaLocation", "http://www.opengis.net/wcseo/1.0 http://schemas.opengis.net/wcseo/1.0/wcsEOAll.xsd"), + ("", "@numberMatched", str(numberMatched)), + ("", "@numberReturned", str(numberReturned)), + ]) + + if coverages is not None and len(coverages) != 0: + root_element.appendChild(self.encodeCoverageDescriptions(coverages)) + if datasetseriess is not None and len(datasetseriess) != 0: + root_element.appendChild(self.encodeDatasetSeriesDescriptions(datasetseriess)) + + return root_element + + def encodeEOProfiles(self): + """ + Returns a list of ows:Profile elements referring to the WCS 2.0 profiles + implemented by EOxServer (EO-WCS and its GET KVP binding as well as + the CRS extension of WCS 2.0). The resulting :mod:`xml.dom.minidom` + elements can be used in WCS 2.0 GetCapabilities responses. + """ + return [self._makeElement("ows", "Profile", "http://www.opengis.net/spec/WCS_application-profile_earth-observation/1.0/conf/eowcs"), + self._makeElement("ows", "Profile", "http://www.opengis.net/spec/WCS_application-profile_earth-observation/1.0/conf/eowcs_get-kvp"), + self._makeElement("ows", "Profile", "http://www.opengis.net/spec/WCS_service-extension_crs/1.0/conf/crs")] #TODO remove once fully supported by mapserver + + def encodeDescribeEOCoverageSetOperation(self, http_service_url): + """ + This method returns an ows:Operation element describing the + additional EO-WCS DescribeEOCoverageSet operation for use in the + WCS 2.0 GetCapabilities response. The return value is - as always - + a :mod:`xml.dom.minidom` element. + + The only parameter is the HTTP service URL of the EOxServer instance. + """ + return self._makeElement("ows", "Operation", [ + ("", "@name", "DescribeEOCoverageSet"), + ("ows", "DCP", [ + ("ows", "HTTP", [ + ("ows", "Get", [ + ("@xlink", "href", "%s?" % http_service_url), + ("@xlink", "type", "simple") + ]), + ("ows", "Post", [ + ("@xlink", "href", "%s?" % http_service_url), + ("@xlink", "type", "simple"), + ("ows", "Constraint", [ + ("@", "name", "PostEncoding"), + ("ows", "AllowedValues", [ + ("ows", "Value", "XML") + ]) + ]) + ]) + ]) + ]) + ]) + + def encodeWGS84BoundingBox(self, dataset_series): + """ + This element returns the ows:WGS84BoundingBox for a Dataset Series. + The input parameter shall be a :class:`~.DatasetSeriesWrapper` object. + """ + minx, miny, maxx, maxy = dataset_series.getWGS84Extent() + + floatFormat = PPREC2[False] + + return self._makeElement("ows", "WGS84BoundingBox", [ + ("ows", "LowerCorner", floatFormat%(minx, miny)), + ("ows", "UpperCorner", floatFormat%(maxx, maxy)) + ]) + + def encodeTimePeriod(self, dataset_series): + """ + This method returns a gml:TimePeriod element referring to the + time period of a Dataset Series. The input argument is expected to + be a :class:`~.DatasetSeriesWrapper` object. + """ + timeFormat = "%Y-%m-%dT%H:%M:%S" + + teoid = "%s_timeperiod" % dataset_series.getEOID() + start = dataset_series.getBeginTime().strftime(timeFormat) + stop = dataset_series.getEndTime().strftime(timeFormat) + + return self._makeElement("gml", "TimePeriod", [ + ("@gml", "id", self._getGMLId(teoid) ), + ("gml", "beginPosition", start ), + ("gml", "endPosition", stop ) + ]) + + def encodeDatasetSeriesSummary(self, dataset_series): + """ + This method returns a wcseo:DatasetSeriesSummary element referring to + ``dataset_series``, a :class:`~.DatasetSeriesWrapper` object. + """ + return self._makeElement("wcseo", "DatasetSeriesSummary", [ + (self.encodeWGS84BoundingBox(dataset_series),), + ("wcseo", "DatasetSeriesId", dataset_series.getEOID()), + (self.encodeTimePeriod(dataset_series),) + ]) + + def encodeRectifiedDataset(self, dataset, req=None, nodes=None, poly=None): + """ + This method returns a wcseo:RectifiedDataset element describing the + ``dataset`` object of type :class:`~.RectifiedDatasetWrapper`. The + ``nodes`` parameter may contain a list of :mod:`xml.dom.minidom` nodes + to be appended to the root element. The ``req`` and ``poly`` arguments + are passed on to :meth:`encodeEOMetadata`. + """ + root_element = self._makeElement("wcseo", "RectifiedDataset", [ + ("@xsi", "schemaLocation", "http://www.opengis.net/wcseo/1.0 http://schemas.opengis.net/wcseo/1.0/wcsEOAll.xsd"), + ("@gml", "id", dataset.getCoverageId()) + ]) + + if nodes is not None: + for node in nodes: + root_element.appendChild(node.cloneNode(deep=True)) + #else: TODO + + root_element.appendChild(self.encodeEOMetadata(dataset, req, poly=poly)) + + return root_element + + def encodeRectifiedStitchedMosaic(self, mosaic, req=None, nodes=None, poly=None): + """ + This method returns a wcseo:RectifiedStitchedMosaic element describing + the ``mosaic`` object of type + :class:`~.RectifiedStitchedMosaicWrapper`. The ``nodes`` parameter may + contain a list of :mod:`xml.dom.minidom` nodes to be appended to the + root element. The ``req`` and ``poly`` arguments are passed on to + :meth:`encodeEOMetadata`. + """ + root_element = self._makeElement("wcseo", "RectifiedStitchedMosaic", [ + ("@xsi", "schemaLocation", "http://www.opengis.net/wcseo/1.0 http://schemas.opengis.net/wcseo/1.0/wcsEOAll.xsd"), + ("@gml", "id", mosaic.getCoverageId()) + ]) + + if nodes is not None: + for node in nodes: + root_element.appendChild(node.cloneNode(deep=True)) + #else: TODO + + root_element.appendChild(self.encodeEOMetadata(mosaic, req, poly=poly)) + + root_element.appendChild(self._makeElement( + "wcseo", "datasets", self.encodeContributingDatasets(mosaic, poly) + )) + + return root_element + + def encodeCountDefaultConstraint(self, count): + """ + This method returns a ows:Constraint element representing the default + maximum of descriptions in an EO-WCS DescribeEOCoverage response for use + in WCS 2.0 GetCapabilities responses. The ``count`` argument is + expected to contain a positive integer. + """ + return self._makeElement("ows", "Constraint", [ + ("", "@name", "CountDefault"), + ("ows", "NoValues", ""), + ("ows", "DefaultValue", count) + ]) diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/interfaces.py eoxserver-0.3.2/eoxserver/services/ows/wcs/interfaces.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/interfaces.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wcs/interfaces.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,115 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -class WCSCapabilitiesRendererInterface(object): - """ Interface for WCS Capabilities renderers. - """ - - def render(self, params): - """ Render the capabilities including information about the given - coverages. - """ - - def supports(self, params): - """ Returns a boolean value to indicate whether or not the renderer is - able to render the capabilities with the given parameters. - """ - - -class WCSCoverageDescriptionRendererInterface(object): - """ Interface for coverage description renderers. - """ - - def render(self, params): - """ Render the description of the given coverages. - """ - - def supports(self, params): - """ Returns a boolean value to indicate whether or not the renderer is - able to render the coverage and the given WCS version. - """ - - -class WCSCoverageRendererInterface(object): - """ Interface for coverage renderers. - """ - - def render(self, params): - """ Render the coverage with the given parameters. - """ - - def supports(self, params): - """ Returns a boolean value to indicate whether or not the renderer is - able to render the coverage with the given parameters. - """ - - -class PackageWriterInterface(object): - """ Interface for package writers. - """ - - def supports(self, format, params): - """ Return a boolen value, whether or not a writer supports a given - format. - """ - - def create_package(self, filename, format, params): - """ Create a package, which the encoder can later add items to with the - `cleanup` and `add_to_package` method. - """ - - def cleanup(self, package): - """ Perform any necessary cleanups, like closing files, etc. - """ - - def add_to_package(self, package, file_obj, size, location): - """ Add the file object to the package, that is returned by the - `create_package` method. - """ - - def get_mime_type(self, package, format, params): - """ Retrieve the output mime type for the given package and/or format - specifier. - """ - - def get_file_extension(self, package, format, params): - """ Retrieve the file extension for the given package and format - specifier. - """ - - -class EncodingExtensionInterface(object): - def supports(self, format, options): - """ Return a boolen value, whether or not an encoding extension - supports a given format. - """ - - def parse_encoding_params(self, request): - """ Return a dict, containing all additional encoding parameters from a - given request. - """ diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/parameters.py eoxserver-0.3.2/eoxserver/services/ows/wcs/parameters.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/parameters.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wcs/parameters.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,94 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -from eoxserver.services.parameters import ( - VersionedParams, CapabilitiesRenderParams -) - - -class WCSParamsMixIn(object): - def __iter__(self): - for k, v in super(WCSParamsMixIn, self).__iter__(): - yield k, v - - yield ("service", "WCS") - - -class WCSCapabilitiesRenderParams(WCSParamsMixIn, CapabilitiesRenderParams): - pass - - -class CoverageRenderParams(WCSParamsMixIn, VersionedParams): - def __init__(self, coverage, version): - super(CoverageRenderParams, self).__init__(version) - self._coverage = coverage - - - @property - def coverage(self): - return self._coverage - - - @property - def coverage_id(self): - return self.coverage.identifier - - - coverage_id_key_name = None - - def __iter__(self): - for k, v in super(CoverageRenderParams, self).__iter__(): - yield k, v - - yield ("request", "GetCoverage") - yield (self.coverage_id_key_name, self.coverage_id) - - -class CoverageDescriptionRenderParams(WCSParamsMixIn, VersionedParams): - def __init__(self, coverages, version): - super(CoverageDescriptionRenderParams, self).__init__(version) - self._coverages = coverages - - @property - def coverages(self): - return self._coverages - - @property - def coverage_ids(self): - return [coverage.identifier for coverage in self.coverages] - - - coverage_ids_key_name = None - - def __iter__(self): - for k, v in super(CoverageDescriptionRenderParams, self).__iter__(): - yield k, v - - yield ("request", "DescribeCoverage") - yield (self.coverage_ids_key_name, ",".join(self.coverage_ids)) - diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/v10/describecoverage.py eoxserver-0.3.2/eoxserver/services/ows/wcs/v10/describecoverage.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/v10/describecoverage.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wcs/v10/describecoverage.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,64 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -from eoxserver.core import Component, implements -from eoxserver.core.decoders import xml, kvp, typelist -from eoxserver.services.ows.interfaces import ( - ServiceHandlerInterface, GetServiceHandlerInterface, - PostServiceHandlerInterface -) -from eoxserver.services.ows.wcs.basehandlers import ( - WCSDescribeCoverageHandlerBase -) -from eoxserver.services.ows.wcs.v10.parameters import ( - WCS10CoverageDescriptionRenderParams -) - - -class WCS10DescribeCoverageHandler(WCSDescribeCoverageHandlerBase, Component): - implements(ServiceHandlerInterface) - implements(GetServiceHandlerInterface) - #implements(PostServiceHandlerInterface) - - versions = ("1.0.0",) - - def get_decoder(self, request): - if request.method == "GET": - return WCS10DescribeCoverageKVPDecoder(request.GET) - # TODO: implement POST - elif request.method == "POST": - #return WCS10GetCoverageXMLDecoder(request.body) - pass - - - def get_params(self, coverages, decoder): - return WCS10CoverageDescriptionRenderParams(coverages) - - -class WCS10DescribeCoverageKVPDecoder(kvp.Decoder): - coverage_ids = kvp.Parameter("coverage", type=typelist(str, ","), num=1) diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/v10/exceptionhandler.py eoxserver-0.3.2/eoxserver/services/ows/wcs/v10/exceptionhandler.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/v10/exceptionhandler.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wcs/v10/exceptionhandler.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,70 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2011 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -from eoxserver.core import Component, implements -from eoxserver.services.ows.interfaces import ExceptionHandlerInterface -from eoxserver.services.ows.common.v20.encoders import OWS20ExceptionXMLEncoder -from eoxserver.core.decoders import ( - DecodingException, MissingParameterException -) - - -class WCS10ExceptionHandler(Component): - implements(ExceptionHandlerInterface) - - service = "WCS" - versions = ("1.0.0",) - request = None - - def handle_exception(self, request, exception): - message = str(exception) - code = getattr(exception, "code", None) - locator = getattr(exception, "locator", None) - status = 400 - - - - # TODO - - - - if code is None: - if isinstance(exception, MissingParameterException): - code = "MissingParameterValue" - elif isinstance(exception, DecodingException): - code = "InvalidParameterValue" - else: - code = "InvalidRequest" - - if code in ("NoSuchCoverage", "InvalidAxisLabel", "InvalidSubsetting"): - status = 404 - elif code in ("OperationNotSupported", "OptionNotSupported"): - status = 501 - - - return message, 400 #content, content_type, status diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/v10/getcapabilities.py eoxserver-0.3.2/eoxserver/services/ows/wcs/v10/getcapabilities.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/v10/getcapabilities.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wcs/v10/getcapabilities.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,78 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2011 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -from eoxserver.core import Component, implements -from eoxserver.core.decoders import xml, kvp, typelist, lower -from eoxserver.services.ows.interfaces import ( - ServiceHandlerInterface, GetServiceHandlerInterface, - PostServiceHandlerInterface, VersionNegotiationInterface -) -from eoxserver.services.ows.wcs.basehandlers import ( - WCSGetCapabilitiesHandlerBase -) -from eoxserver.services.ows.wcs.parameters import WCSCapabilitiesRenderParams -from eoxserver.services.ows.wcs.v10.util import nsmap - - -class WCS10GetCapabilitiesHandler(WCSGetCapabilitiesHandlerBase, Component): - implements(ServiceHandlerInterface) - implements(GetServiceHandlerInterface) - implements(PostServiceHandlerInterface) - implements(VersionNegotiationInterface) - - versions = ("1.0.0",) - - def get_decoder(self, request): - if request.method == "GET": - return WCS10GetCapabilitiesKVPDecoder(request.GET) - elif request.method == "POST": - return WCS10GetCapabilitiesXMLDecoder(request.body) - - def get_params(self, coverages, decoder): - return WCSCapabilitiesRenderParams( - coverages, "1.0.0", decoder.sections, decoder.acceptlanguages, - decoder.acceptformats, decoder.updatesequence - ) - - -class WCS10GetCapabilitiesKVPDecoder(kvp.Decoder): - sections = kvp.Parameter(type=typelist(lower, ","), num="?") - updatesequence = kvp.Parameter(num="?") - acceptversions = kvp.Parameter(type=typelist(str, ","), num="?") - acceptformats = kvp.Parameter(type=typelist(str, ","), num="?", default=["text/xml"]) - acceptlanguages = kvp.Parameter(type=typelist(str, ","), num="?") - - -class WCS10GetCapabilitiesXMLDecoder(xml.Decoder): - sections = xml.Parameter("/ows:Sections/ows:Section/text()", num="*") - updatesequence = xml.Parameter("/@updateSequence", num="?") - acceptversions = xml.Parameter("/ows:AcceptVersions/ows:Version/text()", num="*") - acceptformats = xml.Parameter("/ows:AcceptFormats/ows:OutputFormat/text()", num="*", default=["text/xml"]) - acceptlanguages = xml.Parameter("/ows:AcceptLanguages/ows:Language/text()", num="*") - - namespaces = nsmap diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/v10/getcoverage.py eoxserver-0.3.2/eoxserver/services/ows/wcs/v10/getcoverage.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/v10/getcoverage.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wcs/v10/getcoverage.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,77 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -from eoxserver.core import Component, implements -from eoxserver.core.decoders import xml, kvp -from eoxserver.services.ows.interfaces import ( - ServiceHandlerInterface, GetServiceHandlerInterface, - PostServiceHandlerInterface -) -from eoxserver.services.ows.wcs.basehandlers import WCSGetCoverageHandlerBase -from eoxserver.services.ows.wcs.v10.parameters import WCS10CoverageRenderParams - - -class WCS10GetCoverageHandler(WCSGetCoverageHandlerBase, Component): - implements(ServiceHandlerInterface) - implements(GetServiceHandlerInterface) - #implements(PostServiceHandlerInterface) - - versions = ("1.0.0",) - - def get_decoder(self, request): - if request.method == "GET": - return WCS10GetCoverageKVPDecoder(request.GET) - # TODO: implement POST - elif request.method == "POST": - #return WCS10GetCoverageXMLDecoder(request.body) - pass - - - def get_params(self, coverage, decoder, request): - return WCS10CoverageRenderParams( - coverage, decoder.bbox, decoder.crs, decoder.format, - decoder.response_crs, decoder.width, decoder.height, - decoder.resx, decoder.resy, decoder.interpolation - ) - - -def parse_bbox_kvp(string): - return map(float, string.split(",")) - - -class WCS10GetCoverageKVPDecoder(kvp.Decoder): - coverage_id = kvp.Parameter("coverage", num=1) - crs = kvp.Parameter(num=1) - response_crs = kvp.Parameter(num="?") - bbox = kvp.Parameter(type=parse_bbox_kvp) - width = kvp.Parameter(type=int, num="?") - height = kvp.Parameter(type=int, num="?") - resx = kvp.Parameter(type=float, num="?") - resy = kvp.Parameter(type=float, num="?") - format = kvp.Parameter(num=1) - interpolation = kvp.Parameter(num="?") - exceptions = kvp.Parameter(num="?") diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/v10/parameters.py eoxserver-0.3.2/eoxserver/services/ows/wcs/v10/parameters.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/v10/parameters.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wcs/v10/parameters.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,103 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -from eoxserver.services.ows.wcs.parameters import ( - CoverageRenderParams, CoverageDescriptionRenderParams -) -from eoxserver.services.subset import Subsets, Trim - - -class WCS10CoverageDescriptionRenderParams(CoverageDescriptionRenderParams): - coverage_ids_key_name = "coverage" - - def __init__(self, coverages): - super(WCS10CoverageDescriptionRenderParams, self).__init__( - coverages, "1.0.0" - ) - - -class WCS10CoverageRenderParams(CoverageRenderParams): - def __init__(self, coverage, bbox, crs, format, response_crs=None, - width=None, height=None, resx=None, resy=None, - interpolation=None): - - super(WCS10CoverageRenderParams, self).__init__(coverage, "1.0.0") - self._bbox = bbox - self._crs = crs - self._format = format - self._response_crs = response_crs - self._width = width - self._height = height - self._resx = resx - self._resy = resy - self._interpolation = interpolation - - coverage_id_key_name = "coverage" - - bbox = property(lambda self: self._bbox) - crs = property(lambda self: self._crs) - format = property(lambda self: self._format) - response_crs = property(lambda self: self._response_crs) - width = property(lambda self: self._width) - height = property(lambda self: self._height) - resx = property(lambda self: self._resx) - resy = property(lambda self: self._resy) - interpolation = property(lambda self: self._interpolation) - - @property - def subsets(self): - return Subsets(( - Trim("x", self._bbox[0], self._bbox[2]), - Trim("y", self._bbox[1], self._bbox[3]), - ), crs=self._crs) - - - def __iter__(self): - for k, v in super(WCS10CoverageRenderParams, self).__iter__(): - yield k, v - - yield ("bbox", ",".join(map(str, self.bbox))) - yield ("crs", self.crs) - yield ("format", self.format) - - if self.response_crs: - yield ("response_crs", self.response_crs) - - if self.width is not None: - yield ("width", str(self.width)) - - if self.height is not None: - yield ("height", str(self.height)) - - if self.resx is not None: - yield ("resx", self.resx) - - if self.resy is not None: - yield ("resy", self.resy) - - if self.interpolation: - yield ("interpolation", self.interpolation) diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/v10/util.py eoxserver-0.3.2/eoxserver/services/ows/wcs/v10/util.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/v10/util.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wcs/v10/util.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,36 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -from eoxserver.core.util.xmltools import NameSpace, NameSpaceMap - - -ns_xlink = NameSpace("http://www.w3.org/1999/xlink", "xlink") -ns_ogc = NameSpace("http://www.opengis.net/ogc", "ogc") -ns_ows = NameSpace("http://www.opengis.net/ows/1.0", "ows") - -nsmap = NameSpaceMap(ns_xlink, ns_ogc, ns_ows) diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/v11/describecoverage.py eoxserver-0.3.2/eoxserver/services/ows/wcs/v11/describecoverage.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/v11/describecoverage.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wcs/v11/describecoverage.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,69 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -from eoxserver.core import Component, implements -from eoxserver.core.decoders import xml, kvp, typelist -from eoxserver.services.ows.interfaces import ( - ServiceHandlerInterface, GetServiceHandlerInterface, - PostServiceHandlerInterface -) -from eoxserver.services.ows.wcs.basehandlers import ( - WCSDescribeCoverageHandlerBase -) -from eoxserver.services.ows.wcs.v11.parameters import ( - WCS11CoverageDescrptionRenderParams -) -from eoxserver.services.ows.wcs.v11.util import nsmap - - -class WCS11DescribeCoverageHandler(WCSDescribeCoverageHandlerBase, Component): - implements(ServiceHandlerInterface) - implements(GetServiceHandlerInterface) - implements(PostServiceHandlerInterface) - - versions = ("1.1.0", "1.1.1", "1.1.2",) - - - def get_decoder(self, request): - if request.method == "GET": - return WCS11DescribeCoverageKVPDecoder(request.GET) - elif request.method == "POST": - return WCS11DescribeCoverageXMLDecoder(request.body) - - - def get_params(self, coverages, decoder): - return WCS11CoverageDescrptionRenderParams(coverages) - - -class WCS11DescribeCoverageKVPDecoder(kvp.Decoder): - coverage_ids = kvp.Parameter("identifier", type=typelist(separator=","), num=1) - - -class WCS11DescribeCoverageXMLDecoder(xml.Decoder): - coverage_ids = xml.Parameter("wcs:Identifier/text()", num="+") - namespaces = nsmap diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/v11/exceptionhandler.py eoxserver-0.3.2/eoxserver/services/ows/wcs/v11/exceptionhandler.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/v11/exceptionhandler.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wcs/v11/exceptionhandler.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,63 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2011 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -from eoxserver.core import Component, implements -from eoxserver.services.ows.interfaces import ExceptionHandlerInterface -from eoxserver.services.ows.common.v11.encoders import OWS11ExceptionXMLEncoder -from eoxserver.core.decoders import ( - DecodingException, MissingParameterException -) - - -class WCS11ExceptionHandler(Component): - implements(ExceptionHandlerInterface) - - service = "WCS" - versions = ("1.1.0", "1.1.1", "1.1.2") - request = None - - def handle_exception(self, request, exception): - message = str(exception) - code = getattr(exception, "code", None) - locator = getattr(exception, "locator", None) - status = 400 - - if code is None: - if isinstance(exception, MissingParameterException): - code = "MissingParameterValue" - elif isinstance(exception, DecodingException): - code = "InvalidParameterValue" - else: - code = "InvalidRequest" - - encoder = OWS11ExceptionXMLEncoder() - xml = encoder.serialize( - encoder.encode_exception(message, "1.1.2", code, locator) - ) - - return (xml, encoder.content_type, status) diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/v11/getcapabilities.py eoxserver-0.3.2/eoxserver/services/ows/wcs/v11/getcapabilities.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/v11/getcapabilities.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wcs/v11/getcapabilities.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,79 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2011 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -from eoxserver.core import Component, implements -from eoxserver.core.decoders import xml, kvp, typelist, lower -from eoxserver.services.ows.interfaces import ( - ServiceHandlerInterface, GetServiceHandlerInterface, - PostServiceHandlerInterface, VersionNegotiationInterface -) -from eoxserver.services.ows.wcs.basehandlers import ( - WCSGetCapabilitiesHandlerBase -) -from eoxserver.services.ows.wcs.parameters import WCSCapabilitiesRenderParams -from eoxserver.services.ows.wcs.v11.util import nsmap - - -class WCS11GetCapabilitiesHandler(WCSGetCapabilitiesHandlerBase, Component): - implements(ServiceHandlerInterface) - implements(GetServiceHandlerInterface) - implements(PostServiceHandlerInterface) - implements(VersionNegotiationInterface) - - versions = ("1.1.0", "1.1.1", "1.1.2") - - def get_decoder(self, request): - if request.method == "GET": - return WCS11GetCapabilitiesKVPDecoder(request.GET) - elif request.method == "POST": - return WCS11GetCapabilitiesXMLDecoder(request.body) - - - def get_params(self, coverages, decoder): - return WCSCapabilitiesRenderParams( - coverages, "1.1.2", decoder.sections, decoder.acceptlanguages, - decoder.acceptformats, decoder.updatesequence - ) - - -class WCS11GetCapabilitiesKVPDecoder(kvp.Decoder): - sections = kvp.Parameter(type=typelist(lower, ","), num="?") - updatesequence = kvp.Parameter(num="?") - acceptversions = kvp.Parameter(type=typelist(str, ","), num="?") - acceptformats = kvp.Parameter(type=typelist(str, ","), num="?", default=["text/xml"]) - acceptlanguages = kvp.Parameter(type=typelist(str, ","), num="?") - - -class WCS11GetCapabilitiesXMLDecoder(xml.Decoder): - sections = xml.Parameter("/ows:Sections/ows:Section/text()", num="*") - updatesequence = xml.Parameter("/@updateSequence", num="?") - acceptversions = xml.Parameter("/ows:AcceptVersions/ows:Version/text()", num="*") - acceptformats = xml.Parameter("/ows:AcceptFormats/ows:OutputFormat/text()", num="*", default=["text/xml"]) - acceptlanguages = xml.Parameter("/ows:AcceptLanguages/ows:Language/text()", num="*") - - namespaces = nsmap diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/v11/getcoverage.py eoxserver-0.3.2/eoxserver/services/ows/wcs/v11/getcoverage.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/v11/getcoverage.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wcs/v11/getcoverage.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,125 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -from eoxserver.core import Component, implements -from eoxserver.core.decoders import xml, kvp, typelist -from eoxserver.services.ows.interfaces import ( - ServiceHandlerInterface, GetServiceHandlerInterface, - PostServiceHandlerInterface -) -from eoxserver.services.ows.wcs.basehandlers import WCSGetCoverageHandlerBase -from eoxserver.services.ows.wcs.v11.util import nsmap -from eoxserver.services.ows.wcs.v11.parameters import WCS11CoverageRenderParams - - -class WCS11GetCoverageHandler(WCSGetCoverageHandlerBase, Component): - implements(ServiceHandlerInterface) - implements(GetServiceHandlerInterface) - implements(PostServiceHandlerInterface) - - versions = ("1.1.0", "1.1.1", "1.1.2") - - def get_decoder(self, request): - if request.method == "GET": - return WCS11GetCoverageKVPDecoder(request.GET) - elif request.method == "POST": - return WCS11GetCoverageXMLDecoder(request.body) - - - def get_params(self, coverage, decoder, request): - return WCS11CoverageRenderParams( - coverage, decoder.boundingbox, decoder.format, decoder.gridcs, - decoder.gridbasecrs, decoder.gridtype, decoder.gridorigin, - decoder.gridoffsets - ) - - -def parse_bbox_kvp(string): - minx, miny, maxx, maxy, crs = string.split(",") - minx, miny, maxx, maxy = map(float, (minx, miny, maxx, maxy)) - return minx, miny, maxx, maxy, crs - - -def parse_bbox_xml(node): - try: - lower_corner = node.xpath("ows:LowerCorner/text()", namespaces=nsmap)[0] - upper_corner = node.xpath("ows:UpperCorner/text()", namespaces=nsmap)[0] - ll = map(float, lower_corner.split(" ")) - ur = map(float, upper_corner.split(" ")) - except (IndexError, ValueError): - raise ValueError("Invalid bounding box.") - crs = node.attrib["crs"] - return ll[0], ll[1], ur[0], ur[1], crs - - -def parse_origin_kvp(string): - x, y = map(float, string.split(",")) - return x, y - - -def parse_origin_xml(string): - x, y = map(float, string.split(" ")) - return x, y - - -def parse_offsets_kvp(string): - x, y = map(float, string.split(",")) - return x, y - - -def parse_offsets_xml(string): - x, y = map(float, string.split(" ")) - return x, y - - -class WCS11GetCoverageKVPDecoder(kvp.Decoder): - coverage_id = kvp.Parameter("identifier", num=1) - boundingbox = kvp.Parameter(type=parse_bbox_kvp, num=1) - format = kvp.Parameter(num=1) - gridcs = kvp.Parameter(num="?") - gridbasecrs = kvp.Parameter(num="?") - gridtype = kvp.Parameter(num="?") - gridorigin = kvp.Parameter(type=parse_origin_kvp, num="?") - gridoffsets = kvp.Parameter(type=parse_offsets_kvp, num="?") - - -class WCS11GetCoverageXMLDecoder(xml.Decoder): - coverage_id = xml.Parameter("ows:Identifier/text()", type=str, num=1) - boundingbox = xml.Parameter("wcs:DomainSubset/ows:BoundingBox", type=parse_bbox_xml, num=1) - format = xml.Parameter("wcs:Output/@format", num=1) - gridcs = xml.Parameter("wcs:Output/wcs:GridCRS/wcs:GridCS/text()", num="?") - gridbasecrs = xml.Parameter("wcs:Output/wcs:GridCRS/wcs:GridBaseCRS/text()", num="?") - gridtype = xml.Parameter("wcs:Output/wcs:GridCRS/wcs:GridType/text()", num="?") - gridorigin = xml.Parameter("wcs:Output/wcs:GridCRS/wcs:GridOrigin/text()", type=parse_origin_xml, num="?") - gridoffsets = xml.Parameter("wcs:Output/wcs:GridCRS/wcs:GridOffsets/text()", type=parse_offsets_xml, num="?") - - # TODO - #interpolation = xml.Parameter("wcs:RangeSubset/wcs:InterpolationType/text()", num="?") - #fields = xml.Parameter("wcs:RangeSubset/ows:Identifier/text()", num="*") - - namespaces = nsmap diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/v11/parameters.py eoxserver-0.3.2/eoxserver/services/ows/wcs/v11/parameters.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/v11/parameters.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wcs/v11/parameters.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,94 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -from eoxserver.services.ows.wcs.parameters import ( - CoverageRenderParams, CoverageDescriptionRenderParams -) -from eoxserver.services.subset import Subsets, Trim - - -class WCS11CoverageDescrptionRenderParams(CoverageDescriptionRenderParams): - coverage_ids_key_name = "identifier" - - def __init__(self, coverages): - super(WCS11CoverageDescrptionRenderParams, self).__init__( - coverages, "1.1.2" - ) - - -class WCS11CoverageRenderParams(CoverageRenderParams): - def __init__(self, coverage, bbox, format, gridcs=None, gridbasecrs=None, - gridtype=None, gridorigin=None, gridoffsets=None): - - super(WCS11CoverageRenderParams, self).__init__(coverage, "1.1.2") - self._bbox = bbox - self._format = format - self._gridcs = gridcs - self._gridbasecrs = gridbasecrs - self._gridtype = gridtype - self._gridorigin = gridorigin - self._gridoffsets = gridoffsets - - coverage_id_key_name = "identifier" - - bbox = property(lambda self: self._bbox) - format = property(lambda self: self._format) - gridcs = property(lambda self: self._gridcs) - gridbasecrs = property(lambda self: self._gridbasecrs) - gridtype = property(lambda self: self._gridtype) - gridorigin = property(lambda self: self._gridorigin) - gridoffsets = property(lambda self: self._gridoffsets) - - @property - def subsets(self): - return Subsets(( - Trim("x", self._bbox[0], self._bbox[2]), - Trim("y", self._bbox[1], self._bbox[3]), - ), crs=self._bbox[4]) - - - def __iter__(self): - for k, v in super(WCS11CoverageRenderParams, self).__iter__(): - yield k, v - - yield ("boundingbox", ",".join(map(str, self.bbox))) - yield ("format", self.format) - - if self.gridcs: - yield ("gridcs", self.gridcs) - - if self.gridbasecrs: - yield ("gridbasecrs", self.gridbasecrs) - - if self.gridoffsets: - yield ("gridoffsets", ",".join(map(str, self.gridoffsets))) - - if self.gridtype is not None: - yield ("gridtype", self.gridtype) - - if self.gridorigin is not None: - yield ("gridorigin", ",".join(map(str, self.gridorigin))) diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/v11/util.py eoxserver-0.3.2/eoxserver/services/ows/wcs/v11/util.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/v11/util.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wcs/v11/util.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,37 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -from eoxserver.core.util.xmltools import NameSpace, NameSpaceMap - - -ns_xlink = NameSpace("http://www.w3.org/1999/xlink", "xlink") -ns_ogc = NameSpace("http://www.opengis.net/ogc", "ogc") -ns_ows = NameSpace("http://www.opengis.net/ows/1.1", "ows") -ns_wcs = NameSpace("http://www.opengis.net/wcs/1.1", "wcs") - -nsmap = NameSpaceMap(ns_xlink, ns_ogc, ns_ows, ns_wcs) diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/v20/describecoverage.py eoxserver-0.3.2/eoxserver/services/ows/wcs/v20/describecoverage.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/v20/describecoverage.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wcs/v20/describecoverage.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,69 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2011 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -from eoxserver.core import Component, implements -from eoxserver.core.decoders import xml, kvp, typelist, upper -from eoxserver.services.ows.interfaces import ( - ServiceHandlerInterface, GetServiceHandlerInterface, - PostServiceHandlerInterface -) -from eoxserver.services.ows.wcs.basehandlers import ( - WCSDescribeCoverageHandlerBase -) -from eoxserver.services.ows.wcs.v20.parameters import ( - WCS20CoverageDescriptionRenderParams -) -from eoxserver.services.ows.wcs.v20.util import nsmap - - -class WCS20DescribeCoverageHandler(WCSDescribeCoverageHandlerBase, Component): - implements(ServiceHandlerInterface) - implements(GetServiceHandlerInterface) - implements(PostServiceHandlerInterface) - - versions = ("2.0.0", "2.0.1") - - index = 5 - - def get_decoder(self, request): - if request.method == "GET": - return WCS20DescribeCoverageKVPDecoder(request.GET) - elif request.method == "POST": - return WCS20DescribeCoverageXMLDecoder(request.body) - - def get_params(self, coverages, decoder): - return WCS20CoverageDescriptionRenderParams(coverages) - - -class WCS20DescribeCoverageKVPDecoder(kvp.Decoder): - coverage_ids = kvp.Parameter("coverageid", type=typelist(str, ","), num=1) - - -class WCS20DescribeCoverageXMLDecoder(xml.Decoder): - coverage_ids = xml.Parameter("wcs:CoverageId/text()", num="+") - namespaces = nsmap diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/v20/describeeocoverageset.py eoxserver-0.3.2/eoxserver/services/ows/wcs/v20/describeeocoverageset.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/v20/describeeocoverageset.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wcs/v20/describeeocoverageset.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,255 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -import sys -import logging -from itertools import chain - -from django.db.models import Q - -from eoxserver.core import Component, implements -from eoxserver.core.config import get_eoxserver_config -from eoxserver.core.decoders import xml, kvp, typelist, upper, enum -from eoxserver.resources.coverages import models -from eoxserver.services.ows.interfaces import ( - ServiceHandlerInterface, GetServiceHandlerInterface, - PostServiceHandlerInterface -) -from eoxserver.services.ows.wcs.v20.util import ( - nsmap, SectionsMixIn, parse_subset_kvp, parse_subset_xml -) -from eoxserver.services.ows.wcs.v20.encoders import WCS20EOXMLEncoder -from eoxserver.services.ows.common.config import WCSEOConfigReader -from eoxserver.services.subset import Subsets, Trim -from eoxserver.services.exceptions import ( - NoSuchDatasetSeriesOrCoverageException, InvalidSubsettingException -) - - -logger = logging.getLogger(__name__) - -class WCS20DescribeEOCoverageSetHandler(Component): - implements(ServiceHandlerInterface) - implements(GetServiceHandlerInterface) - implements(PostServiceHandlerInterface) - - service = "WCS" - versions = ("2.0.0", "2.0.1") - request = "DescribeEOCoverageSet" - - index = 20 - - def get_decoder(self, request): - if request.method == "GET": - return WCS20DescribeEOCoverageSetKVPDecoder(request.GET) - elif request.method == "POST": - return WCS20DescribeEOCoverageSetXMLDecoder(request.body) - - @property - def constraints(self): - reader = WCSEOConfigReader(get_eoxserver_config()) - return { - "CountDefault": reader.paging_count_default - } - - def handle(self, request): - decoder = self.get_decoder(request) - eo_ids = decoder.eo_ids - - containment = decoder.containment - if not containment: - containment = "overlaps" - - count_default = self.constraints["CountDefault"] - count = decoder.count - if count_default is not None: - count = min(count, count_default) - - try: - subsets = Subsets( - decoder.subsets, - crs="http://www.opengis.net/def/crs/EPSG/0/4326", - allowed_types=Trim - ) - except ValueError, e: - raise InvalidSubsettingException(str(e)) - - inc_dss_section = decoder.section_included("DatasetSeriesDescriptions") - inc_cov_section = decoder.section_included("CoverageDescriptions") - - if len(eo_ids) == 0: - raise - - # fetch a list of all requested EOObjects - available_ids = models.EOObject.objects.filter( - identifier__in=eo_ids - ).values_list("identifier", flat=True) - - # match the requested EOIDs against the available ones. If any are - # requested, that are not available, raise and exit. - failed = [ eo_id for eo_id in eo_ids if eo_id not in available_ids ] - if failed: - raise NoSuchDatasetSeriesOrCoverageException(failed) - - collections_qs = subsets.filter(models.Collection.objects.filter( - identifier__in=eo_ids - ), containment="overlaps") - - # create a set of all indirectly referenced containers by iterating - # recursively. The containment is set to "overlaps", to also include - # collections that might have been excluded with "contains" but would - # have matching coverages inserted. - - def recursive_lookup(super_collection, collection_set): - sub_collections = models.Collection.objects.filter( - collections__in=[super_collection.pk] - ).exclude( - pk__in=map(lambda c: c.pk, collection_set) - ) - sub_collections = subsets.filter(sub_collections, "overlaps") - - # Add all to the set - collection_set |= set(sub_collections) - - for sub_collection in sub_collections: - recursive_lookup(sub_collection, collection_set) - - collection_set = set(collections_qs) - for collection in set(collection_set): - recursive_lookup(collection, collection_set) - - collection_pks = map(lambda c: c.pk, collection_set) - - # Get all either directly referenced coverages or coverages that are - # within referenced containers. Full subsetting is applied here. - - coverages_qs = subsets.filter(models.Coverage.objects.filter( - Q(identifier__in=eo_ids) | Q(collections__in=collection_pks) - ), containment=containment) - - # save a reference before limits are applied to obtain the full number - # of matched coverages. - coverages_no_limit_qs = coverages_qs - - - num_collections = len( - filter(lambda c: not models.iscoverage(c), collection_set) - ) - - # compute how many (if any) coverages can be retrieved. This depends on - # the "count" parameter and default setting. Also, if we already - # exceeded the count, limit the number of dataset series aswell - - if inc_dss_section: - displayed_collections = num_collections - else: - displayed_collections = 0 - - if displayed_collections < count and inc_cov_section: - coverages_qs = coverages_qs.order_by("identifier")[:count - displayed_collections] - elif displayed_collections == count or not inc_cov_section: - coverages_qs = [] - else: - coverages_qs = [] - collection_set = sorted(collection_set, key=lambda c: c.identifier)[:count] - - # get a number of coverages that *would* have been included, but are not - # because of the count parameter - count_all_coverages = coverages_no_limit_qs.count() - - # if containment is "contains" we need to check all collections again - if containment == "contains": - collection_set = filter(lambda c: subsets.matches(c), collection_set) - - coverages = set() - dataset_series = set() - - # finally iterate over everything that has been retrieved and get - # a list of dataset series and coverages to be encoded into the response - for eo_object in chain(coverages_qs, collection_set): - if inc_cov_section and issubclass(eo_object.real_type, models.Coverage): - coverages.add(eo_object.cast()) - elif inc_dss_section and issubclass(eo_object.real_type, models.DatasetSeries): - dataset_series.add(eo_object.cast()) - - else: - # TODO: what to do here? - pass - - # TODO: coverages should be sorted - #coverages = sorted(coverages, ) - - #encoder = WCS20CoverageDescriptionXMLEncoder() - #return encoder.encode(coverages) - - # TODO: remove this at some point - encoder = WCS20EOXMLEncoder() - - return ( - encoder.serialize( - encoder.encode_eo_coverage_set_description( - sorted(dataset_series, key=lambda s: s.identifier), - sorted(coverages, key=lambda c: c.identifier), - count_all_coverages + num_collections - ), pretty_print=True - ), - encoder.content_type - ) - - -def pos_int(value): - value = int(value) - if value < 0: - raise ValueError("Negative values are not allowed.") - return value - - -containment_enum = enum( - ("overlaps", "contains"), False -) - -sections_enum = enum( - ("DatasetSeriesDescriptions", "CoverageDescriptions", "All"), False -) - -class WCS20DescribeEOCoverageSetKVPDecoder(kvp.Decoder, SectionsMixIn): - eo_ids = kvp.Parameter("eoid", type=typelist(str, ","), num=1, locator="eoid") - subsets = kvp.Parameter("subset", type=parse_subset_kvp, num="*") - containment = kvp.Parameter(type=containment_enum, num="?") - count = kvp.Parameter(type=pos_int, num="?", default=sys.maxint) - sections = kvp.Parameter(type=typelist(sections_enum, ","), num="?") - - -class WCS20DescribeEOCoverageSetXMLDecoder(xml.Decoder, SectionsMixIn): - eo_ids = xml.Parameter("wcseo:eoId/text()", num="+", locator="eoid") - subsets = xml.Parameter("wcs:DimensionTrim", type=parse_subset_xml, num="*") - containment = xml.Parameter("wcseo:containment/text()", type=containment_enum, locator="containment") - count = xml.Parameter("@count", type=pos_int, num="?", default=sys.maxint, locator="count") - sections = xml.Parameter("wcseo:sections/wcseo:section/text()", type=sections_enum, num="*", locator="sections") - - namespaces = nsmap diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/v20/encoders.py eoxserver-0.3.2/eoxserver/services/ows/wcs/v20/encoders.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/v20/encoders.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wcs/v20/encoders.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,734 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -from lxml import etree - -from django.contrib.gis.geos import Polygon -from django.utils.timezone import now - -from eoxserver.core.config import get_eoxserver_config -from eoxserver.core.util.timetools import isoformat -from eoxserver.backends.access import retrieve -from eoxserver.contrib.osr import SpatialReference -from eoxserver.resources.coverages.models import ( - RectifiedStitchedMosaic, ReferenceableDataset -) -from eoxserver.resources.coverages.formats import getFormatRegistry -from eoxserver.resources.coverages import crss, models -from eoxserver.services.gml.v32.encoders import GML32Encoder, EOP20Encoder -from eoxserver.services.ows.component import ServiceComponent, env -from eoxserver.services.ows.common.config import CapabilitiesConfigReader -from eoxserver.services.ows.common.v20.encoders import OWS20Encoder -from eoxserver.services.ows.wcs.v20.util import ( - nsmap, ns_xlink, ns_gml, ns_wcs, ns_eowcs, - OWS, GML, GMLCOV, WCS, CRS, EOWCS, SWE, INT, SUPPORTED_INTERPOLATIONS -) -from eoxserver.services.urls import get_http_service_url - - -class WCS20CapabilitiesXMLEncoder(OWS20Encoder): - def encode_capabilities(self, sections, coverages_qs=None, - dataset_series_qs=None, request=None): - conf = CapabilitiesConfigReader(get_eoxserver_config()) - - all_sections = "all" in sections - caps = [] - if all_sections or "serviceidentification" in sections: - caps.append( - OWS("ServiceIdentification", - OWS("Title", conf.title), - OWS("Abstract", conf.abstract), - OWS("Keywords", *[ - OWS("Keyword", keyword) for keyword in conf.keywords - ]), - OWS("ServiceType", "OGC WCS", codeSpace="OGC"), - OWS("ServiceTypeVersion", "2.0.1"), - OWS("Profile", "http://www.opengis.net/spec/WCS_application-profile_earth-observation/1.0/conf/eowcs"), - OWS("Profile", "http://www.opengis.net/spec/WCS_application-profile_earth-observation/1.0/conf/eowcs_get-kvp"), - OWS("Profile", "http://www.opengis.net/spec/WCS_service-extension_crs/1.0/conf/crs"), - OWS("Profile", "http://www.opengis.net/spec/WCS/2.0/conf/core"), - OWS("Profile", "http://www.opengis.net/spec/WCS_protocol-binding_get-kvp/1.0/conf/get-kvp"), - OWS("Profile", "http://www.opengis.net/spec/WCS_protocol-binding_post-xml/1.0/conf/post-xml"), - OWS("Profile", "http://www.opengis.net/spec/GMLCOV/1.0/conf/gml-coverage"), - OWS("Profile", "http://www.opengis.net/spec/GMLCOV/1.0/conf/multipart"), - OWS("Profile", "http://www.opengis.net/spec/GMLCOV/1.0/conf/special-format"), - OWS("Profile", "http://www.opengis.net/spec/GMLCOV_geotiff-coverages/1.0/conf/geotiff-coverage"), - OWS("Profile", "http://www.opengis.net/spec/WCS_geotiff-coverages/1.0/conf/geotiff-coverage"), - OWS("Profile", "http://www.opengis.net/spec/WCS_service-model_crs-predefined/1.0/conf/crs-predefined"), - OWS("Profile", "http://www.opengis.net/spec/WCS_service-extension_interpolation/1.0/conf/interpolation"), - OWS("Profile", "http://www.opengis.net/spec/WCS_service-extension_range-subsetting/1.0/conf/record-subsetting"), - OWS("Profile", "http://www.opengis.net/spec/WCS_service-extension_scaling/1.0/conf/scaling"), - OWS("Fees", conf.fees), - OWS("AccessConstraints", conf.access_constraints) - ) - ) - - if all_sections or "serviceprovider" in sections: - caps.append( - OWS("ServiceProvider", - OWS("ProviderName", conf.provider_name), - self.encode_reference("ProviderSite", conf.provider_site), - OWS("ServiceContact", - OWS("IndividualName", conf.individual_name), - OWS("PositionName", conf.position_name), - OWS("ContactInfo", - OWS("Phone", - OWS("Voice", conf.phone_voice), - OWS("Facsimile", conf.phone_facsimile) - ), - OWS("Address", - OWS("DeliveryPoint", conf.delivery_point), - OWS("City", conf.city), - OWS("AdministrativeArea", conf.administrative_area), - OWS("PostalCode", conf.postal_code), - OWS("Country", conf.country), - OWS("ElectronicMailAddress", conf.electronic_mail_address) - ), - self.encode_reference( - "OnlineResource", conf.onlineresource - ), - OWS("HoursOfService", conf.hours_of_service), - OWS("ContactInstructions", conf.contact_instructions) - ), - OWS("Role", conf.role) - ) - ) - ) - - if all_sections or "operationsmetadata" in sections: - component = ServiceComponent(env) - versions = ("2.0.0", "2.0.1") - get_handlers = component.query_service_handlers( - service="WCS", versions=versions, method="GET" - ) - post_handlers = component.query_service_handlers( - service="WCS", versions=versions, method="POST" - ) - all_handlers = sorted( - set(get_handlers + post_handlers), - key=lambda h: (getattr(h, "index", 10000), h.request) - ) - - http_service_url = get_http_service_url(request) - - operations = [] - for handler in all_handlers: - methods = [] - if handler in get_handlers: - methods.append( - self.encode_reference("Get", http_service_url) - ) - if handler in post_handlers: - post = self.encode_reference("Post", http_service_url) - post.append( - OWS("Constraint", - OWS("AllowedValues", - OWS("Value", "XML") - ), name="PostEncoding" - ) - ) - methods.append(post) - - operations.append( - OWS("Operation", - OWS("DCP", - OWS("HTTP", *methods) - ), - # apply default values as constraints - *[ - OWS("Constraint", - OWS("NoValues"), - OWS("DefaultValue", str(default)), - name=name - ) for name, default - in getattr(handler, "constraints", {}).items() - ], - name=handler.request - ) - ) - caps.append(OWS("OperationsMetadata", *operations)) - - if all_sections or "servicemetadata" in sections: - service_metadata = WCS("ServiceMetadata") - - # get the list of enabled formats from the format registry - formats = filter( - lambda f: f, getFormatRegistry().getSupportedFormatsWCS() - ) - service_metadata.extend( - map(lambda f: WCS("formatSupported", f.mimeType), formats) - ) - - # get a list of supported CRSs from the CRS registry - supported_crss = crss.getSupportedCRS_WCS( - format_function=crss.asURL - ) - extension = WCS("Extension") - service_metadata.append(extension) - crs_metadata = CRS("CrsMetadata") - extension.append(crs_metadata) - crs_metadata.extend( - map(lambda c: CRS("crsSupported", c), supported_crss) - ) - - base_url = "http://www.opengis.net/def/interpolation/OGC/1/" - - extension.append( - INT("InterpolationMetadata", *[ - INT("InterpolationSupported", - base_url + supported_interpolation - ) for supported_interpolation in SUPPORTED_INTERPOLATIONS - ]) - ) - - caps.append(service_metadata) - - inc_contents = all_sections or "contents" in sections - inc_coverage_summary = inc_contents or "coveragesummary" in sections - inc_dataset_series_summary = inc_contents or "datasetseriessummary" in sections - inc_contents = inc_contents or inc_coverage_summary or inc_dataset_series_summary - - if inc_contents: - contents = [] - - if inc_coverage_summary: - coverages = [] - - # reduce data transfer by only selecting required elements - # TODO: currently runs into a bug - #coverages_qs = coverages_qs.only( - # "identifier", "real_content_type" - #) - - for coverage in coverages_qs: - coverages.append( - WCS("CoverageSummary", - WCS("CoverageId", coverage.identifier), - WCS("CoverageSubtype", coverage.real_type.__name__) - ) - ) - contents.extend(coverages) - - if inc_dataset_series_summary: - dataset_series_set = [] - - # reduce data transfer by only selecting required elements - # TODO: currently runs into a bug - #dataset_series_qs = dataset_series_qs.only( - # "identifier", "begin_time", "end_time", "footprint" - #) - - for dataset_series in dataset_series_qs: - minx, miny, maxx, maxy = dataset_series.extent_wgs84 - - dataset_series_set.append( - EOWCS("DatasetSeriesSummary", - OWS("WGS84BoundingBox", - OWS("LowerCorner", "%f %f" % (miny, minx)), - OWS("UpperCorner", "%f %f" % (maxy, maxx)), - ), - EOWCS("DatasetSeriesId", dataset_series.identifier), - GML("TimePeriod", - GML("beginPosition", isoformat(dataset_series.begin_time)), - GML("endPosition", isoformat(dataset_series.end_time)), - **{ns_gml("id"): dataset_series.identifier + "_timeperiod"} - ) - ) - ) - - contents.append(WCS("Extension", *dataset_series_set)) - - caps.append(WCS("Contents", *contents)) - - root = WCS("Capabilities", *caps, version="2.0.1", updateSequence=conf.update_sequence) - return root - - def get_schema_locations(self): - return nsmap.schema_locations - - -class GMLCOV10Encoder(GML32Encoder): - def __init__(self, *args, **kwargs): - self._cache = {} - - def get_gml_id(self, identifier): - if identifier[0].isdigit(): - return "gmlid_%s" % identifier - return identifier - - def encode_grid_envelope(self, low_x, low_y, high_x, high_y): - return GML("GridEnvelope", - GML("low", "%d %d" % (low_x, low_y)), - GML("high", "%d %d" % (high_x, high_y)) - ) - - def encode_rectified_grid(self, size, extent, sr, grid_name): - size_x, size_y = size - minx, miny, maxx, maxy = extent - srs_name = sr.url - - swap = crss.getAxesSwapper(sr.srid) - frmt = "%.3f %.3f" if sr.IsProjected() else "%.8f %.8f" - labels = ("x", "y") if sr.IsProjected() else ("long", "lat") - - axis_labels = " ".join(swap(*labels)) - origin = frmt % swap(minx, maxy) - x_offsets = frmt % swap((maxx - minx) / float(size_x), 0) - y_offsets = frmt % swap(0, (miny - maxy) / float(size_y)) - - return GML("RectifiedGrid", - GML("limits", - self.encode_grid_envelope(0, 0, size_x - 1, size_y - 1) - ), - GML("axisLabels", axis_labels), - GML("origin", - GML("Point", - GML("pos", origin), - **{ - ns_gml("id"): self.get_gml_id("%s_origin" % grid_name), - "srsName": srs_name - } - ) - ), - GML("offsetVector", x_offsets, srsName=srs_name), - GML("offsetVector", y_offsets, srsName=srs_name), - **{ - ns_gml("id"): self.get_gml_id(grid_name), - "dimension": "2" - } - ) - - def encode_referenceable_grid(self, size, sr, grid_name): - size_x, size_y = size - swap = crss.getAxesSwapper(sr.srid) - labels = ("x", "y") if sr.IsProjected() else ("long", "lat") - axis_labels = " ".join(swap(*labels)) - - return GML("ReferenceableGrid", - GML("limits", - self.encode_grid_envelope(0, 0, size_x - 1, size_y - 1) - ), - GML("axisLabels", axis_labels), - **{ - ns_gml("id"): self.get_gml_id(grid_name), - "dimension": "2" - } - ) - - def encode_domain_set(self, coverage, srid=None, size=None, extent=None, - rectified=True): - grid_name = "%s_grid" % coverage.identifier - srs = SpatialReference(srid) if srid is not None else None - - if rectified: - return GML("domainSet", - self.encode_rectified_grid( - size or coverage.size, extent or coverage.extent, - srs or coverage.spatial_reference, grid_name - ) - ) - else: - return GML("domainSet", - self.encode_referenceable_grid( - size or coverage.size, srs or coverage.spatial_reference, - grid_name - ) - ) - - def encode_bounded_by(self, extent, sr=None): - minx, miny, maxx, maxy = extent - sr = sr or SpatialReference(4326) - swap = crss.getAxesSwapper(sr.srid) - labels = ("x", "y") if sr.IsProjected() else ("long", "lat") - axis_labels = " ".join(swap(*labels)) - axis_units = "m m" if sr.IsProjected() else "deg deg" - frmt = "%.3f %.3f" if sr.IsProjected() else "%.8f %.8f" - # Make sure values are outside of actual extent - if sr.IsProjected(): - minx -= 0.0005 - miny -= 0.0005 - maxx += 0.0005 - maxy += 0.0005 - else: - minx -= 0.000000005 - miny -= 0.000000005 - maxx += 0.000000005 - maxy += 0.000000005 - - return GML("boundedBy", - GML("Envelope", - GML("lowerCorner", frmt % swap(minx, miny)), - GML("upperCorner", frmt % swap(maxx, maxy)), - srsName=sr.url, axisLabels=axis_labels, uomLabels=axis_units, - srsDimension="2" - ) - ) - - # cached range types and nil value sets - def get_range_type(self, pk): - cached_range_types = self._cache.setdefault(models.RangeType, {}) - try: - return cached_range_types[pk] - except KeyError: - cached_range_types[pk] = models.RangeType.objects.get(pk=pk) - return cached_range_types[pk] - - def get_nil_value_set(self, pk): - cached_nil_value_set = self._cache.setdefault(models.NilValueSet, {}) - try: - return cached_nil_value_set[pk] - except KeyError: - try: - cached_nil_value_set[pk] = models.NilValueSet.objects.get( - pk=pk - ) - return cached_nil_value_set[pk] - except models.NilValueSet.DoesNotExist: - return () - - def encode_nil_values(self, nil_value_set): - return SWE("nilValues", - SWE("NilValues", - *[SWE("nilValue", nil_value.raw_value, reason=nil_value.reason - ) for nil_value in nil_value_set] - ) - ) - - def encode_field(self, band): - return SWE("field", - SWE("Quantity", - SWE("description", band.description), - self.encode_nil_values( - self.get_nil_value_set(band.nil_value_set_id) - ), - SWE("uom", code=band.uom), - SWE("constraint", - SWE("AllowedValues", - SWE("interval", "%s %s" % band.allowed_values), - SWE("significantFigures", str(band.significant_figures)) - ) - ), - definition=band.definition - ), - name=band.name - ) - - def encode_range_type(self, range_type): - return GMLCOV("rangeType", - SWE("DataRecord", - *[self.encode_field(band) for band in range_type] - ) - ) - - -class WCS20CoverageDescriptionXMLEncoder(GMLCOV10Encoder): - def encode_coverage_description(self, coverage): - if issubclass(coverage.real_type, ReferenceableDataset): - rectified = False - else: - rectified = True - - return WCS("CoverageDescription", - self.encode_bounded_by(coverage.extent_wgs84), - WCS("CoverageId", coverage.identifier), - self.encode_domain_set(coverage, rectified=rectified), - self.encode_range_type(self.get_range_type(coverage.range_type_id)), - WCS("ServiceParameters", - WCS("CoverageSubtype", coverage.real_type.__name__) - ) - **{ns_gml("id"): self.get_gml_id(coverage.identifier)} - ) - - def encode_coverage_descriptions(self, coverages): - return WCS("CoverageDescriptions", *[ - self.encode_coverage_description(coverage) - for coverage in coverages - ]) - - def get_schema_locations(self): - return {ns_wcs.uri: ns_wcs.schema_location} - - -class WCS20EOXMLEncoder(WCS20CoverageDescriptionXMLEncoder, EOP20Encoder, OWS20Encoder): - def encode_eo_metadata(self, coverage, request=None, subset_polygon=None): - data_items = list(coverage.data_items.filter( - semantic="metadata", format="eogml" - )) - if len(data_items) >= 1: - with open(retrieve(data_items[0])) as f: - earth_observation = etree.parse(f).getroot() - - if subset_polygon: - try: - feature = earth_observation.xpath( - "om:featureOfInterest", namespaces=nsmap - )[0] - feature[0] = self.encode_footprint( - coverage.footprint.intersection(subset_polygon), - coverage.identifier - ) - except IndexError: - pass # no featureOfInterest - - else: - earth_observation = self.encode_earth_observation( - coverage, subset_polygon=subset_polygon - ) - - if not request: - lineage = None - - elif request.method == "GET": - lineage = EOWCS("lineage", - EOWCS("referenceGetCoverage", - self.encode_reference("Reference", - request.build_absolute_uri().replace("&", "&"), - False - ) - ), GML("timePosition", isoformat(now())) - ) - elif request.method == "POST": # TODO: better way to do this - lineage = EOWCS("lineage", - EOWCS("referenceGetCoverage", - OWS("ServiceReference", - OWS("RequestMessage", - etree.parse(request).getroot() - ), **{ns_xlink("href"): request.build_absolute_uri().replace("&", "&")} - ) - ), GML("timePosition", isoformat(now())) - ) - - return GMLCOV("metadata", - GMLCOV("Extension", - EOWCS("EOMetadata", - earth_observation, - *[lineage] if lineage is not None else [] - ) - ) - ) - - def encode_coverage_description(self, coverage, srid=None, size=None, extent=None, footprint=None): - source_mime = None - for data_item in coverage.data_items.filter(semantic__startswith="bands"): - if data_item.format: - source_mime = data_item.format - break - - if source_mime: - source_format = getFormatRegistry().getFormatByMIME(source_mime) - # map the source format to the native one - native_format = getFormatRegistry().mapSourceToNativeWCS20(source_format) - elif issubclass(coverage.real_type, RectifiedStitchedMosaic): - # use the default format for RectifiedStitchedMosaics - native_format = getFormatRegistry().getDefaultNativeFormat() - else: - # TODO: improve if no native format availabe - native_format = None - - if extent: - poly = Polygon.from_bbox(extent) - poly.srid = srid - extent = poly.transform(4326).extent - sr = SpatialReference(4326) - else: - extent = coverage.extent - sr = coverage.spatial_reference - - rectified = False if issubclass(coverage.real_type, ReferenceableDataset) else True - - return WCS("CoverageDescription", - self.encode_bounded_by(extent, sr), - WCS("CoverageId", coverage.identifier), - self.encode_eo_metadata(coverage), - self.encode_domain_set(coverage, srid, size, extent, rectified), - self.encode_range_type(self.get_range_type(coverage.range_type_id)), - WCS("ServiceParameters", - WCS("CoverageSubtype", coverage.real_type.__name__), - WCS("nativeFormat", native_format.mimeType if native_format else "") - ), - **{ns_gml("id"): self.get_gml_id(coverage.identifier)} - ) - - def encode_range_set(self, reference, mime_type): - return GML("rangeSet", - GML("File", - GML("rangeParameters", - **{ - ns_xlink("arcrole"): "fileReference", - ns_xlink("href"): reference, - ns_xlink("role"): mime_type - } - ), - GML("fileReference", reference), - GML("fileStructure"), - GML("mimeType", mime_type) - ) - ) - - def calculate_contribution(self, footprint, contributions, subset_polygon=None): - if subset_polygon: - footprint = footprint.intersection(subset_polygon) - - for contribution in contributions: - footprint = footprint.difference(contribution) - contributions.append(footprint) - return footprint - - - def encode_contributing_datasets(self, coverage, subset_polygon=None): - eo_objects = coverage.eo_objects - if subset_polygon: - if subset_polygon.srid != 4326: - subset_polygon = subset_polygon.transform(4326, True) - - eo_objects = eo_objects.filter( - footprint__intersects=subset_polygon - ) - - # iterate over all subsets in reverse order to get the - eo_objects = eo_objects.order_by("-begin_time") - actual_contributions = [] - all_contributions = [] - for eo_object in eo_objects: - contribution = self.calculate_contribution( - eo_object.footprint, all_contributions, subset_polygon - ) - if not contribution.empty and contribution.num_geom > 0: - actual_contributions.append((eo_object, contribution)) - - return EOWCS("datasets", *[ - EOWCS("dataset", - WCS("CoverageId", eo_object.identifier), - EOWCS("contributingFootprint", - self.encode_footprint( - contribution, eo_object.identifier - ) - ) - ) - for eo_object, contribution in reversed(actual_contributions) - ]) - - def alter_rectified_dataset(self, coverage, request, tree, subset_polygon=None): - return EOWCS("RectifiedDataset", *( - tree.getchildren() + [ - self.encode_eo_metadata(coverage, request, subset_polygon) - ] - ), **tree.attrib) - - def alter_rectified_stitched_mosaic(self, coverage, request, tree, subset_polygon=None): - return EOWCS("RectifiedStitchedMosaic", *( - tree.getchildren() + [ - self.encode_eo_metadata(coverage, request, subset_polygon), - self.encode_contributing_datasets(coverage, subset_polygon) - ] - ), **tree.attrib) - - def encode_referenceable_dataset(self, coverage, range_type, reference, - mime_type, subset=None): - # handle subset - dst_srid = coverage.srid - - if not subset: - # whole area - no subset - domain_set = self.encode_domain_set(coverage, rectified=False) - eo_metadata = self.encode_eo_metadata(coverage) - extent = coverage.extent - sr = SpatialReference(dst_srid) - - else: - # subset is given - srid, size, extent, footprint = subset - srid = srid if srid is not None else 4326 - - domain_set = self.encode_domain_set( - coverage, srid, size, extent, False - ) - eo_metadata = self.encode_eo_metadata( - coverage, subset_polygon=footprint - ) - - # get the WGS84 extent - poly = Polygon.from_bbox(extent) - poly.srid = srid - if srid != dst_srid: - poly.transform(dst_srid) - extent = poly.extent - sr = SpatialReference(srid) - - return EOWCS("ReferenceableDataset", - self.encode_bounded_by(extent, sr), - domain_set, - self.encode_range_set(reference, mime_type), - self.encode_range_type(range_type), - eo_metadata, - **{ - ns_gml("id"): self.get_gml_id(coverage.identifier) - } - ) - - def encode_dataset_series_description(self, dataset_series): - return EOWCS("DatasetSeriesDescription", - self.encode_bounded_by(dataset_series.extent_wgs84), - EOWCS("DatasetSeriesId", dataset_series.identifier), - self.encode_time_period( - dataset_series.begin_time, dataset_series.end_time, - "%s_timeperiod" % dataset_series.identifier - ), - **{ns_gml("id"): self.get_gml_id(dataset_series.identifier)} - ) - - def encode_dataset_series_descriptions(self, dataset_series_set): - return EOWCS("DatasetSeriesDescriptions", *[ - self.encode_dataset_series_description(dataset_series) - for dataset_series in dataset_series_set - ]) - - def encode_eo_coverage_set_description(self, dataset_series_set, coverages, - number_matched=None, - number_returned=None): - if number_matched is None: - number_matched = len(coverages) + len(dataset_series_set) - if number_returned is None: - number_returned = len(coverages) + len(dataset_series_set) - - root = EOWCS("EOCoverageSetDescription", - numberMatched=str(number_matched), - numberReturned=str(number_returned) - ) - - if coverages: - root.append(self.encode_coverage_descriptions(coverages)) - if dataset_series_set: - root.append(self.encode_dataset_series_descriptions( - dataset_series_set - )) - - return root - - def get_schema_locations(self): - return {ns_eowcs.uri: ns_eowcs.schema_location} diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/v20/encodings/geotiff.py eoxserver-0.3.2/eoxserver/services/ows/wcs/v20/encodings/geotiff.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/v20/encodings/geotiff.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wcs/v20/encodings/geotiff.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,131 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -from eoxserver.core import Component, implements -from eoxserver.core.decoders import ( - kvp, xml, upper, enum, value_range, boolean, InvalidParameterException -) -from eoxserver.core.util.xmltools import NameSpace, NameSpaceMap -from eoxserver.services.ows.wcs.interfaces import EncodingExtensionInterface -from eoxserver.services.ows.wcs.v20.util import ns_wcs - - -class WCS20GeoTIFFEncodingExtension(Component): - implements(EncodingExtensionInterface) - - def supports(self, frmt, options): - # To allow "native" GeoTIFF formats aswell - if not frmt: - return True - return frmt.lower() == "image/tiff" - - - def get_decoder(self, request): - if request.method == "GET": - return WCS20GeoTIFFEncodingExtensionKVPDecoder(request.GET) - else: - return WCS20GeoTIFFEncodingExtensionXMLDecoder(request.body) - - def get_encoding_params(self, request): - decoder = self.get_decoder(request) - - # perform some dependant value checking - compression = decoder.compression - predictor = decoder.predictor - jpeg_quality = decoder.jpeg_quality - tiling = decoder.tiling - tileheight = decoder.tileheight - tilewidth = decoder.tilewidth - - if predictor and compression not in ("LZW", "Deflate"): - raise InvalidParameterException( - "geotiff:predictor requires compression method 'LZW' or " - "'Deflate'.", "geotiff:predictor" - ) - - if jpeg_quality is not None and compression != "JPEG": - raise InvalidParameterException( - "geotiff:jpeg_quality requires compression method 'JPEG'.", - "geotiff:jpeg_quality" - ) - - if tiling and (tileheight is None or tilewidth is None): - raise InvalidParameterException( - "geotiff:tiling requires geotiff:tilewidth and " - "geotiff:tileheight to be set.", "geotiff:tiling" - ) - - return { - "compression": compression, - "jpeg_quality": jpeg_quality, - "predictor": predictor, - "interleave": decoder.interleave, - "tiling": tiling, - "tileheight": tileheight, - "tilewidth": tilewidth - } - - -compression_enum = enum( - ("None", "PackBits", "Huffman", "LZW", "JPEG", "Deflate") -) -predictor_enum = enum(("None", "Horizontal", "FloatingPoint")) -interleave_enum = enum(("Pixel", "Band")) - - -def parse_multiple_16(raw): - value = int(raw) - if value < 0: - raise ValueError("Value must be a positive integer.") - elif (value % 16) != 0: - raise ValueError("Value must be a multiple of 16.") - return value - - -class WCS20GeoTIFFEncodingExtensionKVPDecoder(kvp.Decoder): - compression = kvp.Parameter("geotiff:compression", num="?", type=compression_enum) - jpeg_quality = kvp.Parameter("geotiff:jpeg_quality", num="?", type=value_range(1, 100, type=int)) - predictor = kvp.Parameter("geotiff:predictor", num="?", type=predictor_enum) - interleave = kvp.Parameter("geotiff:interleave", num="?", type=interleave_enum) - tiling = kvp.Parameter("geotiff:tiling", num="?", type=boolean) - tileheight = kvp.Parameter("geotiff:tileheight", num="?", type=parse_multiple_16) - tilewidth = kvp.Parameter("geotiff:tilewidth", num="?", type=parse_multiple_16) - - -class WCS20GeoTIFFEncodingExtensionXMLDecoder(xml.Decoder): - compression = xml.Parameter("wcs:Extension/geotiff:parameters/geotiff:compression/text()", num="?", type=compression_enum, locator="geotiff:compression") - jpeg_quality = xml.Parameter("wcs:Extension/geotiff:parameters/geotiff:jpeg_quality/text()", num="?", type=value_range(1, 100, type=int), locator="geotiff:jpeg_quality") - predictor = xml.Parameter("wcs:Extension/geotiff:parameters/geotiff:predictor/text()", num="?", type=predictor_enum, locator="geotiff:predictor") - interleave = xml.Parameter("wcs:Extension/geotiff:parameters/geotiff:interleave/text()", num="?", type=interleave_enum, locator="geotiff:interleave") - tiling = xml.Parameter("wcs:Extension/geotiff:parameters/geotiff:tiling/text()", num="?", type=boolean, locator="geotiff:tiling") - tileheight = xml.Parameter("wcs:Extension/geotiff:parameters/geotiff:tileheight/text()", num="?", type=parse_multiple_16, locator="geotiff:tileheight") - tilewidth = xml.Parameter("wcs:Extension/geotiff:parameters/geotiff:tilewidth/text()", num="?", type=parse_multiple_16, locator="geotiff:tilewidth") - - namespaces = NameSpaceMap( - ns_wcs, NameSpace("http://www.opengis.net/gmlcov/geotiff/1.0", "geotiff") - ) diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/v20/exceptionhandler.py eoxserver-0.3.2/eoxserver/services/ows/wcs/v20/exceptionhandler.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/v20/exceptionhandler.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wcs/v20/exceptionhandler.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,76 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2011 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -from eoxserver.core import Component, implements -from eoxserver.services.ows.interfaces import ExceptionHandlerInterface -from eoxserver.services.ows.common.v20.encoders import OWS20ExceptionXMLEncoder -from eoxserver.core.decoders import ( - DecodingException, MissingParameterException -) - - -CODES_404 = frozenset(( - "NoSuchCoverage", "NoSuchDatasetSeriesOrCoverage", "InvalidAxisLabel", - "InvalidSubsetting", "InterpolationMethodNotSupported", "NoSuchField", - "InvalidFieldSequence", "InvalidScaleFactor", "InvalidExtent", - "ScaleAxisUndefined", "SubsettingCrs-NotSupported", "OutputCrs-NotSupported" -)) - - -class WCS20ExceptionHandler(Component): - implements(ExceptionHandlerInterface) - - service = "WCS" - versions = ("2.0.0", "2.0.1") - request = None - - def handle_exception(self, request, exception): - message = str(exception) - code = getattr(exception, "code", None) - locator = getattr(exception, "locator", None) - status = 400 - - if code is None: - if isinstance(exception, MissingParameterException): - code = "MissingParameterValue" - elif isinstance(exception, DecodingException): - code = "InvalidParameterValue" - else: - code = "InvalidRequest" - - if code in CODES_404: - status = 404 - elif code in ("OperationNotSupported", "OptionNotSupported"): - status = 501 - - encoder = OWS20ExceptionXMLEncoder() - xml = encoder.serialize( - encoder.encode_exception(message, "2.0.1", code, locator) - ) - - return (xml, encoder.content_type, status) diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/v20/getcapabilities.py eoxserver-0.3.2/eoxserver/services/ows/wcs/v20/getcapabilities.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/v20/getcapabilities.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wcs/v20/getcapabilities.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,113 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2011 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -from eoxserver.core import Component, implements -from eoxserver.core.decoders import xml, kvp, typelist, lower -from eoxserver.resources.coverages import models -from eoxserver.services.ows.interfaces import ( - ServiceHandlerInterface, GetServiceHandlerInterface, - PostServiceHandlerInterface, VersionNegotiationInterface -) -from eoxserver.services.ows.wcs.basehandlers import ( - WCSGetCapabilitiesHandlerBase -) -from eoxserver.services.ows.wcs.v20.util import nsmap, SectionsMixIn -from eoxserver.services.ows.wcs.v20.parameters import ( - WCS20CapabilitiesRenderParams -) - - -class WCS20GetCapabilitiesHandler(WCSGetCapabilitiesHandlerBase, Component): - implements(ServiceHandlerInterface) - implements(GetServiceHandlerInterface) - implements(PostServiceHandlerInterface) - implements(VersionNegotiationInterface) - - versions = ("2.0.0", "2.0.1") - - def get_decoder(self, request): - if request.method == "GET": - return WCS20GetCapabilitiesKVPDecoder(request.GET) - elif request.method == "POST": - return WCS20GetCapabilitiesXMLDecoder(request.body) - - def lookup_coverages(self, decoder): - sections = decoder.sections - inc_coverages = ( - "all" in sections or "contents" in sections - or "coveragesummary" in sections - ) - inc_dataset_series = ( - "all" in sections or "contents" in sections - or "datasetseriessummary" in sections - ) - - if inc_coverages: - coverages = models.Coverage.objects \ - .order_by("identifier") \ - .filter(visible=True) - else: - coverages = () - - if inc_dataset_series: - dataset_series = models.DatasetSeries.objects \ - .order_by("identifier") \ - .exclude( - footprint__isnull=True, begin_time__isnull=True, - end_time__isnull=True - ) - else: - dataset_series = () - - return coverages, dataset_series - - def get_params(self, models, decoder): - coverages, dataset_series = models - return WCS20CapabilitiesRenderParams( - coverages, dataset_series, decoder.sections, - decoder.acceptlanguages, decoder.acceptformats, - decoder.updatesequence - ) - - -class WCS20GetCapabilitiesKVPDecoder(kvp.Decoder, SectionsMixIn): - sections = kvp.Parameter(type=typelist(lower, ","), num="?", default=["all"]) - updatesequence = kvp.Parameter(num="?") - acceptversions = kvp.Parameter(type=typelist(str, ","), num="?") - acceptformats = kvp.Parameter(type=typelist(str, ","), num="?", default=["text/xml"]) - acceptlanguages = kvp.Parameter(type=typelist(str, ","), num="?") - - -class WCS20GetCapabilitiesXMLDecoder(xml.Decoder, SectionsMixIn): - sections = xml.Parameter("ows:Sections/ows:Section/text()", num="*", default=["all"]) - updatesequence = xml.Parameter("@updateSequence", num="?") - acceptversions = xml.Parameter("ows:AcceptVersions/ows:Version/text()", num="*") - acceptformats = xml.Parameter("ows:AcceptFormats/ows:OutputFormat/text()", num="*", default=["text/xml"]) - acceptlanguages = xml.Parameter("ows:AcceptLanguages/ows:Language/text()", num="*") - - namespaces = nsmap diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/v20/getcoverage.py eoxserver-0.3.2/eoxserver/services/ows/wcs/v20/getcoverage.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/v20/getcoverage.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wcs/v20/getcoverage.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,131 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -from itertools import chain - -from eoxserver.core import Component, implements, ExtensionPoint -from eoxserver.core.decoders import xml, kvp, typelist -from eoxserver.services.subset import Subsets -from eoxserver.services.ows.interfaces import ( - ServiceHandlerInterface, GetServiceHandlerInterface, - PostServiceHandlerInterface -) -from eoxserver.services.ows.wcs.basehandlers import WCSGetCoverageHandlerBase -from eoxserver.services.ows.wcs.v20.util import ( - nsmap, parse_subset_kvp, parse_subset_xml, parse_range_subset_kvp, - parse_range_subset_xml, parse_interpolation, - parse_scaleaxis_kvp, parse_scalesize_kvp, parse_scaleextent_kvp, - parse_scaleaxis_xml, parse_scalesize_xml, parse_scaleextent_xml, -) -from eoxserver.services.ows.wcs.v20.parameters import WCS20CoverageRenderParams -from eoxserver.services.ows.wcs.interfaces import EncodingExtensionInterface -from eoxserver.services.exceptions import InvalidRequestException - - -class WCS20GetCoverageHandler(WCSGetCoverageHandlerBase, Component): - implements(ServiceHandlerInterface) - implements(GetServiceHandlerInterface) - implements(PostServiceHandlerInterface) - - encoding_extensions = ExtensionPoint(EncodingExtensionInterface) - - versions = ("2.0.0", "2.0.1") - - def get_decoder(self, request): - if request.method == "GET": - return WCS20GetCoverageKVPDecoder(request.GET) - elif request.method == "POST": - return WCS20GetCoverageXMLDecoder(request.body) - - def get_params(self, coverage, decoder, request): - subsets = Subsets(decoder.subsets, crs=decoder.subsettingcrs) - encoding_params = None - for encoding_extension in self.encoding_extensions: - if encoding_extension.supports(decoder.format, {}): - encoding_params = encoding_extension.get_encoding_params( - request - ) - - scalefactor = decoder.scalefactor - scales = list( - chain(decoder.scaleaxes, decoder.scalesize, decoder.scaleextent) - ) - - # check scales validity: ScaleFactor and any other scale - if scalefactor and scales: - raise InvalidRequestException( - "ScaleFactor and any other scale operation are mutually " - "exclusive.", locator="scalefactor" - ) - - # check scales validity: Axis uniqueness - axes = set() - for scale in scales: - if scale.axis in axes: - raise InvalidRequestException( - "Axis '%s' is scaled multiple times." % scale.axis, - locator=scale.axis - ) - axes.add(scale.axis) - - return WCS20CoverageRenderParams( - coverage, subsets, decoder.rangesubset, decoder.format, - decoder.outputcrs, decoder.mediatype, decoder.interpolation, - scalefactor, scales, encoding_params or {}, request - ) - - -class WCS20GetCoverageKVPDecoder(kvp.Decoder): - coverage_id = kvp.Parameter("coverageid", num=1) - subsets = kvp.Parameter("subset", type=parse_subset_kvp, num="*") - scalefactor = kvp.Parameter("scalefactor", type=float, num="?") - scaleaxes = kvp.Parameter("scaleaxes", type=typelist(parse_scaleaxis_kvp, ","), default=(), num="?") - scalesize = kvp.Parameter("scalesize", type=typelist(parse_scalesize_kvp, ","), default=(), num="?") - scaleextent = kvp.Parameter("scaleextent", type=typelist(parse_scaleextent_kvp, ","), default=(), num="?") - rangesubset = kvp.Parameter("rangesubset", type=parse_range_subset_kvp, num="?") - format = kvp.Parameter("format", num="?") - subsettingcrs = kvp.Parameter("subsettingcrs", num="?") - outputcrs = kvp.Parameter("outputcrs", num="?") - mediatype = kvp.Parameter("mediatype", num="?") - interpolation = kvp.Parameter("interpolation", type=parse_interpolation, num="?") - - -class WCS20GetCoverageXMLDecoder(xml.Decoder): - coverage_id = xml.Parameter("wcs:CoverageId/text()", num=1, locator="coverageid") - subsets = xml.Parameter("wcs:DimensionTrim", type=parse_subset_xml, num="*", locator="subset") - scalefactor = xml.Parameter("wcs:Extension/scal:ScaleByFactor/scal:scaleFactor/text()", type=float, num="?", locator="scalefactor") - scaleaxes = xml.Parameter("wcs:Extension/scal:ScaleByAxesFactor/scal:ScaleAxis", type=parse_scaleaxis_xml, num="*", default=(), locator="scaleaxes") - scalesize = xml.Parameter("wcs:Extension/scal:ScaleToSize/scal:TargetAxisSize", type=parse_scalesize_xml, num="*", default=(), locator="scalesize") - scaleextent = xml.Parameter("wcs:Extension/scal:ScaleToExtent/scal:TargetAxisExtent", type=parse_scaleextent_xml, num="*", default=(), locator="scaleextent") - rangesubset = xml.Parameter("wcs:Extension/rsub:RangeSubset", type=parse_range_subset_xml, num="?", locator="rangesubset") - format = xml.Parameter("wcs:format/text()", num="?", locator="format") - subsettingcrs = xml.Parameter("wcs:Extension/crs:subsettingCrs/text()", num="?", locator="subsettingcrs") - outputcrs = xml.Parameter("wcs:Extension/crs:outputCrs/text()", num="?", locator="outputcrs") - mediatype = xml.Parameter("wcs:mediaType/text()", num="?", locator="mediatype") - interpolation = xml.Parameter("wcs:Extension/int:Interpolation/int:globalInterpolation/text()", type=parse_interpolation, num="?", locator="interpolation") - - namespaces = nsmap diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/v20/geteocoverageset.py eoxserver-0.3.2/eoxserver/services/ows/wcs/v20/geteocoverageset.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/v20/geteocoverageset.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wcs/v20/geteocoverageset.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,317 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -import sys -import os -import tempfile -import logging -from itertools import chain -import mimetypes - -from django.db.models import Q -from django.http import HttpResponse -try: - from django.http import StreamingHttpResponse -except: - StreamingHttpResponse = HttpResponse - -from eoxserver.core import Component, implements, ExtensionPoint -from eoxserver.core.config import get_eoxserver_config -from eoxserver.core.decoders import xml, kvp, typelist, enum -from eoxserver.resources.coverages import models -from eoxserver.services.ows.interfaces import ( - ServiceHandlerInterface, GetServiceHandlerInterface, - PostServiceHandlerInterface -) -from eoxserver.services.ows.wcs.v20.util import ( - nsmap, parse_subset_kvp, parse_subset_xml -) -from eoxserver.services.ows.wcs.v20.parameters import WCS20CoverageRenderParams -from eoxserver.services.ows.common.config import WCSEOConfigReader -from eoxserver.services.ows.wcs.interfaces import ( - WCSCoverageRendererInterface, PackageWriterInterface -) -from eoxserver.services.subset import Subsets, Trim -from eoxserver.services.exceptions import ( - NoSuchDatasetSeriesOrCoverageException, InvalidRequestException, - InvalidSubsettingException -) - - -logger = logging.getLogger(__name__) - - -class WCS20GetEOCoverageSetHandler(Component): - implements(ServiceHandlerInterface) - implements(GetServiceHandlerInterface) - implements(PostServiceHandlerInterface) - - coverage_renderers = ExtensionPoint(WCSCoverageRendererInterface) - package_writers = ExtensionPoint(PackageWriterInterface) - - service = "WCS" - versions = ("2.0.0", "2.0.1") - request = "GetEOCoverageSet" - - index = 21 - - def get_decoder(self, request): - if request.method == "GET": - return WCS20GetEOCoverageSetKVPDecoder(request.GET) - elif request.method == "POST": - return WCS20GetEOCoverageSetXMLDecoder(request.body) - - def get_params(self, coverage, decoder, request): - return WCS20CoverageRenderParams( - coverage, Subsets(decoder.subsets), http_request=request - ) - - def get_renderer(self, params): - for renderer in self.coverage_renderers: - if renderer.supports(params): - return renderer - - raise InvalidRequestException( - "Could not find renderer for coverage '%s'." - ) - - def get_pacakge_writer(self, format, params): - for writer in self.package_writers: - if writer.supports(format, params): - return writer - - raise InvalidRequestException( - "Format '%s' is not supported." % format, locator="format" - ) - - @property - def constraints(self): - reader = WCSEOConfigReader(get_eoxserver_config()) - return { - "CountDefault": reader.paging_count_default - } - - def handle(self, request): - decoder = self.get_decoder(request) - eo_ids = decoder.eo_ids - - format, format_params = decoder.format - writer = self.get_pacakge_writer(format, format_params) - - containment = decoder.containment - - count_default = self.constraints["CountDefault"] - count = decoder.count - if count_default is not None: - count = min(count, count_default) - - try: - subsets = Subsets( - decoder.subsets, - crs="http://www.opengis.net/def/crs/EPSG/0/4326", - allowed_types=Trim - ) - except ValueError, e: - raise InvalidSubsettingException(str(e)) - - if len(eo_ids) == 0: - raise - - # fetch a list of all requested EOObjects - available_ids = models.EOObject.objects.filter( - identifier__in=eo_ids - ).values_list("identifier", flat=True) - - # match the requested EOIDs against the available ones. If any are - # requested, that are not available, raise and exit. - failed = [eo_id for eo_id in eo_ids if eo_id not in available_ids] - if failed: - raise NoSuchDatasetSeriesOrCoverageException(failed) - - collections_qs = subsets.filter(models.Collection.objects.filter( - identifier__in=eo_ids - ), containment="overlaps") - - # create a set of all indirectly referenced containers by iterating - # recursively. The containment is set to "overlaps", to also include - # collections that might have been excluded with "contains" but would - # have matching coverages inserted. - - def recursive_lookup(super_collection, collection_set): - sub_collections = models.Collection.objects.filter( - collections__in=[super_collection.pk] - ).exclude( - pk__in=map(lambda c: c.pk, collection_set) - ) - sub_collections = subsets.filter(sub_collections, "overlaps") - - # Add all to the set - collection_set |= set(sub_collections) - - for sub_collection in sub_collections: - recursive_lookup(sub_collection, collection_set) - - collection_set = set(collections_qs) - for collection in set(collection_set): - recursive_lookup(collection, collection_set) - - collection_pks = map(lambda c: c.pk, collection_set) - - # Get all either directly referenced coverages or coverages that are - # within referenced containers. Full subsetting is applied here. - - coverages_qs = models.Coverage.objects.filter( - Q(identifier__in=eo_ids) | Q(collections__in=collection_pks) - ) - coverages_qs = subsets.filter(coverages_qs, containment=containment) - - # save a reference before limits are applied to obtain the full number - # of matched coverages. - coverages_no_limit_qs = coverages_qs - - # compute how many (if any) coverages can be retrieved. This depends on - # the "count" parameter and default setting. Also, if we already - # exceeded the count, limit the number of dataset series aswell - """ - if inc_dss_section: - num_collections = len(collection_set) - else: - num_collections = 0 - - if num_collections < count and inc_cov_section: - coverages_qs = coverages_qs.order_by("identifier")[:count - num_collections] - elif num_collections == count or not inc_cov_section: - coverages_qs = [] - else: - coverages_qs = [] - collection_set = sorted(collection_set, key=lambda c: c.identifier)[:count] - """ - - # get a number of coverages that *would* have been included, but are not - # because of the count parameter - # count_all_coverages = coverages_no_limit_qs.count() - - # TODO: if containment is "within" we need to check all collections - # again - if containment == "within": - collection_set = filter(lambda c: subsets.matches(c), collection_set) - - coverages = [] - dataset_series = [] - - # finally iterate over everything that has been retrieved and get - # a list of dataset series and coverages to be encoded into the response - for eo_object in chain(coverages_qs, collection_set): - if issubclass(eo_object.real_type, models.Coverage): - coverages.append(eo_object.cast()) - - fd, pkg_filename = tempfile.mkstemp() - tmp = os.fdopen(fd) - tmp.close() - package = writer.create_package(pkg_filename, format, format_params) - - for coverage in coverages: - params = self.get_params(coverage, decoder, request) - renderer = self.get_renderer(params) - result_set = renderer.render(params) - all_filenames = set() - for result_item in result_set: - if not result_item.filename: - ext = mimetypes.guess_extension(result_item.content_type) - filename = coverage.identifier + ext - else: - filename = result_item.filename - if filename in all_filenames: - continue # TODO: create new filename - all_filenames.add(filename) - location = "%s/%s" % (coverage.identifier, filename) - writer.add_to_package( - package, result_item.data_file, result_item.size, location - ) - - mime_type = writer.get_mime_type(package, format, format_params) - ext = writer.get_file_extension(package, format, format_params) - writer.cleanup(package) - - response = StreamingHttpResponse( - tempfile_iterator(pkg_filename), mime_type - ) - response["Content-Disposition"] = 'inline; filename="ows%s"' % ext - response["Content-Length"] = str(os.path.getsize(pkg_filename)) - - return response - - -def tempfile_iterator(filename, chunksize=2048, delete=True): - with open(filename) as file_obj: - while True: - data = file_obj.read(chunksize) - if not data: - break - yield data - - if delete: - os.remove(filename) - - -def pos_int(value): - value = int(value) - if value < 0: - raise ValueError("Negative values are not allowed.") - return value - - -containment_enum = enum( - ("overlaps", "contains"), False -) - - -def parse_format(string): - parts = string.split(";") - params = dict( - param.strip().split("=", 1) for param in parts[1:] - ) - return parts[0], params - - -class WCS20GetEOCoverageSetKVPDecoder(kvp.Decoder): - eo_ids = kvp.Parameter("eoid", type=typelist(str, ","), num=1, locator="eoid") - subsets = kvp.Parameter("subset", type=parse_subset_kvp, num="*") - containment = kvp.Parameter(type=containment_enum, num="?") - count = kvp.Parameter(type=pos_int, num="?", default=sys.maxint) - format = kvp.Parameter(num=1, type=parse_format) - - -class WCS20GetEOCoverageSetXMLDecoder(xml.Decoder): - eo_ids = xml.Parameter("/wcseo:EOID/text()", num="+", locator="eoid") - subsets = xml.Parameter("/wcs:DimensionTrim", type=parse_subset_xml, num="*") - containment = xml.Parameter("/wcseo:containment/text()", type=containment_enum, locator="containment") - count = xml.Parameter("/@count", type=pos_int, num="?", default=sys.maxint, locator="count") - format = xml.Parameter("/wcs:format/text()", type=parse_format, num=1, locator="format") - - namespaces = nsmap diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/v20/packages/tar.py eoxserver-0.3.2/eoxserver/services/ows/wcs/v20/packages/tar.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/v20/packages/tar.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wcs/v20/packages/tar.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,79 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -import tarfile - -from eoxserver.core import Component, implements -from eoxserver.services.ows.wcs.interfaces import ( - PackageWriterInterface -) - - -gzip_mimes = ("application/gzip", "application/x-gzip") -bzip_mimes = ("application/bzip", "application/x-bzip") -mime_list = ("application/tar", "application/x-tar") + gzip_mimes + bzip_mimes - - -class TarPackageWriter(Component): - """ Package writer for compressed and uncompressed tar files. - """ - - implements(PackageWriterInterface) - - def supports(self, format, params): - return format.lower() in mime_list - - def create_package(self, filename, format, params): - if format in gzip_mimes: - mode = "w:gz" - elif format in bzip_mimes: - mode = "w:bz2" - else: - mode = "w" - - return tarfile.open(filename, mode) - - def cleanup(self, package): - package.close() - - def add_to_package(self, package, file_obj, size, location): - info = tarfile.TarInfo(location) - info.size = size - package.addfile(info, file_obj) - - def get_mime_type(self, package, format, params): - return "application/x-compressed-tar" - - def get_file_extension(self, package, format, params):# - if format in gzip_mimes: - return ".tar.gz" - - elif format in bzip_mimes: - return ".tar.bz2" - - return ".tar" diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/v20/packages/zip.py eoxserver-0.3.2/eoxserver/services/ows/wcs/v20/packages/zip.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/v20/packages/zip.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wcs/v20/packages/zip.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,59 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -import zipfile - -from eoxserver.core import Component, implements -from eoxserver.services.ows.wcs.interfaces import ( - PackageWriterInterface -) - -class ZipPackageWriter(Component): - implements(PackageWriterInterface) - - def supports(self, format, params): - return format.lower() == "application/zip" - - def create_package(self, filename, format, params): - compression = zipfile.ZIP_STORED - if params.get("compression", "").upper() == "DEFLATED": - print compression - compression = zipfile.ZIP_DEFLATED - return zipfile.ZipFile(filename, "a", compression) - - def cleanup(self, package): - package.close() - - def add_to_package(self, package, file_obj, size, location): - package.writestr(location, file_obj.read()) - - def get_mime_type(self, package, format, params): - return "application/zip" - - def get_file_extension(self, package, format, params): - return ".zip" \ No newline at end of file diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/v20/parameters.py eoxserver-0.3.2/eoxserver/services/ows/wcs/v20/parameters.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/v20/parameters.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wcs/v20/parameters.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,126 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -from eoxserver.core.util.timetools import isoformat -from eoxserver.services.subset import Slice -from eoxserver.services.ows.wcs.parameters import ( - CoverageRenderParams, CoverageDescriptionRenderParams, - WCSCapabilitiesRenderParams -) - - -class WCS20CapabilitiesRenderParams(WCSCapabilitiesRenderParams): - def __init__(self, coverages, dataset_series=None, sections=None, - accept_languages=None, accept_formats=None, - updatesequence=None, request=None): - super(WCS20CapabilitiesRenderParams, self).__init__( - coverages, "2.0.1", sections, accept_languages, accept_formats, - updatesequence, request - ) - self._dataset_series = dataset_series or () - - dataset_series = property(lambda self: self._dataset_series) - - -class WCS20CoverageDescriptionRenderParams(CoverageDescriptionRenderParams): - coverage_ids_key_name = "coverageid" - - def __init__(self, coverages): - super(WCS20CoverageDescriptionRenderParams, self).__init__( - coverages, "2.0.1" - ) - - -class WCS20CoverageRenderParams(CoverageRenderParams): - def __init__(self, coverage, subsets=None, rangesubset=None, format=None, - outputcrs=None, mediatype=None, interpolation=None, - scalefactor=None, scales=None, encoding_params=None, - http_request=None): - - super(WCS20CoverageRenderParams, self).__init__(coverage, "2.0.1") - self._subsets = subsets - self._rangesubset = rangesubset or () - self._scalefactor = scalefactor - self._scales = scales or () - self._format = format - self._outputcrs = outputcrs - self._mediatype = mediatype - self._interpolation = interpolation - self._encoding_params = encoding_params or {} - self._http_request = http_request - - - coverage_id_key_name = "coverageid" - - subsets = property(lambda self: self._subsets) - rangesubset = property(lambda self: self._rangesubset) - scalefactor = property(lambda self: self._scalefactor) - scales = property(lambda self: self._scales) - format = property(lambda self: self._format) - outputcrs = property(lambda self: self._outputcrs) - mediatype = property(lambda self: self._mediatype) - interpolation = property(lambda self: self._interpolation) - encoding_params = property(lambda self: self._encoding_params) - http_request = property(lambda self: self._http_request) - - - def __iter__(self): - for k, v in super(WCS20CoverageRenderParams, self).__iter__(): - yield k, v - - for subset in self.subsets: - yield self.subset_to_kvp(subset) - - if self.format: - yield ("format", self.format) - - if self.outputcrs: - yield ("outputcrs", self.outputcrs) - - if self.mediatype: - yield ("mediatype", self.mediatype) - - if self.interpolation: - yield ("interpolation", self.interpolation) - - - def subset_to_kvp(self, subset): - temporal_format = lambda v: ('"%s"' % isoformat(v) if v else "*") - spatial_format = lambda v: (str(v) if v is not None else "*") - - frmt = temporal_format if subset.is_temporal else spatial_format - - if isinstance(subset, Slice): - value = frmt(subset.value) - else: - value = "%s,%s" % (frmt(subset.low), frmt(subset.high)) - - crs = self.subsets.crs - if crs: - return "subset", "%s,%s(%s)" % (subset.axis, crs, value) - else: - return "subset", "%s(%s)" % (subset.axis, value) diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/v20/util.py eoxserver-0.3.2/eoxserver/services/ows/wcs/v20/util.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/v20/util.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wcs/v20/util.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,417 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -import re -from datetime import datetime - -from lxml.builder import ElementMaker - -from eoxserver.core.util.xmltools import NameSpace, NameSpaceMap, ns_xsi -from eoxserver.core.util.timetools import parse_iso8601 -from eoxserver.services.subset import Trim, Slice, is_temporal, all_axes -from eoxserver.services.gml.v32.encoders import ( - ns_gml, ns_gmlcov, ns_om, ns_eop, GML, GMLCOV, OM, EOP -) -from eoxserver.services.ows.common.v20.encoders import ns_xlink, ns_ows, OWS -from eoxserver.services.exceptions import ( - InvalidSubsettingException, InvalidAxisLabelException, - NoSuchFieldException, InvalidFieldSequenceException, - InterpolationMethodNotSupportedException, InvalidScaleFactorException, - InvalidScaleExtentException, ScaleAxisUndefinedException -) - - -# namespace declarations -ns_ogc = NameSpace("http://www.opengis.net/ogc", "ogc") -ns_wcs = NameSpace("http://www.opengis.net/wcs/2.0", "wcs") -ns_crs = NameSpace("http://www.opengis.net/wcs/crs/1.0", "crs") -ns_rsub = NameSpace("http://www.opengis.net/wcs/range-subsetting/1.0", "rsub") -ns_eowcs = NameSpace("http://www.opengis.net/wcs/wcseo/1.0", "wcseo", - "http://schemas.opengis.net/wcs/wcseo/1.0/wcsEOAll.xsd") -ns_swe = NameSpace("http://www.opengis.net/swe/2.0", "swe") -ns_int = NameSpace("http://www.opengis.net/wcs/interpolation/1.0", "int") -ns_scal = NameSpace("http://www.opengis.net/wcs/scaling/1.0", "scal") - -# namespace map -nsmap = NameSpaceMap( - ns_xlink, ns_ogc, ns_ows, ns_gml, ns_gmlcov, ns_wcs, ns_crs, ns_rsub, - ns_eowcs, ns_om, ns_eop, ns_swe, ns_int, ns_scal -) - -# Element factories - -WCS = ElementMaker(namespace=ns_wcs.uri, nsmap=nsmap) -CRS = ElementMaker(namespace=ns_crs.uri, nsmap=nsmap) -EOWCS = ElementMaker(namespace=ns_eowcs.uri, nsmap=nsmap) -SWE = ElementMaker(namespace=ns_swe.uri, nsmap=nsmap) -INT = ElementMaker(namespace=ns_int.uri, nsmap=nsmap) - - -SUBSET_RE = re.compile(r'(\w+)\(([^,]*)(,([^)]*))?\)') -SCALEAXIS_RE = re.compile(r'(\w+)\(([^)]*)\)') -SCALESIZE_RE = SCALEAXIS_RE -SCALEEXTENT_RE = re.compile(r'(\w+)\(([^:]*):([^)]*)\)') - - -class RangeSubset(list): - def get_band_indices(self, range_type, offset=0): - current_idx = -1 - all_bands = range_type.cached_bands[:] - - for subset in self: - if isinstance(subset, basestring): - # slice, i.e single band - start = stop = subset - - else: - start, stop = subset - - start_idx = self._find(all_bands, start) - if start != stop: - stop_idx = self._find(all_bands, stop) - if stop_idx <= start_idx: - raise IllegalFieldSequenceException( - "Invalid interval '%s:%s'." % (start, stop), start - ) - - # expand interval to indices - for i in range(start_idx, stop_idx+1): - yield i + offset - - else: - # return the item - yield start_idx + offset - - - def _find(self, all_bands, name): - for i, band in enumerate(all_bands): - if band.name == name or band.identifier == name: - return i - raise NoSuchFieldException("Field '%s' does not exist." % name, name) - - -class Scale(object): - """ Abstract base class for all Scaling operations. - """ - def __init__(self, axis): - self.axis = axis - - -class ScaleAxis(Scale): - """ Scale a single axis by a specific value. - """ - def __init__(self, axis, scale): - super(ScaleAxis, self).__init__(axis) - self.scale = scale - - -class ScaleSize(Scale): - """ Scale a single axis to a specific size. - """ - def __init__(self, axis, size): - super(ScaleSize, self).__init__(axis) - self.size = size - - -class ScaleExtent(Scale): - """ Scale a single axis to a specific extent. - """ - def __init__(self, axis, low, high): - super(ScaleExtent, self).__init__(axis) - self.low = low - self.high = high - - -class SectionsMixIn(object): - """ Mix-in for request decoders that use sections. - """ - - def section_included(self, *sections): - """ See if one of the sections is requested. - """ - if not self.sections: - return True - - requested_sections = map(lambda s: s.lower(), self.sections) - - for section in map(lambda s: s.lower(), sections): - section = section.lower() - if "all" in requested_sections or section in requested_sections: - return True - - return False - - -def parse_subset_kvp(string): - """ Parse one subset from the WCS 2.0 KVP notation. - """ - - try: - match = SUBSET_RE.match(string) - if not match: - raise Exception("Could not parse input subset string.") - - axis = match.group(1) - parser = get_parser_for_axis(axis) - - if match.group(4) is not None: - return Trim( - axis, parser(match.group(2)), parser(match.group(4)) - ) - else: - return Slice(axis, parser(match.group(2))) - except InvalidAxisLabelException: - raise - except Exception, e: - raise InvalidSubsettingException(str(e)) - - -def parse_range_subset_kvp(string): - """ Parse a rangesubset structure from the WCS 2.0 KVP notation. - """ - - rangesubset = RangeSubset() - for item in string.split(","): - if ":" in item: - rangesubset.append(item.split(":")) - else: - rangesubset.append(item) - - return rangesubset - - -def parse_scaleaxis_kvp(string): - """ Parses the KVP notation of a single scale axis. - """ - - match = SCALEAXIS_RE.match(string) - if not match: - raise Exception("Could not parse input scale axis string.") - - axis = match.group(1) - if axis not in all_axes: - raise ScaleAxisUndefinedException(axis) - try: - value = float(match.group(2)) - except ValueError: - raise InvalidScaleFactorException(match.group(2)) - - return ScaleAxis(axis, value) - - -def parse_scalesize_kvp(string): - """ Parses the KVP notation of a single scale size. - """ - - match = SCALESIZE_RE.match(string) - if not match: - raise Exception("Could not parse input scale size string.") - - axis = match.group(1) - if axis not in all_axes: - raise ScaleAxisUndefinedException(axis) - try: - value = int(match.group(2)) - except ValueError: - raise InvalidScaleFactorException(match.group(2)) - - return ScaleSize(axis, value) - - -def parse_scaleextent_kvp(string): - """ Parses the KVP notation of a single scale extent. - """ - - match = SCALEEXTENT_RE.match(string) - if not match: - raise Exception("Could not parse input scale extent string.") - - axis = match.group(1) - if axis not in all_axes: - raise ScaleAxisUndefinedException(axis) - try: - low = int(match.group(2)) - high = int(match.group(3)) - except ValueError: - raise InvalidScaleFactorException(match.group(3)) - - if low >= high: - raise InvalidScaleExtentException(low, high) - - return ScaleExtent(axis, low, high) - - -def parse_subset_xml(elem): - """ Parse one subset from the WCS 2.0 XML notation. Expects an lxml.etree - Element as parameter. - """ - - try: - dimension = elem.findtext(ns_wcs("Dimension")) - parser = get_parser_for_axis(dimension) - if elem.tag == ns_wcs("DimensionTrim"): - return Trim( - dimension, - parser(elem.findtext(ns_wcs("TrimLow"))), - parser(elem.findtext(ns_wcs("TrimHigh"))) - ) - elif elem.tag == ns_wcs("DimensionSlice"): - return Slice( - dimension, - parser(elem.findtext(ns_wcs("SlicePoint"))) - ) - except Exception, e: - raise InvalidSubsettingException(str(e)) - - -SUPPORTED_INTERPOLATIONS = ( - "average", "nearest-neighbour", "bilinear", "cubic", "cubic-spline", - "lanczos", "mode" -) - -def parse_interpolation(raw): - """ Returns a unified string denoting the interpolation method used. - """ - if raw.startswith("http://www.opengis.net/def/interpolation/OGC/1/"): - raw = raw[len("http://www.opengis.net/def/interpolation/OGC/1/"):] - value = raw.lower() - else: - value = raw.lower() - - if value not in SUPPORTED_INTERPOLATIONS: - raise InterpolationMethodNotSupportedException( - "Interpolation method '%s' is not supported." % raw - ) - return value - - -def parse_range_subset_xml(elem): - """ Parse a rangesubset structure from the WCS 2.0 XML notation. - """ - - rangesubset = RangeSubset() - - for child in elem: - item = child[0] - if item.tag == ns_rsub("RangeComponent"): - rangesubset.append(item.text) - elif item.tag == ns_rsub("RangeInterval"): - rangesubset.append(( - item.findtext(ns_rsub("startComponent")), - item.findtext(ns_rsub("endComponent")) - )) - - return rangesubset - - -def parse_scaleaxis_xml(elem): - """ Parses the XML notation of a single scale axis. - """ - - axis = elem.findtext(ns_scal("axis")) - if axis not in all_axes: - raise ScaleAxisUndefinedException(axis) - try: - raw = elem.findtext(ns_scal("scaleFactor")) - value = float(raw) - except ValueError: - InvalidScaleFactorException(raw) - - return ScaleAxis(axis, value) - - -def parse_scalesize_xml(elem): - """ Parses the XML notation of a single scale size. - """ - - axis = elem.findtext(ns_scal("axis")) - if axis not in all_axes: - raise ScaleAxisUndefinedException(axis) - try: - raw = elem.findtext(ns_scal("targetSize")) - value = int(raw) - except ValueError: - InvalidScaleFactorException(raw) - - return ScaleSize(axis, value) - - -def parse_scaleextent_xml(elem): - """ Parses the XML notation of a single scale extent. - """ - - axis = elem.findtext(ns_scal("axis")) - if axis not in all_axes: - raise ScaleAxisUndefinedException(axis) - try: - raw_low = elem.findtext(ns_scal("low")) - raw_high = elem.findtext(ns_scal("high")) - low = int(raw_low) - high = int(raw_high) - except ValueError: - InvalidScaleFactorException(raw_high) - - if low >= high: - raise InvalidScaleExtentException(low, high) - - return ScaleExtent(axis, low, high) - - -def float_or_star(value): - """ Parses a string value that is either a floating point value or the '*' - character. Raises a `ValueError` if no float could be parsed. - """ - - if value == "*": - return None - return float(value) - - -def parse_quoted_temporal(value): - """ Parses a quoted temporal value. - """ - - if value == "*": - return None - - if not value[0] == '"' and not value[-1] == '"': - raise ValueError( - "Temporal value needs to be quoted with double quotes." - ) - - return parse_iso8601(value[1:-1]) - - -def get_parser_for_axis(axis): - """ Returns the correct parsing function for the given axis. - """ - - if is_temporal(axis): - return parse_quoted_temporal - else: - return float_or_star diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/wcs20/common.py eoxserver-0.3.2/eoxserver/services/ows/wcs/wcs20/common.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/wcs20/common.py 1970-01-01 00:00:00.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wcs/wcs20/common.py 2013-12-10 14:57:00.000000000 +0000 @@ -0,0 +1,51 @@ +#------------------------------------------------------------------------------- +# $Id$ +# +# Project: EOxServer +# Authors: Stephan Meissl +# +#------------------------------------------------------------------------------- +# Copyright (C) 2013 EOX IT Services GmbH +# +# 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 of this Software or works derived from this 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. +#------------------------------------------------------------------------------- + +""" +This module contains handlers and functions commonly used by the different WCS +2.0 operation implementations. +""" + +from eoxserver.core.system import System + + +class WCS20ConfigReader(object): + REGISTRY_CONF = { + "name": "WCS 2.0 Configuration Reader", + "impl_id": "services.ows.wcs20.WCS20ConfigReader" + } + + def validate(self, config): + pass + + def getPagingCountDefault(self): + value = System.getConfig().getConfigValue("services.ows.wcs20", "paging_count_default") + if value is not None: + return int(value) + + return value diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/wcs20/desccov.py eoxserver-0.3.2/eoxserver/services/ows/wcs/wcs20/desccov.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/wcs20/desccov.py 1970-01-01 00:00:00.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wcs/wcs20/desccov.py 2013-12-10 14:57:00.000000000 +0000 @@ -0,0 +1,118 @@ +#------------------------------------------------------------------------------- +# $Id$ +# +# Project: EOxServer +# Authors: Stephan Krause +# Stephan Meissl +# +#------------------------------------------------------------------------------- +# Copyright (C) 2011 EOX IT Services GmbH +# +# 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 of this Software or works derived from this 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. +#------------------------------------------------------------------------------- + +""" +This module contains the handler for WCS 2.0 / EO-WCS DescribeCoverage requests. +""" + +from eoxserver.core.system import System +from eoxserver.core.util.xmltools import DOMElementToXML +from eoxserver.services.base import BaseRequestHandler +from eoxserver.services.requests import Response +from eoxserver.services.exceptions import InvalidRequestException +from eoxserver.services.ows.wcs.encoders import WCS20EOAPEncoder + +class WCS20DescribeCoverageHandler(BaseRequestHandler): + """ + This handler generates responses to WCS 2.0 / EO-WCS DescribeCoverage + requests. It inherits directly from :class:`~.BaseRequestHandler` and + does NOT reuse MapServer. + + The workflow implemented by the handler starts with the + :meth:`createCoverages` method and generates the coverage descriptions + using the :class:`~.WCS20EOAPEncoder` method + :meth:`~.WCS20EOAPEncoder.encodeCoverageDescriptions`. + """ + + REGISTRY_CONF = { + "name": "WCS 2.0 DescribeCoverage Handler", + "impl_id": "services.ows.wcs20.WCS20DescribeCoverageHandler", + "registry_values": { + "services.interfaces.service": "wcs", + "services.interfaces.version": "2.0.0", + "services.interfaces.operation": "describecoverage" + } + } + + PARAM_SCHEMA = { + "service": {"xml_location": "/@service", "xml_type": "string", "kvp_key": "service", "kvp_type": "string"}, + "version": {"xml_location": "/@version", "xml_type": "string", "kvp_key": "version", "kvp_type": "string"}, + "operation": {"xml_location": "/", "xml_type": "localName", "kvp_key": "request", "kvp_type": "string"}, + "coverageids": {"xml_location": "/{http://www.opengis.net/wcs/2.0}CoverageId", "xml_type": "string[]", "kvp_key": "coverageid", "kvp_type": "stringlist"} + } + + def _processRequest(self, req): + req.setSchema(self.PARAM_SCHEMA) + + self.createCoverages(req) + + encoder = WCS20EOAPEncoder() + + return Response( + content=DOMElementToXML(encoder.encodeCoverageDescriptions(req.coverages, True)), # TODO: Distinguish between encodeEOCoverageDescriptions and encodeCoverageDescription? + content_type="text/xml", + status=200 + ) + + def createCoverages(self, req): + """ + This method retrieves the coverage metadata for the coverages denoted + by the coverageid parameter of the request. It raises an + :exc:`~.InvalidRequestException` if the coverageid parameter is + missing or if it contains an unknown coverage ID. + """ + coverage_ids = req.getParamValue("coverageids") + + if coverage_ids is None: + raise InvalidRequestException("Missing 'coverageid' parameter.", "MissingParameterValue", "coverageid") + else: + for coverage_id in coverage_ids: + coverage = System.getRegistry().getFromFactory( + "resources.coverages.wrappers.EOCoverageFactory", + {"obj_id": coverage_id} + ) + if coverage is None: + raise InvalidRequestException( + "No coverage with coverage id '%s' found" % coverage_id, + "NoSuchCoverage", + coverage_id + ) + + req.coverages.append(coverage) + +class WCS20CorrigendumDescribeCoverageHandler(WCS20DescribeCoverageHandler): + REGISTRY_CONF = { + "name": "WCS 2.0 DescribeCoverage Handler", + "impl_id": "services.ows.wcs20.WCS20CorrigendumDescribeCoverageHandler", + "registry_values": { + "services.interfaces.service": "wcs", + "services.interfaces.version": "2.0.1", + "services.interfaces.operation": "describecoverage" + } + } diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/wcs20/desceo.py eoxserver-0.3.2/eoxserver/services/ows/wcs/wcs20/desceo.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/wcs20/desceo.py 1970-01-01 00:00:00.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wcs/wcs20/desceo.py 2013-12-10 14:57:00.000000000 +0000 @@ -0,0 +1,234 @@ +#------------------------------------------------------------------------------- +# $Id$ +# +# Project: EOxServer +# Authors: Stephan Krause +# Stephan Meissl +# +#------------------------------------------------------------------------------- +# Copyright (C) 2011 EOX IT Services GmbH +# +# 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 of this Software or works derived from this 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. +#------------------------------------------------------------------------------- + +""" +This method provides a handler for EO-WCS DescribeEOCoverageSet operations. +""" + +import sys + +from eoxserver.core.system import System +from eoxserver.core.util.xmltools import DOMElementToXML +from eoxserver.resources.coverages.helpers import CoverageSet +from eoxserver.services.base import BaseRequestHandler +from eoxserver.services.requests import Response +from eoxserver.services.exceptions import ( + InvalidRequestException, InvalidSubsettingException, + InvalidAxisLabelException +) +from eoxserver.services.ows.wcs.encoders import WCS20EOAPEncoder +from eoxserver.services.ows.wcs.wcs20.common import WCS20ConfigReader +from eoxserver.services.ows.wcs.wcs20.subset import WCS20SubsetDecoder + + +class WCS20DescribeEOCoverageSetHandler(BaseRequestHandler): + """ + This handler generates responses to EO-WCS DescribeEOCoverageSet requests. + It derives directly from :class:`~.BaseRequestHandler` and does not + reuse MapServer (as MapServer does not support EO-WCS). + + The implented workflow begins with a call to :meth:`createWCSEOObjects` + and then goes on to encode the EO coverage and Dataset Series metadata. + + The handler is aware of the count and sections parameters of + DescribeEOCoverageSet which allow to limit the number of coverage + and Dataset Series descriptions returned and the sections + (CoverageDescriptions, DatasetSeriesDescriptions, All) included in the + requests. + + An :exc:`~.InvalidRequestException` will be raised if incorrect parameters + are encountered or the mandatory eoid parameter is missing. + """ + + REGISTRY_CONF = { + "name": "WCS 2.0 EO-AP DescribeEOCoverageSet Handler", + "impl_id": "services.ows.wcs20.WCS20DescribeEOCoverageSetHandler", + "registry_values": { + "services.interfaces.service": "wcs", + "services.interfaces.version": "2.0.0", + "services.interfaces.operation": "describeeocoverageset" + } + } + + PARAM_SCHEMA = { + "service": {"xml_location": "/service", "xml_type": "string", "kvp_key": "service", "kvp_type": "string"}, + "version": {"xml_location": "/version", "xml_type": "string", "kvp_key": "version", "kvp_type": "string"}, + "operation": {"xml_location": "/", "xml_type": "localName", "kvp_key": "request", "kvp_type": "string"}, + "eoid": {"xml_location": "/{http://www.opengis.net/wcseo/1.0}eoId", "xml_type": "string[]", "kvp_key": "eoid", "kvp_type": "stringlist"}, # TODO: what about multiple ids + "containment": {"xml_location": "/{http://www.opengis.net/wcseo/1.0}containment", "xml_type": "string", "kvp_key": "containment", "kvp_type": "string"}, + "trims": {"xml_location": "/{http://www.opengis.net/wcs/2.0}DimensionTrim", "xml_type": "element[]"}, + "slices": {"xml_location": "/{http://www.opengis.net/wcs/2.0}DimensionSlice", "xml_type": "element[]"}, + "count": {"xml_location": "/@count", "xml_type": "string", "kvp_key": "count", "kvp_type": "string"}, #TODO: kvp location + "sections": {"xml_location": "/ows:section", "xml_type": "string[]", "kvp_key": "sections", "kvp_type": "stringlist"} + } + + def _processRequest(self, req): + + req.setSchema(self.PARAM_SCHEMA) + + dataset_series_set, coverages = self.createWCSEOObjects(req) + + if req.getParamValue("count") is not None: + try: + count_req = int(req.getParamValue("count")) + except: + raise InvalidRequestException( + "Non-integer 'count' parameter.", + "InvalidParameterValue", + "count" + ) + + if count_req < 0: + raise InvalidRequestException( + "Negative 'count' parameter.", + "InvalidParameterValue", + "count" + ) + + else: + count_req = sys.maxint + + + count_default = WCS20ConfigReader().getPagingCountDefault() + count_used = count_req + if count_default is not None: + count_used = min(count_req, count_default) + + count_all_coverages = len(coverages) + if count_used < count_all_coverages: + coverages = coverages[:count_used] + else: + count_used = count_all_coverages + + encoder = WCS20EOAPEncoder() + + # "sections" parameter can be on of: CoverageDescriptions, DatasetSeriesDescriptions, or All. + sections = req.getParamValue("sections") + if sections is None or len(sections) == 0 or\ + "CoverageDescriptions" in sections or\ + "DatasetSeriesDescriptions" in sections or\ + "All" in sections: + if sections is not None and len(sections) != 0 and\ + "DatasetSeriesDescriptions" not in sections and "All" not in sections: + dataset_series_set = None + if sections is not None and len(sections) != 0 and\ + "CoverageDescriptions" not in sections and "All" not in sections: + coverages = None + + return Response( + content=DOMElementToXML( + encoder.encodeEOCoverageSetDescription( + dataset_series_set, + coverages, + count_all_coverages, + count_used + ) + ), + content_type="text/xml", + status=200 + ) + else: + raise InvalidRequestException("'sections' parameter must be either 'CoverageDescriptions', 'DatasetSeriesDescriptions', or 'All'.", "InvalidParameterValue", "sections") + + def createWCSEOObjects(self, req): + """ + This method returns a tuple ``(dataset_series_set, coverages)`` of + two lists containing Dataset Series or EO Coverage objects respectively. + It parses the request parameters in ``req`` in order to determine the + subset of EO-WCS objects to be included. + + The method makes use of + :meth:`~.WCS20SubsetDecoder.getFilterExpressions` in order to parse + subset expressions sent with the request and to obtain filter + expressions that restrict the subset of EO-WCS objects to be included. + + The method will raise :exc:`~.InvalidRequestException` if parameters + are missing, subset expressions are invalid or if the eoid parameter + contains unknown names. + """ + + eo_ids = req.getParamValue("eoid") + + try: + filter_exprs = \ + WCS20SubsetDecoder(req, 4326).getFilterExpressions() + except InvalidSubsettingException, e: + raise InvalidRequestException(e.msg, "InvalidSubsetting", "subset") + except InvalidAxisLabelException, e: + raise InvalidRequestException(e.msg, "InvalidAxisLabel", "subset") + + if eo_ids is None: + raise InvalidRequestException( + "Missing 'eoid' parameter", + "MissingParameterValue", + "eoid" + ) + else: + dataset_series_set = [] + coverages = CoverageSet() + + for eo_id in eo_ids: + dataset_series = System.getRegistry().getFromFactory( + "resources.coverages.wrappers.DatasetSeriesFactory", + {"obj_id": eo_id} + ) + if dataset_series is not None: + dataset_series_set.append(dataset_series) + coverages.union(dataset_series.getEOCoverages(filter_exprs)) + + else: + coverage = System.getRegistry().getFromFactory( + "resources.coverages.wrappers.EOCoverageFactory", + {"obj_id": eo_id} + ) + if coverage is not None: + if coverage.matches(filter_exprs): + coverages.add(coverage) + for dataset in coverage.getDatasets(filter_exprs): + coverages.add(dataset) + else: + raise InvalidRequestException( + "No coverage or dataset series with EO ID '%s' found" % eo_id, + "NoSuchCoverage", + "eoid" + ) + + return (dataset_series_set, coverages.to_sorted_list()) + + +class WCS20CorrigendumDescribeEOCoverageSetHandler(WCS20DescribeEOCoverageSetHandler): + REGISTRY_CONF = { + "name": "WCS 2.0 EO-AP DescribeEOCoverageSet Handler", + "impl_id": "services.ows.wcs20.WCS20CorrigendumDescribeEOCoverageSetHandler", + "registry_values": { + "services.interfaces.service": "wcs", + "services.interfaces.version": "2.0.1", + "services.interfaces.operation": "describeeocoverageset" + } + } diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/wcs20/getcap.py eoxserver-0.3.2/eoxserver/services/ows/wcs/wcs20/getcap.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/wcs20/getcap.py 1970-01-01 00:00:00.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wcs/wcs20/getcap.py 2013-12-10 14:57:00.000000000 +0000 @@ -0,0 +1,307 @@ +#------------------------------------------------------------------------------- +# $Id$ +# +# Project: EOxServer +# Authors: Stephan Krause +# Stephan Meissl +# +#------------------------------------------------------------------------------- +# Copyright (C) 2011 EOX IT Services GmbH +# +# 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 of this Software or works derived from this 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. +#------------------------------------------------------------------------------- + +""" +This module provides handlers for WCS 2.0 / EO-WCS GetCapabilities requests. +""" + +from xml.dom import minidom + +from eoxserver.core.system import System +from eoxserver.core.util.xmltools import DOMtoXML +from eoxserver.core.exceptions import InternalError +from eoxserver.services.owscommon import OWSCommonConfigReader +from eoxserver.services.ows.wcs.common import WCSCommonHandler, \ + getMSOutputFormatsAll,getMSWCSFormatMD,getMSWCSSRSMD +from eoxserver.services.ows.wcs.encoders import WCS20EOAPEncoder +from eoxserver.services.ows.wcs.wcs20.common import WCS20ConfigReader +from eoxserver.services.ows.wcst.wcst11AlterCapabilities import wcst11AlterCapabilities20 + + +class WCS20GetCapabilitiesHandler(WCSCommonHandler): + """ + This is the handler for WCS 2.0 / EO-WCS GetCapabilities requests. It + inherits from :class:`~.WCSCommonHandler`. + + As for all handlers, the entry point is the + :meth:`~.WCSCommonHandler.handle` method. The + handler then performs a workflow that is described in the + :class:`~.WCSCommonHandler` documentation. + + This handler follows this workflow with adaptations to the + :meth:`createCoverages`, :meth:`configureMapObj` and :meth:`postprocess` + methods. The latter one modifies the GetCapabilities response obtained by + MapServer to contain EO-WCS specific extensions. + """ + + SERVICE = "wcs" + + REGISTRY_CONF = { + "name": "WCS 2.0 GetCapabilities Handler", + "impl_id": "services.ows.wcs20.WCS20GetCapabilitiesHandler", + "registry_values": { + "services.interfaces.service": "wcs", + "services.interfaces.version": "2.0.0", + "services.interfaces.operation": "getcapabilities" + } + } + + PARAM_SCHEMA = { + "service": {"xml_location": "/@service", "xml_type": "string", "kvp_key": "service", "kvp_type": "string"}, + "version": {"xml_location": "/@version", "xml_type": "string", "kvp_key": "version", "kvp_type": "string"}, + "operation": {"xml_location": "/", "xml_type": "localName", "kvp_key": "request", "kvp_type": "string"}, + "updatesequence": {"xml_location": "/@updateSequence", "xml_type": "string", "kvp_key": "updatesequence", "kvp_type": "string"}, + "sections": {"xml_location": "/{http://www.opengis.net/ows/2.0}section", "xml_type": "string[]", "kvp_key": "sections", "kvp_type": "stringlist"} + } + + # TODO: override createCoverages, configureRequest, configureMapObj + def createCoverages(self): + """ + This method adds all Rectified Datasets and Rectified Stitched Mosaics + to the ``coverages`` property of the handler. For each of these + coverages, a layer will be added to the MapScript :class:`mapObj`. + """ + self.coverages = self._get_coverages( + [ + "resources.coverages.wrappers.RectifiedDatasetWrapper", + "resources.coverages.wrappers.RectifiedStitchedMosaicWrapper" + ] + ) + + def _get_coverages(self, impl_ids=None): + visible_expr = System.getRegistry().getFromFactory( + "resources.coverages.filters.CoverageExpressionFactory", + {"op_name": "attr", "operands": ("visible", "=", True)} + ) + + factory = System.getRegistry().bind("resources.coverages.wrappers.EOCoverageFactory") + + if impl_ids: + return factory.find( + impl_ids=impl_ids, + filter_exprs=[visible_expr] + ) + else: + return factory.find(filter_exprs=[visible_expr]) + + + def configureMapObj(self): + """ + This method extends the :class:`~.WCSCommonHandler.configureMapObj` + method to include informations on the available output formats as + well as the supported CRSes. + """ + super(WCS20GetCapabilitiesHandler, self).configureMapObj() + + # set all the supported formats + for output_format in getMSOutputFormatsAll() : + self.map.appendOutputFormat(output_format) + + self.map.setMetaData("wcs_formats",getMSWCSFormatMD()) + + # set supported CRSes + self.map.setMetaData( 'ows_srs' , getMSWCSSRSMD() ) + self.map.setMetaData( 'wcs_srs' , getMSWCSSRSMD() ) + + + def getMapServerLayer(self, coverage): + """ + This method returns a MapScript :class:`layerObj` for the input + ``coverage``. It extends the + :class:`~.WCSCommonHandler.getMapServerLayer` function by configuring + the input data using the appropriate connectors (see + :mod:`eoxserver.services.connectors`). + """ + layer = super(WCS20GetCapabilitiesHandler, self).getMapServerLayer(coverage) + + connector = System.getRegistry().findAndBind( + intf_id = "services.mapserver.MapServerDataConnectorInterface", + params = { + "services.mapserver.data_structure_type": \ + coverage.getDataStructureType() + } + ) + + layer = connector.configure(layer, coverage) + + return layer + + + def postprocess(self, resp): + """ + This method transforms the standard WCS 2.0 response ``resp`` obtained + from MapServer into an EO-WCS compliant GetCapabilities response and + returns the corresponding :class:`~.Response` object. + + Specifically, + + * the xsi:schemaLocation attribute of the document root is set + to the EO-WCS schema URL + * the extensions supported by EOxServer are added to the + wcs:ServiceMetadata element + * the supported EO-WCS profiles are added to the + wcs:ServiceIdentification element + * the metadata for the DescribeEOCoverageSet operation is added + to the ows:OperationsMetadata element + * the wcs:Contents section is replaced by an EO-WCS compliant + structure + + The wcs:Contents section is configured with the coverage summaries of + all visible Rectified and Referenceable Datasets, of all Rectified + Stitched Mosaics and the summaries of all Dataset Series. Note that + the handler is aware of the OWS Common sections parameter which allows + to deselect all or parts of the wcs:Contents section and acts + accordingly. + + Should MapServer return an exception report in ``resp``, it is + passed on unchanged except for the xsi:schemaLocation attribute. + """ + + dom = minidom.parseString(resp.content) + + # add the additional CRS namespace + + + # change xsi:schemaLocation + schema_location_attr = dom.documentElement.getAttributeNode("xsi:schemaLocation") + schema_location_attr.nodeValue = "http://www.opengis.net/wcseo/1.0 http://schemas.opengis.net/wcseo/1.0/wcsEOAll.xsd" + + + # we are finished if the response is an ows:ExceptionReport + # proceed otherwise + if dom.documentElement.localName != "ExceptionReport": + + encoder = WCS20EOAPEncoder() + + #TODO: Following part should be handled by the MapServer. + # Remove the code when MapServer really does it. + + # append SupportedCRSs to ServiceMetadata + svc_md = dom.getElementsByTagName("wcs:ServiceMetadata").item(0) + + if svc_md is not None : + extension = encoder.encodeExtension() + svc_md.appendChild(extension) + supported_crss = encoder.encodeSupportedCRSs() + + for sc in supported_crss : + extension.appendChild( sc ) + + # append EO Profiles to ServiceIdentification + svc_identification = dom.getElementsByTagName("ows:ServiceIdentification").item(0) + + if svc_identification is not None: + eo_profiles = encoder.encodeEOProfiles() + + profiles = svc_identification.getElementsByTagName("ows:Profile") + if len(profiles) == 0: + for eo_profile in eo_profiles: + svc_identification.appendChild(eo_profile) + else: + for eo_profile in eo_profiles: + svc_identification.insertBefore(eo_profile, profiles.item(0)) + + # append DescribeEOCoverageSet + op_metadata = dom.getElementsByTagName("ows:OperationsMetadata").item(0) + + if op_metadata is not None: + desc_eo_cov_set_op = encoder.encodeDescribeEOCoverageSetOperation( + OWSCommonConfigReader().getHTTPServiceURL() + ) + + op_metadata.appendChild(desc_eo_cov_set_op) + + count_default = WCS20ConfigReader().getPagingCountDefault() + op_metadata.appendChild(encoder.encodeCountDefaultConstraint(count_default)) + + + # rewrite wcs:Contents + # adjust wcs:CoverageSubtype and add wcseo:DatasetSeriesSummary + sections = self.req.getParamValue("sections") + + if sections is None or len(sections) == 0 or "Contents" in sections or\ + "CoverageSummary" in sections or\ + "DatasetSeriesSummary" in sections or\ + "All" in sections: + + contents_new = encoder.encodeContents() + + # adjust wcs:CoverageSubtype + if sections is None or len(sections) == 0 or "Contents" in sections or\ + "CoverageSummary" in sections or "All" in sections: + + all_coverages = self._get_coverages() + + for coverage in all_coverages: + cov_summary = encoder.encodeCoverageSummary(coverage) + contents_new.appendChild(cov_summary) + + # append dataset series summaries + if sections is None or len(sections) == 0 or "Contents" in sections or\ + "DatasetSeriesSummary" in sections or "All" in sections: + + extension = encoder.encodeExtension() + contents_new.appendChild(extension) + + dss_factory = System.getRegistry().bind( + "resources.coverages.wrappers.DatasetSeriesFactory" + ) + + for dataset_series in dss_factory.find(): + dss_summary = encoder.encodeDatasetSeriesSummary(dataset_series) + extension.appendChild(dss_summary) + + contents_old = dom.getElementsByTagName("wcs:Contents").item(0) + + if contents_old is None: + dom.documentElement.appendChild(contents_new) + else: + contents_old.parentNode.replaceChild(contents_new, contents_old) + + # rewrite XML and replace it in the response + resp.content = DOMtoXML(dom) + + dom.unlink() + + # TODO: integrate WCS Transaction Operation in getcapabilities response + #return wcst11AlterCapabilities20(resp) + return resp + + +class WCS20CorrigendumGetCapabilitiesHandler(WCS20GetCapabilitiesHandler): + + REGISTRY_CONF = { + "name": "WCS 2.0 GetCapabilities Handler", + "impl_id": "services.ows.wcs20.WCS20CorrigendumGetCapabilitiesHandler", + "registry_values": { + "services.interfaces.service": "wcs", + "services.interfaces.version": "2.0.1", + "services.interfaces.operation": "getcapabilities" + } + } diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/wcs20/getcov.py eoxserver-0.3.2/eoxserver/services/ows/wcs/wcs20/getcov.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/wcs20/getcov.py 1970-01-01 00:00:00.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wcs/wcs20/getcov.py 2013-12-10 14:57:00.000000000 +0000 @@ -0,0 +1,741 @@ +#------------------------------------------------------------------------------- +# $Id$ +# +# Project: EOxServer +# Authors: Stephan Krause +# Stephan Meissl +# Martin Paces +# +#------------------------------------------------------------------------------- +# Copyright (C) 2011 EOX IT Services GmbH +# +# 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 of this Software or works derived from this 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. +#------------------------------------------------------------------------------- + + +""" +This module contains handlers for WCS 2.0 / EO-WCS GetCoverage requests. +""" + +import logging +from xml.dom import minidom +from datetime import datetime + +import mapscript +from django.contrib.gis.geos import GEOSGeometry + +from eoxserver.core.system import System +from eoxserver.core.exceptions import InternalError, InvalidExpressionError +from eoxserver.core.util.xmltools import DOMElementToXML +from eoxserver.core.util.multiparttools import mpPack +from eoxserver.core.util.bbox import BBox +from eoxserver.core.util.filetools import TmpFile +from eoxserver.contrib import gdal +from eoxserver.processing.gdal import reftools as rt +from eoxserver.services.base import BaseRequestHandler +from eoxserver.services.requests import Response +from eoxserver.services.mapserver import ( + gdalconst_to_imagemode, gdalconst_to_imagemode_string +) +from eoxserver.services.exceptions import ( + InvalidRequestException, InvalidSubsettingException, + InvalidAxisLabelException +) +from eoxserver.services.ows.wcs.common import ( + WCSCommonHandler, getMSOutputFormat, + getWCSNativeFormat, getMSWCSFormatMD, + getMSWCSNativeFormat, getMSWCSSRSMD, + parse_format_param +) +from eoxserver.services.ows.wcs.encoders import WCS20EOAPEncoder +from eoxserver.services.ows.wcs.wcs20.subset import WCS20SubsetDecoder +from eoxserver.services.ows.wcs.wcs20.mask import WCS20MaskDecoder +from eoxserver.resources.coverages.formats import getFormatRegistry +from eoxserver.resources.coverages import crss + + +logger = logging.getLogger(__name__) + +# stripping dot from file extension +_stripDot = lambda ext : ext[1:] if ext.startswith('.') else ext + +MASK_LAYER_NAME = "masklayername"#"__mask_layer__" + +# register all GDAL drivers + + +class WCS20GetCoverageHandler(WCSCommonHandler): + """ + This handler takes care of all WCS 2.0 / EO-WCS GetCoverage requests. It + inherits from :class:`~.WCSCommonHandler`. + + The main processing step is to determine the coverage concerned by the + request and delegate the request handling to the handlers for Referenceable + Datasets or other (rectified) coverages according to the coverage type. + + An :exc:`~.InvalidRequestException` is raised if the coverage ID parameter + is missing in the request or the coverage ID is unknown. + """ + REGISTRY_CONF = { + "name": "WCS 2.0 GetCoverage Handler", + "impl_id": "services.ows.wcs20.WCS20GetCoverageHandler", + "registry_values": { + "services.interfaces.service": "wcs", + "services.interfaces.version": "2.0.0", + "services.interfaces.operation": "getcoverage" + } + } + + PARAM_SCHEMA = { + "service": {"xml_location": "/service", "xml_type": "string", "kvp_key": "service", "kvp_type": "string"}, + "version": {"xml_location": "/version", "xml_type": "string", "kvp_key": "version", "kvp_type": "string"}, + "operation": {"xml_location": "/", "xml_type": "localName", "kvp_key": "request", "kvp_type": "string"}, + "coverageid": {"xml_location": "/{http://www.opengis.net/wcs/2.0}CoverageId", "xml_type": "string", "kvp_key": "coverageid", "kvp_type": "string"}, + } + + def _processRequest(self, req): + req.setSchema(self.PARAM_SCHEMA) + + coverage = self._get_coverage(req) + + if coverage.getType() == "plain": + handler = WCS20GetRectifiedCoverageHandler() # TODO: write plain coverage handler + return handler.handle(req) + elif coverage.getType() in ("eo.rect_stitched_mosaic", "eo.rect_dataset"): + handler = WCS20GetRectifiedCoverageHandler() + return handler.handle(req) + elif coverage.getType() == "eo.ref_dataset": + handler = WCS20GetReferenceableCoverageHandler() + return handler.handle(req, coverage) + + def _get_coverage(self, req): + coverage_id = req.getParamValue("coverageid") + + if coverage_id is None: + raise InvalidRequestException("Missing 'coverageid' parameter", "MissingParameterValue", "coverageid") + else: + coverage = System.getRegistry().getFromFactory( + "resources.coverages.wrappers.EOCoverageFactory", + {"obj_id": coverage_id} + ) + + if coverage is not None: + return coverage + else: + raise InvalidRequestException( + "No coverage with id '%s' found" % coverage_id, "NoSuchCoverage", coverage_id + ) + +class WCS20CorrigendumGetCoverageHandler(WCS20GetCoverageHandler): + """ + This handler takes care of all WCS 2.0.1 / EO-WCS GetCoverage requests. It + inherits from :class:`~.WCS20GetCoverageHandler`. + """ + + REGISTRY_CONF = { + "name": "WCS 2.0 GetCoverage Handler", + "impl_id": "services.ows.wcs20.WCS20CorrigendumGetCoverageHandler", + "registry_values": { + "services.interfaces.service": "wcs", + "services.interfaces.version": "2.0.1", + "services.interfaces.operation": "getcoverage" + } + } + +class WCS20GetReferenceableCoverageHandler(BaseRequestHandler): + """ + This class handles WCS 2.0 / EO-WCS GetCoverage requests for Referenceable + datasets. It inherits from :class:`~.BaseRequestHandler`. It is instantiated + by :class:`WCS20GetCoverageHandler`. + """ + PARAM_SCHEMA = { + "service": {"xml_location": "/service", "xml_type": "string", "kvp_key": "service", "kvp_type": "string"}, + "version": {"xml_location": "/version", "xml_type": "string", "kvp_key": "version", "kvp_type": "string"}, + "operation": {"xml_location": "/", "xml_type": "localName", "kvp_key": "request", "kvp_type": "string"}, + "coverageid": {"xml_location": "/{http://www.opengis.net/wcs/2.0}CoverageId", "xml_type": "string", "kvp_key": "coverageid", "kvp_type": "string"}, + "trims": {"xml_location": "/{http://www.opengis.net/wcs/2.0}DimensionTrim", "xml_type": "element[]"}, + "slices": {"xml_location": "/{http://www.opengis.net/wcs/2.0}DimensionSlice", "xml_type": "element[]"}, + "format": {"xml_location": "/{http://www.opengis.net/wcs/2.0}format", "xml_type": "string", "kvp_key": "format", "kvp_type": "string"}, + "mediatype": {"xml_location": "/{http://www.opengis.net/wcs/2.0}mediaType", "xml_type": "string", "kvp_key": "mediatype", "kvp_type": "string"} + } + + def handle(self, req, coverage): + """ + This method handles the GetCoverage request for Referenceable Datasets. + It takes two parameters: the :class:`~.OWSRequest` object ``req`` and + the :class:`~.ReferenceableDatasetWrapper` object ``coverage``. + + The method makes ample use of the functions in + :mod:`eoxserver.processing.gdal.reftools` in order to transform + the pixel coordinates to the underlying CRS. + + It starts by decoding the (optional) subset parameters using the + methods of :class:`~.WCS20SubsetDecoder`. There are two possible + meanings of the subset coordinates: absent a CRS definition, they are + assumed to be expressed in pixel coordinates (imageCRS); otherwise they + are treated as coordinates in the respective CRS. + + In the latter case, the subset is transformed to pixel coordinates + using :func:`eoxserver.processing.gdal.reftools.rect_from_subset`. + This results in a pixel subset that contains the whole area of the + subset taking into account the GCP information. See the function + docs for details. + + The next step is to determine the format of the response data. This + is done based on the format parameter and the format configurations + (see also :mod:`eoxserver.resources.coverages.formats`). The format + MIME type has to be known to the server and it has to be supported by + GDAL, otherwise an :exc:`~.InternalError` is raised. + + For technical reasons, though, the initial dataset is not created with + the output format driver, but as a virtual dataset in the memory. Only + later the dataset is copied using the :meth:`CreateCopy` method of + the GDAL driver. + + The method tags several metadata items on the output, most importantly + the GCPs. Note that all GCPs of the coverage are tagged on the + output dataset even if only a subset has been requested. This because + all of them may have influence on the computation of the coordinate + transformation in the subset even if they lie outside. + + Finally, the response is composed. According to the mediatype parameter, + either a multipart message containing the coverage description of the + output coverage and the output coverage data or just the data is + returned. + """ + # set request schema + req.setSchema(self.PARAM_SCHEMA) + + # get reftool transformer's parameters + rt_prm = rt.suggest_transformer(coverage.getData().getGDALDatasetIdentifier()) + + #============================================= + # coverage subsetting + + # get image bounds as a bounding box + bb_img = BBox( *coverage.getSize() ) + + #decode subset + + decoder = WCS20SubsetDecoder(req, "imageCRS") + + try: + subset = decoder.getSubset( bb_img.sx, bb_img.sy, coverage.getFootprint()) + except InvalidSubsettingException, e: + raise InvalidRequestException( str(e), "InvalidSubsetting", "subset") + except InvalidAxisLabelException, e: + raise InvalidRequestException( str(e), "InvalidAxisLabel", "subset" ) + + # convert subset to bounding box in image coordinates (bbox) + + if subset is None: # whole coverage + + bbox = bb_img + + elif subset.crs_id == "imageCRS" : # pixel subset + + bbox = BBox( None, None, subset.minx, subset.miny, + subset.maxx+1, subset.maxy+1 ) + + else : # otherwise let GDAL handle the projection + + bbox = rt.rect_from_subset( + coverage.getData().getGDALDatasetIdentifier(), subset.crs_id, + subset.minx, subset.miny, subset.maxx, subset.maxy, **rt_prm ) + + # calculate effective offsets and size of the overlapped area + + bb_src = bbox & bb_img # trim bounding box to match the coverage + bb_dst = bb_src - bbox.off # adjust the output offset + + # check the extent of the effective area + + if 0 == bb_src.ext : + raise InvalidRequestException( "Subset outside coverage extent.", + "InvalidParameterValue", "subset" ) + + #====================================================================== + + # get the range type + rtype = coverage.getRangeType() + + # get format + format_param = req.getParamValue("format") + + # handling format + if format_param is None: + + # map the source format to the native one + format = getWCSNativeFormat( coverage.getData().getSourceFormat() ) + + format_options = [] + + else : + + # unpack format specification + mime_type, format_options = parse_format_param(format_param) + + format = getFormatRegistry().getFormatByMIME( mime_type ) + + if format is None : + raise InvalidRequestException( + "Unknown or unsupported format '%s'" % mime_type, + "InvalidParameterValue", "format" ) + + #====================================================================== + # creating the output image + + # check anf get the output GDAL driver + backend_name , _ , driver_name = format.driver.partition("/") ; + + if backend_name != "GDAL" : + raise InternalError( "Unsupported format backend \"%s\"!" % backend_name ) + + drv_dst = gdal.GetDriverByName( driver_name ) + + if drv_dst is None: + raise InternalError( "Invalid GDAL Driver identifier '%s'" % driver_name ) + + # get the GDAL virtual driver + drv_vrt = gdal.GetDriverByName( "VRT" ) + + #input DS - NOTE: GDAL is not capable to handle unicode filenames! + src_path = str( coverage.getData().getGDALDatasetIdentifier() ) + ds_src = gdal.OpenShared( src_path ) + + # create a new GDAL in-memory virtual DS + ds_vrt = drv_vrt.Create( "", bbox.sx, bbox.sy, len(rtype.bands), + rtype.data_type ) + + # set mapping from the source DS + + # simple source XML template + tmp = [] + tmp.append( "" ) + tmp.append( "%s" % src_path ) + tmp.append( "%d" ) + tmp.append( "" % bb_src.as_tuple() ) + tmp.append( "" % bb_dst.as_tuple() ) + tmp.append( "" ) + tmp = "".join(tmp) + + # raster data mapping + for i in xrange(1,len(rtype.bands)+1) : + ds_vrt.GetRasterBand(i).SetMetadataItem( "source_0", tmp%i, + "new_vrt_sources" ) + + # copy metadata + for key, value in ds_src.GetMetadata().items() : + ds_vrt.SetMetadataItem(key, value) + + # copy tie-points + + # tiepoint offset - higher order function + def _tpOff( ( ox , oy ) ) : + def function( p ) : + return gdal.GCP( p.GCPX, p.GCPY, p.GCPZ, p.GCPPixel - ox, + p.GCPLine - oy, p.Info, p.Id ) + return function + + # instantiate tiepoint offset function for current offset value + tpOff = _tpOff( bbox.off ) + + # copy tiepoints + ds_vrt.SetGCPs( [ tpOff(p) for p in ds_src.GetGCPs() ], + ds_src.GetGCPProjection() ) + + #====================================================================== + # create final DS + + # get the requested media type + media_type = req.getParamValue("mediatype") + + # NOTE: MP: Direct use of MIME params as GDAL param is quite smelly, + # thus I made decision to keep it away. (",".join(format_options)) + + with TmpFile( format.defaultExt , "tmp_" ) as dst_path : + + #NOTE: the format option (last param.) must be None or a sequence + drv_dst.CreateCopy( dst_path , ds_vrt , True , () ) + + # get footprint if needed + + if ( media_type is not None ) and ( subset is not None ) : + footprint = GEOSGeometry(rt.get_footprint_wkt(dst_path,**rt_prm)) + else : + footprint = None + + # load data + f = open(dst_path) ; data = f.read() ; f.close() + + #====================================================================== + # prepare response + + # set the response filename + time_stamp = datetime.now().strftime("%Y%m%d%H%M%S") + filename = "%s_%s%s" % ( coverage.getCoverageId(), time_stamp, format.defaultExt ) + + if media_type is None: + + resp = self._get_default_response( data, format.mimeType, filename) + + elif media_type in ( "multipart/related" , "multipart/mixed" ) : + + encoder = WCS20EOAPEncoder() + + reference = "coverage/%s" % filename + mpsubtype = media_type.partition("/")[2] + + if subset is None : + _subset = None + else : + + if subset.crs_id == "imageCRS": + _subset = ( 4326, bbox.size, footprint.extent, footprint ) + else: + _subset = ( subset.crs_id, bbox.size, + (subset.minx, subset.miny, subset.maxx, subset.maxy), footprint ) + + cov_desc_el = encoder.encodeReferenceableDataset(coverage, + "cid:%s"%reference,mime_type,True,_subset) + + # NOTE: the multipart subtype will be the same as the one requested + resp = self._get_multipart_response( data, format.mimeType, filename, reference, + DOMElementToXML(cov_desc_el), boundary = "wcs" , subtype = mpsubtype ) + + else: + raise InvalidRequestException( + "Unknown media type '%s'" % media_type, + "InvalidParameterValue", + "mediatype" + ) + + return resp + + def _get_default_response(self, data, mime_type, filename): + + # create response + resp = Response( + content_type = mime_type, + content = data, + headers = {'Content-Disposition': "inline; filename=\"%s\"" % filename}, + status = 200 + ) + + return resp + + def _get_multipart_response(self, data, mime_type, filename, + reference, cov_desc, boundary = "wcs", subtype = "related" ): + + # prepare multipart package + parts = [ # u + ( [( "Content-Type" , "text/xml" )] , cov_desc ) , + ( [( "Content-Type" , str(mime_type) ) , + ( "Content-Description" , "coverage data" ), + ( "Content-Transfer-Encoding" , "binary" ), + ( "Content-Id" , str(reference) ), + ( "Content-Disposition" , "inline; filename=\"%s\"" % str(filename) ) , + ] , data ) ] + + # create response + resp = Response( + content = mpPack( parts , boundary ) , + content_type = "multipart/%s; boundary=%s"%(subtype,boundary), + headers = {}, + status = 200 + ) + + return resp + + +class WCS20GetRectifiedCoverageHandler(WCSCommonHandler): + """ + This is the handler for GetCoverage requests for Rectified Datasets + and Rectified Stitched Mosaics. It inherits from + :class:`~.WCSCommonHandler`. + + It follows the workflow of the base class and modifies the + :meth:`createCoverages`, :meth:`configureMapObj`, :meth:`getMapServerLayer` + and :meth:`postprocess` methods. + """ + PARAM_SCHEMA = { + "service": {"xml_location": "/service", "xml_type": "string", "kvp_key": "service", "kvp_type": "string"}, + "version": {"xml_location": "/version", "xml_type": "string", "kvp_key": "version", "kvp_type": "string"}, + "operation": {"xml_location": "/", "xml_type": "localName", "kvp_key": "request", "kvp_type": "string"}, + "coverageid": {"xml_location": "/{http://www.opengis.net/wcs/2.0}CoverageId", "xml_type": "string", "kvp_key": "coverageid", "kvp_type": "string"}, + "trims": {"xml_location": "/{http://www.opengis.net/wcs/2.0}DimensionTrim", "xml_type": "element[]"}, + "slices": {"xml_location": "/{http://www.opengis.net/wcs/2.0}DimensionSlice", "xml_type": "element[]"}, + "format": {"xml_location": "/{http://www.opengis.net/wcs/2.0}format", "xml_type": "string", "kvp_key": "format", "kvp_type": "string"}, + "mediatype": {"xml_location": "/{http://www.opengis.net/wcs/2.0}mediaType", "xml_type": "string", "kvp_key": "mediatype", "kvp_type": "string"}, + "polygonmasks": {"xml_location": "/{http://www.opengis.net/wcs/2.0}Extension/{http://www.opengis.net/wcs/mask/1.0}polygonMask", "xml_type": "string[]", "kvp_key": "polygon", "kvp_type": "string"}, + } + + def addLayers(self): + masks = WCS20MaskDecoder(self.req) + + self.has_mask = masks.has_mask + + if self.has_mask: + # create a mask layer + mask_layer = mapscript.layerObj() + mask_layer.name = MASK_LAYER_NAME + mask_layer.status = mapscript.MS_DEFAULT + mask_layer.type = mapscript.MS_LAYER_POLYGON + mask_layer.setProjection("init=epsg:4326") # TODO: make this dependant on the actually given crs + + # add features for each mask polygon + for polygon in masks.polygons: + shape = mapscript.shapeObj(mapscript.MS_SHAPE_POLYGON) + line = mapscript.lineObj() + for x, y in polygon: + line.add(mapscript.pointObj(x, y)) + shape.add(line) + mask_layer.addFeature(shape) + + cls = mapscript.classObj(mask_layer) + style = mapscript.styleObj(cls) + style.color.setRGB(0, 0, 0) # requires a color + + self.map.insertLayer(mask_layer) + + self.has_mask = True + + super(WCS20GetRectifiedCoverageHandler, self).addLayers() + + + def createCoverages(self): + """ + This method retrieves the coverage object denoted by the request and + stores it in the ``coverages`` property of the handler. The method + also checks if the subset expressions (if present) match with the + coverage extent. + + An :exc:`~.InvalidRequestException` is raised if the coverageid + parameter is missing or the coverage ID is unknown or the subset + expressions do not match with the coverage extent. + """ + coverage_id = self.req.getParamValue("coverageid") + + if coverage_id is None: + raise InvalidRequestException("Missing 'coverageid' parameter", "MissingParameterValue", "coverageid") + else: + coverage = System.getRegistry().getFromFactory( + "resources.coverages.wrappers.EOCoverageFactory", + {"obj_id": coverage_id} + ) + + if coverage is not None: + decoder = WCS20SubsetDecoder(self.req, "imageCRS") + try: + filter_exprs = decoder.getFilterExpressions() + except InvalidSubsettingException, e: + raise InvalidRequestException(str(e), "InvalidParameterValue", "subset") + + try: + if coverage.matches(filter_exprs): + self.coverages.append(coverage) + else: + # TODO: check for right exception report + raise InvalidRequestException( + "Coverage does not match subset expressions.", + "NoSuchCoverage", + coverage_id + ) + except InvalidExpressionError, e: + raise InvalidRequestException( + "Error when evaluating subset expression: %s" % str(e), + "InvalidParameterValue", + "subset" + ) + else: + raise InvalidRequestException( + "No coverage with id '%s' found" % coverage_id, "NoSuchCoverage", coverage_id + ) + + def _setParameter(self, key, value): + if key.lower() == "format": + super(WCS20GetRectifiedCoverageHandler, self)._setParameter("format", "custom") + else: + super(WCS20GetRectifiedCoverageHandler, self)._setParameter(key, value) + + + def configureMapObj(self): + """ + This method extends the base method + (:meth:`~.WCSCommonHandler.configureMapObj`). The format configurations + are added to the MapScript :class:`mapObj`. + """ + super(WCS20GetRectifiedCoverageHandler, self).configureMapObj() + + # get format + format_param = self.req.getParamValue("format") + + if format_param is None: + # no format specification provided -> use the native one + format_param = getMSWCSNativeFormat( self.coverages[0].getData().getSourceFormat() ) + + # prepare output format (the subroutine checks format and throws proper exception + # in case of an incorrect format parameter ) + output_format = getMSOutputFormat( format_param, self.coverages[0] ) + + # set only the currently requested output format + self.map.appendOutputFormat(output_format) + self.map.setOutputFormat(output_format) + + + def getMapServerLayer(self, coverage): + """ + This method returns a MapServer :class:`layerObj` for the corresponding + coverage. It extends the base class method + :class:`~.WCSCommonHandler.getMapServerLayer`. The method configures + the input data for the layer using the appropriate connector for the + coverage (see :mod:`eoxserver.services.connectors`). Furthermore, + it sets WCS 2.0 specific metadata on the layer. + """ + + layer = super(WCS20GetRectifiedCoverageHandler, self).getMapServerLayer(coverage) + + connector = System.getRegistry().findAndBind( + intf_id = "services.mapserver.MapServerDataConnectorInterface", + params = { + "services.mapserver.data_structure_type": \ + coverage.getDataStructureType() + } + ) + layer = connector.configure(layer, coverage) + + # TODO: Change the following comment to something making sense or remove it! + # this was under the "eo.rect_mosaic"-path. minor accurracy issues + # have evolved since making it accissible to all paths + rangetype = coverage.getRangeType() + + layer.setMetaData("wcs_bandcount", "%d" % len(rangetype.bands)) + layer.setMetaData("wcs_band_names", " ".join([band.name for band in rangetype.bands]) ) + layer.setMetaData("wcs_interval", "%f %f" % rangetype.getAllowedValues()) + layer.setMetaData("wcs_significant_figures", "%d" % rangetype.getSignificantFigures()) + + # set layer depending metadata + for band in rangetype.bands: + axis_metadata = { + "%s_band_description" % band.name: band.description, + "%s_band_definition" % band.name: band.definition, + "%s_band_uom" % band.name: band.uom + } + for key, value in axis_metadata.items(): + if value != '': + layer.setMetaData(key, value) + + # set the layer's native format + layer.setMetaData("wcs_native_format", getMSWCSNativeFormat(coverage.getData().getSourceFormat()) ) + + # set per-layer supported formats (using the per-service global data) + layer.setMetaData("wcs_formats", getMSWCSFormatMD() ) + + layer.setMetaData( "wcs_imagemode", gdalconst_to_imagemode_string(rangetype.data_type) ) + + if self.has_mask: + layer.mask = MASK_LAYER_NAME + + return layer + + def postprocess(self, resp): + """ + This method overrides the no-op method of the base class. It adds + EO-WCS specific metadata to the multipart messages that include an + XML coverage description part. It expects a :class:`~.MapServerResponse` + object ``resp`` as input and returns it either unchanged or a + new :class:`~.Response` object containing the modified multipart + message. + + MapServer returns a WCS 2.0 coverage description, but this does not + include EO-WCS specific parts like the coverage subtype (Rectified + Dataset or Rectified Stitched Mosaic) and EO-WCS metadata. Therefore + the description is replaced with the corresponding EO-WCS complient + XML. + """ + coverage_id = self.req.getParamValue("coverageid") + + if self.coverages[0].getType() == "eo.rect_stitched_mosaic": + include_composed_of = False #True + + else: + include_composed_of = False + poly = None + + resp.splitResponse() + + if resp.ms_response_xml: + + dom = minidom.parseString(resp.ms_response_xml) + + rectified_grid_coverage = dom.getElementsByTagName("gmlcov:RectifiedGridCoverage").item(0) + + if rectified_grid_coverage is not None: + + encoder = WCS20EOAPEncoder() + + coverage = self.coverages[0] + + decoder = WCS20SubsetDecoder(self.req, "imageCRS") + + poly = decoder.getBoundingPolygon( + coverage.getFootprint(), + coverage.getSRID(), + coverage.getSize()[0], + coverage.getSize()[1], + coverage.getExtent() + ) + + if coverage.getType() == "eo.rect_dataset": + resp_xml = encoder.encodeRectifiedDataset( + coverage, + req=self.req, + nodes=rectified_grid_coverage.childNodes, + poly=poly + ) + elif coverage.getType() == "eo.rect_stitched_mosaic": + resp_xml = encoder.encodeRectifiedStitchedMosaic( + coverage, + req=self.req, + nodes=rectified_grid_coverage.childNodes, + poly=poly + ) + + dom.unlink() + + #TODO: MP: Set the subtype to 'related' for 'multipart/related' responses! + resp = resp.getProcessedResponse( DOMElementToXML(resp_xml) , subtype = "mixed" ) + + # else : pass - using the unchanged original response TODO: Is this correct? MP + + else: # coverage only + + coverage = self.coverages[0] + mime_type = resp.getContentType() + + if not mime_type.lower().startswith("multipart/"): + filename = "%s_%s%s" % ( + coverage.getCoverageId(), + datetime.now().strftime("%Y%m%d%H%M%S"), + getFormatRegistry().getFormatByMIME( mime_type ).defaultExt + ) + + resp.headers.update({'Content-Disposition': "inline; filename=\"%s\"" % filename}) + + return resp diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/wcs20/__init__.py eoxserver-0.3.2/eoxserver/services/ows/wcs/wcs20/__init__.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/wcs20/__init__.py 1970-01-01 00:00:00.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wcs/wcs20/__init__.py 2013-12-10 14:57:00.000000000 +0000 @@ -0,0 +1 @@ + diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/wcs20/mask.py eoxserver-0.3.2/eoxserver/services/ows/wcs/wcs20/mask.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/wcs20/mask.py 1970-01-01 00:00:00.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wcs/wcs20/mask.py 2013-12-10 14:57:00.000000000 +0000 @@ -0,0 +1,136 @@ +#------------------------------------------------------------------------------- +# $Id$ +# +# Project: EOxServer +# Authors: Fabian Schindler +# +#------------------------------------------------------------------------------- +# Copyright (C) 2011 EOX IT Services GmbH +# +# 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 of this Software or works derived from this 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. +#------------------------------------------------------------------------------- + +import re +from itertools import izip +import logging + +from eoxserver.resources.coverages import crss #import hasSwappedAxes, fromURL + + +logger = logging.getLogger(__name__) + +def pairwise(iterable): + "s -> (s0,s1), (s2,s3), (s4, s5), ..." + a = iter(iterable) + return izip(a, a) + + +class PolygonMask(object): + def __init__(self, coords, crs=None): + # check if the axes should be swapped + + self.srid = crss.fromURL(crs) if crs else None + self.crs = crs + + if self.srid is not None and crss.hasSwappedAxes(self.srid): + self.coords = [(x, y) for (y, x) in coords] + else: + self.coords = list(coords) + + # make sure the polygon is closed + if self.coords[:2] != self.coords[-2:]: + self.coords.extend(self.coords[:2]) + + + def __iter__(self): + return iter(self.coords) + + + def __len__(self): + return len(self.coords) + + + def to_wkt(self): + pass # TODO: implement + + + +class WCS20MaskDecoder(object): + """ Parser class for WCS 2.0 mask request parameters. Not yet OGC standardized. """ + + def __init__(self, req): + self.polygons = [] + self.coverages = [] + self._decode(req) + + + @property + def has_mask(self): + return (len(self.polygons) + len(self.coverages)) > 0 + + + def _decode(self, req): + if req.getParamType() == "kvp": + self._decodeKVP(req) + else: + self._decodeXML(req) + + + def _decodeKVP(self, req): + for key, values in req.getParams().items(): + if not key.startswith("mask"): + continue + + for value in values: + match = re.match( + r'(\w+)(,([^(]+))?()\(([^(^)]*)\)', value + ) + + if not match: + raise Exception("Invalid `mask` expression.") + + method = match.group(1) + crs = match.group(3) + mask_value = match.group(5) + + if method.lower() == "polygon": + raw_coords = map(float, mask_value.split(",")) + if len(raw_coords) % 2 != 0: + raise Exception("Invalid number of coordinates given.") + + pairs = pairwise(raw_coords) + self.polygons.append(PolygonMask(pairs, crs)) + + + elif method.lower() in ("coverage", "coverages", "coverageid", "coverageids"): + self.coverages.extend(mask_value.split(",")) + + def _decodeXML(self, req): + # TODO: implement + polygons = req.getParamValue("polygonmasks") + for polygon in polygons: + raw_coords = map(float, polygon.split(" ")) + if len(raw_coords) % 2 != 0: + raise Exception("Invalid number of coordinates given.") + + pairs = pairwise(raw_coords) + self.polygons.append(PolygonMask(pairs)) + # TODO: crs? + + \ No newline at end of file diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/wcs20/subset.py eoxserver-0.3.2/eoxserver/services/ows/wcs/wcs20/subset.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wcs/wcs20/subset.py 1970-01-01 00:00:00.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wcs/wcs20/subset.py 2013-12-10 14:57:00.000000000 +0000 @@ -0,0 +1,632 @@ +#------------------------------------------------------------------------------- +# $Id$ +# +# Project: EOxServer +# Authors: Stephan Krause +# Stephan Meissl +# +#------------------------------------------------------------------------------- +# Copyright (C) 2011 EOX IT Services GmbH +# +# 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 of this Software or works derived from this 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. +#------------------------------------------------------------------------------- + +""" +This module provides utilities for handling WCS 2.0 subset expressions. +""" + +import re +from django.contrib.gis.geos import Polygon + +from eoxserver.core.system import System +from eoxserver.core.exceptions import InvalidExpressionError +from eoxserver.core.util.timetools import getDateTime +from eoxserver.resources.coverages import crss +from eoxserver.resources.coverages.filters import ( + BoundedArea, Slice, TimeInterval +) +from eoxserver.services.exceptions import ( + InvalidRequestException, InvalidSubsettingException, + InvalidAxisLabelException +) + +class WCS20SubsetDecoder(object): + """ + This class provides methods for decoding a subset expression according + to WCS 2.0.0. Note that in WCS 2.0.1 the syntax and semantics of these + subset expressions have changed quite considerably, especially for the + GET KVP encoding. These are not yet supported. + + The constructor takes two arguments. The first one, ``req`` is required + and shall contain a :class:`~.OWSRequest` instance. The ``default_crs_id`` + parameter is optional and may contain a CRS ID that will be used as + fallback if no CRS is specified in the subset expression itself. The + CRS ID can be either the string ``"imageCRS"`` or an integer EPSG ID of + a CRS. + """ + def __init__(self, req, default_crs_id=None): + self.req = req + self.default_crs_id = default_crs_id + + self.slices = None + self.trims = None + + self.containment = "overlaps" + + def _decodeKVPExpression(self, key, value): + match = re.match( + r'(\w+)(,([^(]+))?\(([^,]*)(,([^)]*))?\)', value + ) + if match is None: + raise InvalidRequestException( + "Invalid subsetting operation '%s=%s'" % (key, value), + "InvalidSubsetting", + key + ) + else: + axis_label = match.group(1) + crs = match.group(3) + + if match.group(6) is not None: + subset = (axis_label, crs, match.group(4), match.group(6)) + subset_type = "trim" + else: + subset = (axis_label, crs, match.group(4)) + subset_type = "slice" + + return (subset, subset_type) + + def _decodeKVP(self): + trims = [] + slices = [] + + for key, values in self.req.getParams().items(): + if key.startswith("subset"): + for value in values: + subset, subset_type = \ + self._decodeKVPExpression(key, value) + + if subset_type == "trim": + trims.append(subset) + else: + slices.append(subset) + + self.trims = trims + self.slices = slices + + def _decodeXML(self): + trims = [] + slices = [] + + slice_elements = self.req.getParamValue("slices") + trim_elements = self.req.getParamValue("trims") + + for slice_element in slice_elements: + dimension = slice_element.getElementsByTagName("wcs:Dimension")[0] + axis_label = dimension.firstChild.data + + # TODO: until the WCS 2.0 CRS extension is finished we have to use the + # MapServer way to parse the CRS from the Trim/Slice + crs = None + if dimension.hasAttribute("crs"): + crs = dimension.getAttribute("crs") + + slice_point = slice_element.getElementsByTagName("wcs:SlicePoint")[0].firstChild.data + + slices.append((axis_label, crs, slice_point)) + + for trim_element in trim_elements: + dimension = trim_element.getElementsByTagName("wcs:Dimension")[0] + axis_label = dimension.firstChild.data + + # TODO: see above + crs = None + if dimension.hasAttribute("crs"): + crs = dimension.getAttribute("crs") + + trim_low = trim_element.getElementsByTagName("wcs:TrimLow")[0].firstChild.data + trim_high = trim_element.getElementsByTagName("wcs:TrimHigh")[0].firstChild.data + + trims.append((axis_label, crs, trim_low, trim_high)) + + self.slices = slices + self.trims = trims + + def _decode(self): + if self.req.getParamType() == "kvp": + self._decodeKVP() + else: + self._decodeXML() + + try: + self.containment = \ + self.req.getParamValue("containment", "overlaps") + except: + pass + + def _getSliceExpression(self, slice): + axis_label = slice[0] + + if axis_label in ("t", "time", "phenomenonTime"): + return self._getTimeSliceExpression(slice) + else: + return self._getSpatialSliceExpression(slice) + + def _getTimeSliceExpression(self, slice): + axis_label = slice[0] + + if slice[1] is not None and \ + slice[1] != "http://www.opengis.net/def/trs/ISO-8601/0/Gregorian+UTC": + raise InvalidSubsettingException( + "Time reference system '%s' not recognized. Please use UTC." %\ + slice[1] + ) + + if self.req.getParamType() == "kvp": + if not slice[2].startswith('"') and slice[2].endswith('"'): + raise InvalidSubsettingException( + "Date/Time tokens have to be enclosed in quotation marks (\")" + ) + else: + dt_str = slice[2].strip('"') + else: + dt_str = slice[0] # TODO: FIXME + + try: + slice_point = getDateTime(dt_str) + except: + raise InvalidSubsettingException( + "Could not convert slice point token '%s' to date/time." %\ + dt_str + ) + + return System.getRegistry().getFromFactory( + "resources.coverages.filters.CoverageExpressionFactory", + {"op_name": "time_slice", "operands": (slice_point,)} + ) + + def _getSpatialSliceExpression(self, slice): + axis_label , crs_id_str , slice_point_str = slice[:3] + + if crs_id_str is None: + crs_id = self.default_crs_id + else: + crs_id = crss.parseEPSGCode(crs_id_str,(crss.fromURL,crss.fromURN)) + if crs_id is None: + raise InvalidSubsettingException( "Failed to extrac an EPSG" + " code from the CRS URI '%s'!" % crs_id_str ) + + try: + slice_point = float(slice_point_str) + except ValueError: + raise InvalidSubsettingException( "Failed to convert slice point" + " '%s' to a real number." % slice_point_str ) + + return System.getRegistry().getFromFactory( + "resources.coverages.filters.CoverageExpressionFactory", + { + "op_name": "spatial_slice", + "operands": (Slice( + axis_label = axis_label, + crs_id = crs_id, + slice_point = slice_point + ),) + } + ) + + def _getTrimExpressions(self, trims): + time_intv, crs_id, x_bounds, y_bounds = self._decodeTrims(trims) + + if self.containment.lower() == "overlaps": + op_part = "intersects" + elif self.containment.lower() == "contains": + op_part = "within" + else: + raise InvalidRequestException( + "Unknown containment mode '%s'." % self.containment + ) + + filter_exprs = [] + + if time_intv is not None: + filter_exprs.append(System.getRegistry().getFromFactory( + "resources.coverages.filters.CoverageExpressionFactory", + { + "op_name": "time_%s" % op_part, + "operands": (time_intv,) + } + )) + + if x_bounds is None and y_bounds is None: + pass + else: + # NOTE: cannot filter w.r.t. imageCRS + if crs_id != "imageCRS": + if x_bounds is None: + x_bounds = ("unbounded", "unbounded") + + if y_bounds is None: + y_bounds = ("unbounded", "unbounded") + + try: + area = BoundedArea( + crs_id, + x_bounds[0], + y_bounds[0], + x_bounds[1], + y_bounds[1] + ) + except InvalidExpressionError, e: + raise InvalidSubsettingException(str(e)) + + filter_exprs.append(System.getRegistry().getFromFactory( + "resources.coverages.filters.CoverageExpressionFactory", + { + "op_name": "footprint_%s_area" % op_part, + "operands": (area,) + } + )) + + return filter_exprs + + def _decodeTrims(self, trims): + time_intv = None + crs_id = None + x_bounds = None + y_bounds = None + + for trim in trims: + if trim[0] in ("t", "time", "phenomenonTime"): + if time_intv is None: + begin = self._getDateTimeBound(trim[2]) + end = self._getDateTimeBound(trim[3]) + + try: + time_intv = TimeInterval(begin, end) + except Exception, e: + raise InvalidSubsettingException(str(e)) + else: + raise InvalidSubsettingException( + "Multiple definitions for time subsetting." + ) + elif trim[0] in ("x", "lon", "Lon", "long", "Long"): + + if x_bounds is None: + new_crs_id = self._getCRSID(trim[1]) + + if crs_id is None: + crs_id = new_crs_id + elif crs_id != new_crs_id: + raise InvalidSubsettingException( + "CRSs for multiple spatial trims must be the same." + ) + + x_bounds = ( + self._getCoordinateBound(crs_id, trim[2]), + self._getCoordinateBound(crs_id, trim[3]) + ) + else: + raise InvalidSubsettingException( + "Multiple definitions for first spatial axis subsetting." + ) + elif trim[0] in ("y", "lat", "Lat"): + if y_bounds is None: + new_crs_id = self._getCRSID(trim[1]) + + if crs_id is None: + crs_id = new_crs_id + elif crs_id != new_crs_id: + raise InvalidSubsettingException( + "CRSs for multiple spatial trims must be the same." + ) + + y_bounds = ( + self._getCoordinateBound(crs_id, trim[2]), + self._getCoordinateBound(crs_id, trim[3]) + ) + else: + raise InvalidSubsettingException( + "Multiple definitions for second spatial axis subsetting." + ) + else: + raise InvalidAxisLabelException( + "Invalid axis label '%s'." % trim[0] + ) + + return (time_intv, crs_id, x_bounds, y_bounds) + + + def _getDateTimeBound(self, token): + if token is None: + return "unbounded" + else: + if self.req.getParamType() == "kvp": + if not token.startswith('"') or not token.endswith('"'): + raise InvalidSubsettingException( + "Date/Time tokens have to be enclosed in quotation marks (\")" + ) + else: + dt_str = token.strip('"') + else: + dt_str = token + + try: + return getDateTime(dt_str) + except: + raise InvalidSubsettingException( + "Cannot convert token '%s' to Date/Time." % token + ) + + def _getCoordinateBound(self, crs_id, token): + if token is None: + return "unbounded" + elif crs_id != "imageCRS": + try: + return float(token) + except: + raise InvalidSubsettingException( + "Cannot convert token '%s' to number." % token + ) + else: + try: + return int(token) + except: + raise InvalidSubsettingException( + "Cannot convert token '%s' to integer." % token + ) + + def _getCRSID(self, crs_id_str): + if crs_id_str is None: + crs_id = self.default_crs_id + else: + crs_id = crss.parseEPSGCode(crs_id_str,(crss.fromURL,crss.fromURN)) + if crs_id is None: + raise InvalidSubsettingException( "Failed to extract an EPSG" + " code from the CRS URI '%s'!" % crs_id_str ) + return crs_id + + def _get_image_extent(self, x_bounds, y_bounds, x_size, y_size): + if x_bounds[0] == "unbounded": + minx = 0 + else: + minx = x_bounds[0] + + if x_bounds[1] == "unbounded": + maxx = x_size + else: + maxx = x_bounds[1] + + if y_bounds[0] == "unbounded": + miny = 0 + else: + miny = y_bounds[0] + + if y_bounds[1] == "unbounded": + maxy = y_size + else: + maxy = y_bounds[1] + + return (minx, miny, maxx, maxy) + + def _get_geo_extent(self, crs_id, x_bounds, y_bounds, footprint): + fp_minx, fp_miny, fp_maxx, fp_maxy = \ + footprint.transform(crs_id, True).extent + + if x_bounds[0] == "unbounded": + minx = fp_minx + else: + minx = x_bounds[0] + + if x_bounds[1] == "unbounded": + maxx = fp_maxx + else: + maxx = x_bounds[1] + + if y_bounds[0] == "unbounded": + miny = fp_miny + else: + miny = y_bounds[0] + + if y_bounds[1] == "unbounded": + maxy = fp_maxy + else: + maxy = y_bounds[1] + + return (minx, miny, maxx, maxy) + + def getFilterExpressions(self): + """ + This method returns a list of filter expressions based on the subset + expression. These filter expressions can be used to select coverages + from the database. + + Depending on the types of the subset expressions + :class:`~.SpatialSliceExpression`, + :class:`~.FootprintIntersectsAreaExpression`, + :class:`~.FootprintWithinAreaExpression`, + :class:`~.TimeSliceExpression`, + :class:`~.IntersectingTimeIntervalExpression` or + :class:`~.ContainingTimeIntervalExpression` instances will be returned. + + The return values depend on the ``subset`` as well as the + ``containment`` parameters contained in the request. + + This method raises :exc:`~.InvalidRequestException` if the subset + expression syntax is found to be invalid. It raises + :exc:`~.InvalidSubsettingException` if the subset expression is + syntactically correct, but cannot be interpretated, e.g. because of + wrong date/time, number or spatial reference formats. + """ + self._decode() + + filter_exprs = [] + + for slice in self.slices: + filter_exprs.append(self._getSliceExpression(slice)) + + filter_exprs.extend(self._getTrimExpressions(self.trims)) + return filter_exprs + + def getBoundingPolygon(self, footprint, srid, size_x, size_y, extent): + """ + This method returns a GeoDjango :class:`Polygon` object computed from + the subset expression as well as the submitted parameters. The + ``footprint`` contains the footprint of the underlying coverage, + the ``srid`` is the integer EPSG ID of the coverage CRS, ``size_x`` and + ``size_y`` denote the size of the coverage in pixels and ``extent`` is + the well-known 4-tuple ``(minx, miny, maxx, maxy)``. + + In case the CRS ID of the subsets relates to the image CRS, the + bounding polygon is the intersection of the subset trims and the + coverage extent expressed in the coverage CRS denoted by ``srid``. + The pixel coordinates in the subset statements are computed using the + ``extent``, ``size_x`` and ``size_y`` parameters. + + In case the CRS ID of the subsets is an EPSG ID denoting a CRS the + bounding polygon is computed from the footprint and the subset trims. + The CRS of the polygon is the one used in the subset expressions, not + necessarily the native CRS of the coverage. It corresponds to the + intersection of the bounding box of the footprint transformed into the + subset CRS and the subset trims. + + This method raises :exc:`~.InvalidRequestException` if the subset + expression syntax is found to be invalid. It raises + :exc:`~.InvalidSubsettingException` if the subset expression is + syntactically correct, but cannot be interpretated, e.g. because of + wrong date/time, number or spatial reference formats. + """ + self._decode() + + time_intv, crs_id, x_bounds, y_bounds = \ + self._decodeTrims(self.trims) + + if x_bounds is None and y_bounds is None: + return None + + if x_bounds is None: + x_bounds = ("unbounded", "unbounded") + if y_bounds is None: + y_bounds = ("unbounded", "unbounded") + + if crs_id == "imageCRS": + if x_bounds[0] == "unbounded": + minx = extent[0] + else: + l = max(float(x_bounds[0]) / float(size_x), 0.0) + minx = extent[0] + l * (extent[2] - extent[0]) + + if y_bounds[0] == "unbounded": + miny = extent[1] + else: + l = max(float(y_bounds[0]) / float(size_y), 0.0) + miny = extent[3] - l * (extent[3] - extent[1]) + + if x_bounds[1] == "unbounded": + maxx = extent[2] + else: + l = min(float(x_bounds[1]) / float(size_x), 1.0) + maxx = extent[0] + l * (extent[2] - extent[0]) + + if y_bounds[1] == "unbounded": + maxy = extent[3] + else: + l = min(float(y_bounds[1]) / float(size_y), 1.0) + maxy = extent[3] - l * (extent[3] - extent[1]) + + poly = Polygon.from_bbox((minx, miny, maxx, maxy)) + poly.srid = srid + + else: + fp_minx, fp_miny, fp_maxx, fp_maxy = \ + footprint.transform(crs_id, True).extent + + if x_bounds[0] == "unbounded": + minx = fp_minx + else: + minx = max(fp_minx, x_bounds[0]) + + if y_bounds[0] == "unbounded": + miny = fp_miny + else: + miny = max(fp_miny, y_bounds[0]) + + if x_bounds[1] == "unbounded": + maxx = fp_maxx + else: + maxx = min(fp_maxx, x_bounds[1]) + + if y_bounds[1] == "unbounded": + maxy = fp_maxy + else: + maxy = min(fp_maxy, y_bounds[1]) + + poly = Polygon.from_bbox((minx, miny, maxx, maxy)) + poly.srid = crs_id + + return poly + + def getSubset(self, x_size, y_size, footprint): + """ + This method returns a :class:`~.BoundedArea` object containing the + bounding coordinates described by the subsets. The ``x_size`` and + ``y_size`` parameters describe the pixel size of the coverage. The + ``footprint`` argument is the coverage footprint as GeoDjango geometry. + If there are no trim statements contained in the subset expressions + ``None`` is returned. + + Depending on the CRS ID used in the subset expressions, the result + may contain pixel coordinates (imageCRS) or coordinates expressed in + the CRS of the subset expression. In the former case, the returned area + corresponds to the intersection of the pixel ranges defined by the trims + and the size of the coverage. In the latter case the bounded area + corresponds to the intersection of the subset with the bounding box of + the footprint in the subset CRS. + + This method raises :exc:`~.InvalidRequestException` if the subset + expression syntax is found to be invalid. It raises + :exc:`~.InvalidSubsettingException` if the subset expression is + syntactically correct, but cannot be interpretated, e.g. because of + wrong date/time, number or spatial reference formats. + """ + self._decode() + + time_intv, crs_id, x_bounds, y_bounds = \ + self._decodeTrims(self.trims) + + if x_bounds is None and y_bounds is None: + return None + + if x_bounds is None: + x_bounds = ("unbounded", "unbounded") + if y_bounds is None: + y_bounds = ("unbounded", "unbounded") + + if crs_id == "imageCRS": + minx, miny, maxx, maxy = self._get_image_extent(x_bounds, y_bounds, x_size, y_size) + else: + minx, miny, maxx, maxy = self._get_geo_extent(crs_id, x_bounds, y_bounds, footprint) + + return BoundedArea( + crs_id = crs_id, + minx = minx, + miny = miny, + maxx = maxx, + maxy = maxy + ) + diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wcs11Transaction.py eoxserver-0.3.2/eoxserver/services/ows/wcs11Transaction.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wcs11Transaction.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wcs11Transaction.py 2013-12-10 14:57:00.000000000 +0000 @@ -1,4 +1,5 @@ #----------------------------------------------------------------------- +# $Id$ # # Description: # @@ -15,8 +16,8 @@ # 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 +# 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 diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wcs1x.py eoxserver-0.3.2/eoxserver/services/ows/wcs1x.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wcs1x.py 1970-01-01 00:00:00.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wcs1x.py 2013-12-10 14:57:00.000000000 +0000 @@ -0,0 +1,464 @@ +#------------------------------------------------------------------------------- +# $Id$ +# +# Project: EOxServer +# Authors: Stephan Krause +# Stephan Meissl +# Martin Paces +# +#------------------------------------------------------------------------------- +# Copyright (C) 2011 EOX IT Services GmbH +# +# 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 of this Software or works derived from this 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. +#------------------------------------------------------------------------------- + +import logging +import os.path + +import mapscript + +from eoxserver.core.system import System +from eoxserver.core.util.xmltools import DOMElementToXML +from eoxserver.core.exceptions import InternalError +from eoxserver.services.interfaces import ( + ServiceHandlerInterface, VersionHandlerInterface, + OperationHandlerInterface +) +from eoxserver.services.owscommon import ( + OWSCommonServiceHandler, OWSCommonVersionHandler, OWSCommon11ExceptionHandler +) +from eoxserver.services.ogc import OGCExceptionHandler +from eoxserver.services.mapserver import ( + gdalconst_to_imagemode, gdalconst_to_imagemode_string +) +from eoxserver.services.exceptions import InvalidRequestException +from eoxserver.services.ows.wcs.common import ( + WCSCommonHandler, getMSOutputFormat, + getMSOutputFormatsAll, getMSWCSFormatMD, + getMSWCSSRSMD, getMSWCSNativeFormat, + getMSWCS10NativeFormat, getMSWCS10FormatMD +) + +# following import is needed by WCS-T +from eoxserver.services.ows.wcst.wcst11AlterCapabilities import wcst11AlterCapabilities11 + +# format registry +from eoxserver.resources.coverages.formats import getFormatRegistry + +# crs utilities +from eoxserver.resources.coverages import crss + + +#============================================================================== +# WCS service handler (required by WCS 2.0 as well ) + +class WCSServiceHandler(OWSCommonServiceHandler): + SERVICE = "wcs" + + REGISTRY_CONF = { + "name": "WCS Service Handler", + "impl_id": "services.ows.wcs1x.WCSServiceHandler", + "registry_values": { + "services.interfaces.service": "wcs" + } + } + +WCSServiceHandlerImplementation = ServiceHandlerInterface.implement(WCSServiceHandler) + +#============================================================================== +# WCS 1.x version handlers + +class WCS10VersionHandler(OWSCommonVersionHandler): + SERVICE = "wcs" + + REGISTRY_CONF = { + "name": "WCS 1.0.0 Version Handler", + "impl_id": "services.ows.wcs1x.WCS10VersionHandler", + "registry_values": { + "services.interfaces.service": "wcs", + "services.interfaces.version": "1.0.0" + } + } + + def _handleException(self, req, exception): + schema_locations = { + "http://www.opengis.net/ogc": "http://schemas.opengis.net/wcs/1.0.0/OGC-exception.xsd" + } + + return OGCExceptionHandler(schema_locations).handleException(req, exception) + +WCS10VersionHandlerImplementation = VersionHandlerInterface.implement(WCS10VersionHandler) + +class WCS11VersionHandler(OWSCommonVersionHandler): + SERVICE = "wcs" + + REGISTRY_CONF = { + "name": "WCS 1.1.0 Version Handler", + "impl_id": "services.ows.wcs1x.WCS11VersionHandler", + "registry_values": { + "services.interfaces.service": "wcs", + "services.interfaces.version": "1.1.0" + } + } + + def _handleException(self, req, exception): + schemas = { + "http://www.opengis.net/ows/1.1": "http://schemas.opengis.net/ows/1.1.0/owsExceptionReport.xsd" + } + return OWSCommon11ExceptionHandler(schemas, "1.1.0").handleException(req, exception) + +WCS11VersionHandlerImplementation = VersionHandlerInterface.implement(WCS11VersionHandler) + +class WCS112VersionHandler(OWSCommonVersionHandler): + SERVICE = "wcs" + + REGISTRY_CONF = { + "name": "WCS 1.1.2 Version Handler", + "impl_id": "services.ows.wcs1x.WCS112VersionHandler", + "registry_values": { + "services.interfaces.service": "wcs", + "services.interfaces.version": "1.1.2" + } + } + + def _handleException(self, req, exception): + schemas = { + "http://www.opengis.net/ows/1.1": "http://schemas.opengis.net/ows/1.1.0/owsExceptionReport.xsd" + } + return OWSCommon11ExceptionHandler(schemas, "1.1.2").handleException(req, exception) + +WCS112VersionHandlerImplementation = VersionHandlerInterface.implement(WCS112VersionHandler) + +#============================================================================== +# WCS 1.x base operation handler + +class WCS1XOperationHandler(WCSCommonHandler): + def createCoverages(self): + visible_expr = System.getRegistry().getFromFactory( + "resources.coverages.filters.CoverageExpressionFactory", + {"op_name": "attr", "operands": ("visible", "=", True)} + ) + factory = System.getRegistry().bind("resources.coverages.wrappers.EOCoverageFactory") + + #NOTE: currently, we do not support referenceable grid coverages in WCS 1.X + + self.coverages = factory.find( + impl_ids = ( + #"resources.coverages.wrappers.PlainCoverageWrapper", + "resources.coverages.wrappers.RectifiedDatasetWrapper", + "resources.coverages.wrappers.RectifiedStitchedMosaicWrapper" + ), + filter_exprs=[visible_expr] + ) + + def configureMapObj(self): + super(WCS1XOperationHandler, self).configureMapObj() + + # set all supported formats + for output_format in getMSOutputFormatsAll(): + self.map.appendOutputFormat(output_format) + + def getMapServerLayer(self, coverage): + layer = super(WCS1XOperationHandler, self).getMapServerLayer(coverage) + + # set the detaset projection + layer.setProjection( crss.asProj4Str( coverage.getSRID() )) + + # set per-layer supported CRSes + layer.setMetaData( 'ows_srs' , getMSWCSSRSMD() ) + layer.setMetaData( 'wcs_srs' , getMSWCSSRSMD() ) + + connector = System.getRegistry().findAndBind( + intf_id = "services.mapserver.MapServerDataConnectorInterface", + params = { + "services.mapserver.data_structure_type": \ + coverage.getDataStructureType() + } + ) + layer = connector.configure(layer, coverage) + + #TODO: Check why the following are set for mosaics only. + if coverage.getType() == "eo.rect_stitched_mosaic": + extent = coverage.getExtent() + size_x, size_y = coverage.getSize() + + layer.setMetaData("wcs_extent", "%.10g %.10g %.10g %.10g" % extent) + layer.setMetaData("wcs_resolution", "%.10g %.10g" % ((extent[2]-extent[0]) / float(size_x), (extent[3]-extent[1]) / float(size_y))) + layer.setMetaData("wcs_size", "%d %d" % (size_x, size_y)) + + # set up rangetype metadata information + rangetype = coverage.getRangeType() + layer.setMetaData("wcs_bandcount", "%d"%len(rangetype.bands)) + layer.setMetaData("wcs_rangeset_name", rangetype.name) + layer.setMetaData("wcs_rangeset_label", rangetype.name) + layer.setMetaData("wcs_rangeset_axes", ",".join(band.name for band in rangetype.bands)) + for band in rangetype.bands: + layer.setMetaData("wcs_%s_label" % band.name, band.name) + layer.setMetaData("wcs_%s_interval" % band.name, "%d %d" % rangetype.getAllowedValues()) + + layer.setMetaData( "wcs_imagemode", gdalconst_to_imagemode_string(rangetype.data_type) ) + + #set the layer's native format + layer.setMetaData( 'wcs_nativeformat' , getMSWCSNativeFormat(coverage.getData().getSourceFormat()) ) + + #set the per-layer supported formats + layer.setMetaData( 'wcs_formats' , getMSWCSFormatMD() ) + + return layer + +#============================================================================== +# WCS 1.x common operation handlers + +class WCS1XDescribeCoverageHandler(WCS1XOperationHandler): + def createCoverages(self): + factory = System.getRegistry().bind("resources.coverages.wrappers.EOCoverageFactory") + + obj_ids = self.req.getParamValue("coverageids") + if obj_ids is None: + key = self.PARAM_SCHEMA["coverageids"]["kvp_key"] + raise InvalidRequestException("Missing required parameter '%s'." % key, "ParameterError", key) + + for coverage_id in obj_ids: + coverage = factory.get(obj_id=coverage_id) + + if ( coverage is None ) or ( coverage.getType() not in ("plain", + "eo.rect_dataset", "eo.rect_stitched_mosaic") ): + raise InvalidRequestException("No rectified coverage with id"\ + " '%s' found."%coverage_id, "NoSuchCoverage", coverage_id) + + self.coverages.append(coverage) + +class WCS1XGetCoverageHandler(WCS1XOperationHandler): + def createCoverages(self): + factory = System.getRegistry().bind("resources.coverages.wrappers.EOCoverageFactory") + + obj_id = self.req.getParamValue("coverageid") + + if obj_id is None: + key = self.PARAM_SCHEMA["coverageid"]["kvp_key"] + raise InvalidRequestException("Missing required parameter '%s.'" % key, "ParameterError", key) + + coverage = factory.get(obj_id=obj_id) + + if ( coverage is None ) or ( coverage.getType() not in ("plain", + "eo.rect_dataset", "eo.rect_stitched_mosaic") ): + raise InvalidRequestException("No rectified coverage with id '%s'"\ + " found."%obj_id, "NoSuchCoverage", obj_id) + + self.coverages.append(coverage) + + def _setParameter(self, key, value): + if key.lower() == "format": + super(WCS1XGetCoverageHandler, self)._setParameter("format", "custom") + else: + super(WCS1XGetCoverageHandler, self)._setParameter(key, value) + + def configureMapObj(self): + format_param = self.req.getParamValue("format") + if not format_param: + raise InvalidRequestException( + "Missing mandatory 'format' parameter", + "MissingParameterValue", + "format" + ) + + output_format = getMSOutputFormat( format_param, self.coverages[0] ) + + self.map.appendOutputFormat(output_format) + self.map.setOutputFormat(output_format) + + super(WCS1XGetCoverageHandler, self).configureMapObj() + +class WCS10GetCapabilitiesHandler(WCS1XOperationHandler): + REGISTRY_CONF = { + "name": "WCS 1.0.0 GetCapabilities Handler", + "impl_id": "services.ows.wcs1x.WCS10GetCapabilitiesHandler", + "registry_values": { + "services.interfaces.service": "wcs", + "services.interfaces.version": "1.0.0", + "services.interfaces.operation": "getcapabilities" + } + } + +WCS10GetCapabilitiesHandlerImplementation = OperationHandlerInterface.implement(WCS10GetCapabilitiesHandler) + +#============================================================================== +# WCS 1.x specific operation handlers + +class WCS11GetCapabilitiesHandler(WCS1XOperationHandler): + REGISTRY_CONF = { + "name": "WCS 1.1.0 GetCapabilities Handler", + "impl_id": "services.ows.wcs1x.WCS11GetCapabilitiesHandler", + "registry_values": { + "services.interfaces.service": "wcs", + "services.interfaces.version": "1.1.0", + "services.interfaces.operation": "getcapabilities" + } + } + + # GetCapabilities() response altering + def _processRequest(self, req): + + # call the original method + response = WCS1XOperationHandler._processRequest( self , req ) + + # alter the capabilities response + response = wcst11AlterCapabilities11( response ) + + return response + +WCS11GetCapabilitiesHandlerImplementation = OperationHandlerInterface.implement(WCS11GetCapabilitiesHandler) + +class WCS112GetCapabilitiesHandler(WCS1XOperationHandler): + REGISTRY_CONF = { + "name": "WCS 1.1.2 GetCapabilities Handler", + "impl_id": "services.ows.wcs1x.WCS112GetCapabilitiesHandler", + "registry_values": { + "services.interfaces.service": "wcs", + "services.interfaces.version": "1.1.2", + "services.interfaces.operation": "getcapabilities" + } + } + + # GetCapabilities() response altering + def _processRequest(self, req): + + # call the original method + response = WCS1XOperationHandler._processRequest( self , req ) + + # alter the capabilities response + response = wcst11AlterCapabilities11( response ) + + return response + +WCS112GetCapabilitiesHandlerImplementation = OperationHandlerInterface.implement(WCS112GetCapabilitiesHandler) + +class WCS10DescribeCoverageHandler(WCS1XDescribeCoverageHandler): + REGISTRY_CONF = { + "name": "WCS 1.0.0 DescribeCoverage Handler", + "impl_id": "services.ows.wcs1x.WCS10DescribeCoverageHandler", + "registry_values": { + "services.interfaces.service": "wcs", + "services.interfaces.version": "1.0.0", + "services.interfaces.operation": "describecoverage" + } + } + + PARAM_SCHEMA = { + "coverageids": {"xml_location": "/{http://www.opengis.net/wcs/1.0.0}Coverage", "xml_type": "string[]", "kvp_key": "coverage", "kvp_type": "stringlist"}, + } + + # special WCS 1.0 format handling + def getMapServerLayer(self, coverage): + layer = super(WCS10DescribeCoverageHandler, self).getMapServerLayer(coverage) + + layer.setMetaData( 'wcs_nativeformat' , getMSWCS10NativeFormat(coverage.getData().getSourceFormat()) ) + layer.setMetaData( 'wcs_formats', getMSWCS10FormatMD() ) + + return layer + +WCS10DescribeCoverageHandlerImplementation = OperationHandlerInterface.implement(WCS10DescribeCoverageHandler) + +class WCS11DescribeCoverageHandler(WCS1XDescribeCoverageHandler): + REGISTRY_CONF = { + "name": "WCS 1.1.0 DescribeCoverage Handler", + "impl_id": "services.ows.wcs1x.WCS11DescribeCoverageHandler", + "registry_values": { + "services.interfaces.service": "wcs", + "services.interfaces.version": "1.1.0", + "services.interfaces.operation": "describecoverage" + } + } + + PARAM_SCHEMA = { + "coverageids": {"xml_location": "/{http://www.opengis.net/wcs/1.1}Identifier", "xml_type": "string[]", "kvp_key": "identifier", "kvp_type": "stringlist"}, + } + +WCS11DescribeCoverageHandlerImplementation = OperationHandlerInterface.implement(WCS11DescribeCoverageHandler) + +class WCS112DescribeCoverageHandler(WCS1XDescribeCoverageHandler): + REGISTRY_CONF = { + "name": "WCS 1.1.2 DescribeCoverage Handler", + "impl_id": "services.ows.wcs1x.WCS112DescribeCoverageHandler", + "registry_values": { + "services.interfaces.service": "wcs", + "services.interfaces.version": "1.1.2", + "services.interfaces.operation": "describecoverage" + } + } + + PARAM_SCHEMA = { + "coverageids": {"xml_location": "/{http://www.opengis.net/wcs/1.1}Identifier", "xml_type": "string[]", "kvp_key": "identifier", "kvp_type": "stringlist"}, + } + +WCS112DescribeCoverageHandlerImplementation = OperationHandlerInterface.implement(WCS112DescribeCoverageHandler) + +class WCS10GetCoverageHandler(WCS1XGetCoverageHandler): + REGISTRY_CONF = { + "name": "WCS 1.0.0 GetCoverage Handler", + "impl_id": "services.ows.wcs1x.WCS10GetCoverageHandler", + "registry_values": { + "services.interfaces.service": "wcs", + "services.interfaces.version": "1.0.0", + "services.interfaces.operation": "getcoverage" + } + } + + PARAM_SCHEMA = { + "coverageid": {"xml_location": "/{http://www.opengis.net/wcs/1.0.0}sourceCoverage", "xml_type": "string", "kvp_key": "coverage", "kvp_type": "string"}, + "format": {"xml_location": "/{http://www.opengis.net/wcs/1.0.0}output/{http://www.opengis.net/wcs/1.0.0}format", "xml_type": "string", "kvp_key": "format", "kvp_type": "string"} + } + +WCS10GetCoverageHandlerImplementation = OperationHandlerInterface.implement(WCS10GetCoverageHandler) + +class WCS11GetCoverageHandler(WCS1XGetCoverageHandler): + REGISTRY_CONF = { + "name": "WCS 1.1.0 GetCoverage Handler", + "impl_id": "services.ows.wcs1x.WCS11GetCoverageHandler", + "registry_values": { + "services.interfaces.service": "wcs", + "services.interfaces.version": "1.1.0", + "services.interfaces.operation": "getcoverage" + } + } + + PARAM_SCHEMA = { + "coverageid": {"xml_location": "/{http://www.opengis.net/ows/1.1}Identifier", "xml_type": "string", "kvp_key": "identifier", "kvp_type": "string"}, + "format": {"xml_location": "/{http://www.opengis.net/wcs/1.1}Output/@format", "xml_type": "string", "kvp_key": "format", "kvp_type": "string"} + } + +WCS11GetCoverageHandlerImplementation = OperationHandlerInterface.implement(WCS11GetCoverageHandler) + +class WCS112GetCoverageHandler(WCS1XGetCoverageHandler): + REGISTRY_CONF = { + "name": "WCS 1.1.2 GetCoverage Handler", + "impl_id": "services.ows.wcs1x.WCS112GetCoverageHandler", + "registry_values": { + "services.interfaces.service": "wcs", + "services.interfaces.version": "1.1.2", + "services.interfaces.operation": "getcoverage" + } + } + + PARAM_SCHEMA = { + "coverageid": {"xml_location": "/{http://www.opengis.net/ows/1.1}Identifier", "xml_type": "string", "kvp_key": "identifier", "kvp_type": "string"}, + "format": {"xml_location": "/{http://www.opengis.net/wcs/1.1}Output/@format", "xml_type": "string", "kvp_key": "format", "kvp_type": "string"} + } + +WCS112GetCoverageHandlerImplementation = OperationHandlerInterface.implement(WCS112GetCoverageHandler) diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wcs20.py eoxserver-0.3.2/eoxserver/services/ows/wcs20.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wcs20.py 1970-01-01 00:00:00.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wcs20.py 2013-12-10 14:57:00.000000000 +0000 @@ -0,0 +1,105 @@ +#------------------------------------------------------------------------------- +# $Id$ +# +# Project: EOxServer +# Authors: Stephan Krause +# Stephan Meissl +# +#------------------------------------------------------------------------------- +# Copyright (C) 2011 EOX IT Services GmbH +# +# 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 of this Software or works derived from this 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. +#------------------------------------------------------------------------------- + +from eoxserver.services.interfaces import ( + VersionHandlerInterface, OperationHandlerInterface +) +from eoxserver.services.owscommon import ( + OWSCommonVersionHandler, OWSCommonExceptionHandler, + OWSCommonConfigReader +) +from eoxserver.services.ows.wcs.wcs20.getcap import ( + WCS20GetCapabilitiesHandler, WCS20CorrigendumGetCapabilitiesHandler +) +from eoxserver.services.ows.wcs.wcs20.desccov import ( + WCS20DescribeCoverageHandler, WCS20CorrigendumDescribeCoverageHandler +) +from eoxserver.services.ows.wcs.wcs20.desceo import ( + WCS20DescribeEOCoverageSetHandler, WCS20CorrigendumDescribeEOCoverageSetHandler +) +from eoxserver.services.ows.wcs.wcs20.getcov import ( + WCS20GetCoverageHandler, WCS20CorrigendumGetCoverageHandler +) + + +class WCS20VersionHandler(OWSCommonVersionHandler): + SERVICE = "wcs" + + REGISTRY_CONF = { + "name": "WCS 2.0 Version Handler", + "impl_id": "services.ows.wcs20.WCS20VersionHandler", + "registry_values": { + "services.interfaces.service": "wcs", + "services.interfaces.version": "2.0.0" + } + } + + def _handleException(self, req, exception): + schemas = { + "http://www.opengis.net/ows/2.0": "http://schemas.opengis.net/ows/2.0/owsAll.xsd" + } + handler = OWSCommonExceptionHandler(schemas) + + handler.setHTTPStatusCodes({ + "NoSuchCoverage": 404, + "InvalidAxisLabel": 404, + "InvalidSubsetting": 404 + }) + + return handler.handleException(req, exception) + + +class WCS20CorrigendumVersionHandler(WCS20VersionHandler): + REGISTRY_CONF = { + "name": "WCS 2.0 Version Handler", + "impl_id": "services.ows.wcs20.WCS20CorrigendumVersionHandler", + "registry_values": { + "services.interfaces.service": "wcs", + "services.interfaces.version": "2.0.1" + } + } + + +WCS20VersionHandlerImplementation = VersionHandlerInterface.implement(WCS20VersionHandler) +WCS20CorrigendumVersionHandlerImplementation = VersionHandlerInterface.implement(WCS20CorrigendumVersionHandler) + +WCS20GetCapabilitiesHandlerImplementation = OperationHandlerInterface.implement(WCS20GetCapabilitiesHandler) +WCS20CorrigendumGetCapabilitiesHandlerImplementation = OperationHandlerInterface.implement(WCS20CorrigendumGetCapabilitiesHandler) + +WCS20DescribeCoverageHandlerImplementation = OperationHandlerInterface.implement(WCS20DescribeCoverageHandler) +WCS20CorrigendumDescribeCoverageHandlerImplementation = OperationHandlerInterface.implement(WCS20CorrigendumDescribeCoverageHandler) + +WCS20DescribeEOCoverageSetHandlerImplementation = OperationHandlerInterface.implement(WCS20DescribeEOCoverageSetHandler) +WCS20CorrigendumDescribeEOCoverageSetHandlerImplementation = OperationHandlerInterface.implement(WCS20CorrigendumDescribeEOCoverageSetHandler) + +WCS20GetCoverageHandlerImplementation = OperationHandlerInterface.implement(WCS20GetCoverageHandler) +WCS20CorrigendumGetCoverageHandlerImplementation = OperationHandlerInterface.implement(WCS20CorrigendumGetCoverageHandler) + + + diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wcst/eopParse.py eoxserver-0.3.2/eoxserver/services/ows/wcst/eopParse.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wcst/eopParse.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wcst/eopParse.py 2013-12-10 14:57:00.000000000 +0000 @@ -1,5 +1,6 @@ #!/usr/bin/env python #----------------------------------------------------------------------- +# $Id$ # # Description: # @@ -16,8 +17,8 @@ # 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 +# 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 diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wcst/gdalGeoTiff.py eoxserver-0.3.2/eoxserver/services/ows/wcst/gdalGeoTiff.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wcst/gdalGeoTiff.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wcst/gdalGeoTiff.py 2013-12-10 14:57:00.000000000 +0000 @@ -1,5 +1,6 @@ #!/usr/bin/env python #----------------------------------------------------------------------- +# $Id$ # # Description: # @@ -16,8 +17,8 @@ # 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 +# 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 diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wcst/qhull.py eoxserver-0.3.2/eoxserver/services/ows/wcst/qhull.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wcst/qhull.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wcst/qhull.py 2013-12-10 14:57:00.000000000 +0000 @@ -1,4 +1,5 @@ #----------------------------------------------------------------------- +# $Id$ # # Description: # @@ -15,8 +16,8 @@ # 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 +# 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 diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wcst/wcst11ActionAdd.py eoxserver-0.3.2/eoxserver/services/ows/wcst/wcst11ActionAdd.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wcst/wcst11ActionAdd.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wcst/wcst11ActionAdd.py 2013-12-10 14:57:00.000000000 +0000 @@ -1,4 +1,5 @@ #----------------------------------------------------------------------- +# $Id$ # # Description: # @@ -15,8 +16,8 @@ # 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 +# 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 diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wcst/wcst11ActionCommon.py eoxserver-0.3.2/eoxserver/services/ows/wcst/wcst11ActionCommon.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wcst/wcst11ActionCommon.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wcst/wcst11ActionCommon.py 2013-12-10 14:57:00.000000000 +0000 @@ -1,4 +1,5 @@ #----------------------------------------------------------------------- +# $Id$ # # Description: # @@ -15,8 +16,8 @@ # 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 +# 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 diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wcst/wcst11ActionDelete.py eoxserver-0.3.2/eoxserver/services/ows/wcst/wcst11ActionDelete.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wcst/wcst11ActionDelete.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wcst/wcst11ActionDelete.py 2013-12-10 14:57:00.000000000 +0000 @@ -1,4 +1,5 @@ #----------------------------------------------------------------------- +# $Id$ # # Description: # @@ -15,8 +16,8 @@ # 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 +# 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 diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wcst/wcst11ActionUpdateAll.py eoxserver-0.3.2/eoxserver/services/ows/wcst/wcst11ActionUpdateAll.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wcst/wcst11ActionUpdateAll.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wcst/wcst11ActionUpdateAll.py 2013-12-10 14:57:00.000000000 +0000 @@ -1,4 +1,5 @@ #----------------------------------------------------------------------- +# $Id$ # # Description: # @@ -15,8 +16,8 @@ # 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 +# 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 diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wcst/wcst11ActionUpdateDataPart.py eoxserver-0.3.2/eoxserver/services/ows/wcst/wcst11ActionUpdateDataPart.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wcst/wcst11ActionUpdateDataPart.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wcst/wcst11ActionUpdateDataPart.py 2013-12-10 14:57:00.000000000 +0000 @@ -1,4 +1,5 @@ #----------------------------------------------------------------------- +# $Id$ # # Description: # @@ -15,8 +16,8 @@ # 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 +# 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 diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wcst/wcst11ActionUpdateMetadata.py eoxserver-0.3.2/eoxserver/services/ows/wcst/wcst11ActionUpdateMetadata.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wcst/wcst11ActionUpdateMetadata.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wcst/wcst11ActionUpdateMetadata.py 2013-12-10 14:57:00.000000000 +0000 @@ -1,4 +1,5 @@ #----------------------------------------------------------------------- +# $Id$ # # Description: # @@ -15,8 +16,8 @@ # 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 +# 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 diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wcst/wcst11AlterCapabilities.py eoxserver-0.3.2/eoxserver/services/ows/wcst/wcst11AlterCapabilities.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wcst/wcst11AlterCapabilities.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wcst/wcst11AlterCapabilities.py 2013-12-10 14:57:00.000000000 +0000 @@ -1,4 +1,5 @@ #------------------------------------------------------------------------------- +# $Id$ # # Project: EOxServer # Authors: Martin Paces @@ -9,8 +10,8 @@ # MEPermission 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 +# 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 diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wcst/wcst11Context.py eoxserver-0.3.2/eoxserver/services/ows/wcst/wcst11Context.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wcst/wcst11Context.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wcst/wcst11Context.py 2013-12-10 14:57:00.000000000 +0000 @@ -1,4 +1,5 @@ #----------------------------------------------------------------------- +# $Id$ # # Description: # @@ -15,8 +16,8 @@ # 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 +# 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 diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wcst/wcst11Exception.py eoxserver-0.3.2/eoxserver/services/ows/wcst/wcst11Exception.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wcst/wcst11Exception.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wcst/wcst11Exception.py 2013-12-10 14:57:00.000000000 +0000 @@ -1,4 +1,5 @@ #----------------------------------------------------------------------- +# $Id$ # # Description: # @@ -15,8 +16,8 @@ # 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 +# 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 diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wcst/wcst11Transaction.py eoxserver-0.3.2/eoxserver/services/ows/wcst/wcst11Transaction.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wcst/wcst11Transaction.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wcst/wcst11Transaction.py 2013-12-10 14:57:00.000000000 +0000 @@ -1,4 +1,5 @@ #----------------------------------------------------------------------- +# $Id$ # # Description: # @@ -15,8 +16,8 @@ # 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 +# 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 diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wcst/wcstCommon.py eoxserver-0.3.2/eoxserver/services/ows/wcst/wcstCommon.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wcst/wcstCommon.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wcst/wcstCommon.py 2013-12-10 14:57:00.000000000 +0000 @@ -1,4 +1,5 @@ #----------------------------------------------------------------------- +# $Id$ # # Description: # @@ -15,8 +16,8 @@ # 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 +# 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 diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wcst/wcstUtils.py eoxserver-0.3.2/eoxserver/services/ows/wcst/wcstUtils.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wcst/wcstUtils.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wcst/wcstUtils.py 2013-12-10 14:57:00.000000000 +0000 @@ -1,4 +1,5 @@ #----------------------------------------------------------------------- +# $Id$ # # Description: # @@ -15,8 +16,8 @@ # 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 +# 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 diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wcst/wcstXML.py eoxserver-0.3.2/eoxserver/services/ows/wcst/wcstXML.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wcst/wcstXML.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wcst/wcstXML.py 2013-12-10 14:57:00.000000000 +0000 @@ -1,4 +1,5 @@ #----------------------------------------------------------------------- +# $Id$ # # Description: # @@ -15,8 +16,8 @@ # 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 +# 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 diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wms/basehandlers.py eoxserver-0.3.2/eoxserver/services/ows/wms/basehandlers.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wms/basehandlers.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wms/basehandlers.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,68 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# Stephan Meissl -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -"""\ -This module contains a set of handler base classes which shall help to implement -a specific handler. Interface methods need to be overridden in order to work, -default methods can be overidden. -""" - -from eoxserver.core import UniqueExtensionPoint -from eoxserver.resources.coverages import models -from eoxserver.services.ows.wms.interfaces import ( - WMSCapabilitiesRendererInterface -) -from eoxserver.services.result import to_http_response - - -class WMSGetCapabilitiesHandlerBase(object): - """ Base for WMS capabilities handlers. - """ - - service = "WMS" - request = "GetCapabilities" - - renderer = UniqueExtensionPoint(WMSCapabilitiesRendererInterface) - - def handle(self, request): - collections_qs = models.Collection.objects \ - .order_by("identifier") \ - .exclude( - footprint__isnull=True, begin_time__isnull=True, - end_time__isnull=True - ) - coverages = [ - coverage for coverage in models.Coverage.objects \ - .filter(visible=True) - if not issubclass(coverage.real_type, models.Collection) - ] - - result, _ = self.renderer.render( - collections_qs, coverages, request.GET.items(), request - ) - return to_http_response(result) diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wms/common.py eoxserver-0.3.2/eoxserver/services/ows/wms/common.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wms/common.py 1970-01-01 00:00:00.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wms/common.py 2013-12-10 14:57:00.000000000 +0000 @@ -0,0 +1,797 @@ +#------------------------------------------------------------------------------- +# $Id$ +# +# Project: EOxServer +# Authors: Stephan Krause +# Stephan Meissl +# Martin Paces +# +#------------------------------------------------------------------------------- +# Copyright (C) 2011 EOX IT Services GmbH +# +# 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 of this Software or works derived from this 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. +#------------------------------------------------------------------------------- + +import logging +import os +from uuid import uuid4 + +import mapscript + +from eoxserver.core.system import System +from eoxserver.core.util.timetools import ( + isotime, getDateTime +) +from eoxserver.core.exceptions import InternalError, InvalidParameterException +from eoxserver.contrib import gdal +from eoxserver.resources.coverages.filters import ( + BoundedArea, TimeInterval +) +from eoxserver.resources.coverages.formats import getFormatRegistry +from eoxserver.resources.coverages import crss +from eoxserver.resources.coverages.dateline import ( + extent_crosses_dateline, wrap_extent_around_dateline +) + +from eoxserver.processing.gdal import reftools as rt +from eoxserver.services.owscommon import OWSCommonConfigReader +from eoxserver.services.mapserver import MapServerOperationHandler +from eoxserver.services.exceptions import InvalidRequestException +from eoxserver.processing.gdal.vrt import create_simple_vrt + + +logger = logging.getLogger(__name__) + +_stripDot = lambda s : s[1:] if s[0] == '.' else s + +def getMSWMSSRSMD(): + """ get the space separated list of CRS EPSG codes to be passed + to MapScript setMedata("wms_srs",...) method + """ + return " ".join(crss.getSupportedCRS_WMS(format_function=crss.asShortCode)) + +class WMSLayer(object): + def __init__(self): + self.group_name = None + + self.temp_files = [] + + def getName(self): + raise NotImplementedError + + def setGroup(self, group_name): + self.group_name = group_name + + def getGroup(self): + return self.group_name + + def getMapServerLayer(self, req): + layer = mapscript.layerObj() + + layer.name = self.getName() + layer.setMetaData("ows_title", self.getName()) + layer.setMetaData("wms_label", self.getName()) + layer.addProcessing("CLOSE_CONNECTION=CLOSE") + + if self.group_name: + layer.group = self.group_name + layer.setMetaData("wms_group_title", self.group_name) + + return layer + + def cleanup(self): + for temp_file in self.temp_files: + try: + os.remove(temp_file) + except: + logger.warning("Could not remove temporary file '%s'" % temp_file) + +class WMSEmptyLayer(WMSLayer): + def __init__(self, layer_name): + super(WMSEmptyLayer, self).__init__() + + self.layer_name = layer_name + + def getName(self): + return self.layer_name + + def getMapServerLayer(self, req): + layer = super(WMSEmptyLayer, self).getMapServerLayer(req) + + layer.setMetaData("wms_enable_request", "getmap") + + return layer + +class WMSCoverageLayer(WMSLayer): + def __init__(self, coverage): + super(WMSCoverageLayer, self).__init__() + + self.coverage = coverage + self.name = None + + def getName(self): + if self.name: + return self.name + else: + return self.coverage.getCoverageId() + + def isRGB(self): + return self.coverage.getRangeType().name == "RGB" + + def isRGBA(self): + return self.coverage.getRangeType().name == "RGBA" + + def isGrayscale(self): + return self.coverage.getRangeType().name == "Grayscale" + + def getBandIndices(self, req): + if len(self.coverage.getRangeType().bands) >= 3: + return [1, 2, 3] + else: + return [1, 1, 1] + + def getBandSelection(self, req): + bands = self.coverage.getRangeType().bands + + if len(bands) in range(1, 5): # 1 to 4 + return bands + else: + return bands[:3] + + def setOffsiteColor(self, layer, bands): + nil_values = [] + + if len(bands) == 1: + if len(bands[0].nil_values) > 0: + nil_values = [ + int(bands[0].nil_values[0].value), + int(bands[0].nil_values[0].value), + int(bands[0].nil_values[0].value) + ] + + layer.offsite = mapscript.colorObj(*nil_values) + + if len(bands) == 3: + for band in bands: + if len(band.nil_values) > 0: + nil_values.append(int(band.nil_values[0].value)) + else: + return + + layer.offsite = mapscript.colorObj(*nil_values) + + def setScale(self, layer): + if self.coverage.getRangeType().data_type != gdal.GDT_Byte: + layer.setProcessingKey("SCALE", "AUTO") + + def configureBands(self, layer, req): + bands = self.getBandSelection(req) + + if not self.isRGB() and not self.isGrayscale() and not self.isRGBA(): + layer.setProcessingKey("BANDS", "%d,%d,%d" % tuple(self.getBandIndices(req))) + + self.setOffsiteColor(layer, bands) + + self.setScale(layer) + + def getMapServerLayer(self, req): + layer = super(WMSCoverageLayer, self).getMapServerLayer(req) + + layer.setMetaData("wms_enable_request", "getcapabilities,getmap") + + for key, value in self.coverage.getLayerMetadata(): + layer.setMetaData(key, value) + + layer.type = mapscript.MS_LAYER_RASTER + layer.setConnectionType(mapscript.MS_RASTER, '') + + self.configureBands(layer, req) + + return layer + +class WMSRectifiedDatasetLayer(WMSCoverageLayer): + + def getMapServerLayer(self, req): + layer = super(WMSRectifiedDatasetLayer, self).getMapServerLayer(req) + + # general rectified coverage metadata + srid = self.coverage.getSRID() + extent = self.coverage.getExtent() + + layer.setProjection(crss.asProj4Str(srid)) + layer.setMetaData("ows_srs", crss.asShortCode(srid)) + layer.setMetaData("wms_srs", crss.asShortCode(srid)) + layer.setMetaData("wms_extent", "%.10g %.10g %.10g %.10g" % extent) + layer.setExtent(*extent) + + # bind rectified dataset + connector = System.getRegistry().findAndBind( + intf_id = "services.mapserver.MapServerDataConnectorInterface", + params = { + "services.mapserver.data_structure_type": \ + self.coverage.getDataStructureType() + } + ) + + layer = connector.configure(layer, self.coverage) + + return layer + +class WMSWrappedRectifiedDatasetLayer(WMSRectifiedDatasetLayer): + + def __init__(self, coverage, vrt_path, extent=None): + super(WMSWrappedRectifiedDatasetLayer, self).__init__(coverage) + self.vrt_path = vrt_path + + + def getMapServerLayer(self, req): + layer = super(WMSWrappedRectifiedDatasetLayer, self).getMapServerLayer(req) + + e = wrap_extent_around_dateline(self.coverage.getExtent(), + self.coverage.getSRID()) + + layer.setMetaData("wms_extent", "%.10g %.10g %.10g %.10g" % e) + layer.setExtent(*e) + + data_package = self.coverage.getData() + data_package.prepareAccess() + ds = gdal.Open(data_package.getGDALDatasetIdentifier()) + + vrt_ds = create_simple_vrt(ds, self.vrt_path) + + + size_x = ds.RasterXSize + size_y = ds.RasterYSize + + dx = abs(e[0] - e[2]) / size_x + dy = abs(e[1] - e[3]) / size_y + + vrt_ds.SetGeoTransform([e[0], dx, 0, e[3], 0, -dy]) + + vrt_ds = None + + layer.data = self.vrt_path + + return layer + + + def cleanup(self): + super(WMSWrappedRectifiedDatasetLayer, self).cleanup() + gdal.Unlink(str(self.vrt_path)) + + + +class WMSReferenceableDatasetLayer(WMSCoverageLayer): + def setScale(self, layer): + layer.setProcessingKey("SCALE", "1,2000") # TODO: make this configurable + + def getMapServerLayer(self, req): + layer = super(WMSReferenceableDatasetLayer, self).getMapServerLayer(req) + + # general rectified coverage metadata + srid = self.coverage.getSRID() + layer.setProjection( crss.asProj4Str( srid ) ) + layer.setMetaData("ows_srs", crss.asShortCode( srid ) ) + layer.setMetaData("wms_srs", crss.asShortCode( srid ) ) + layer.setMetaData("wms_extent", "%.10g %.10g %.10g %.10g" \ + % self.coverage.getExtent()) + layer.setExtent(*self.coverage.getExtent()) + + # project the dataset + vrt_path = self.rectify() + layer.data = vrt_path + self.temp_files.append(vrt_path) + + return layer + + def rectify(self): + ds_path = self.coverage.getData().getGDALDatasetIdentifier() + rt_prm = rt.suggest_transformer( ds_path ) + return rt.create_temporary_rectified_vrt( ds_path , **rt_prm ) + +class WMSRectifiedStitchedMosaicLayer(WMSCoverageLayer): + def getMapServerLayer(self, req): + layer = super(WMSRectifiedStitchedMosaicLayer, self).getMapServerLayer(req) + + connector = System.getRegistry().findAndBind( + intf_id = "services.mapserver.MapServerDataConnectorInterface", + params = { + "services.mapserver.data_structure_type": \ + self.coverage.getDataStructureType() + } + ) + + layer = connector.configure(layer, self.coverage) + + extent = self.coverage.getExtent() + srid = self.coverage.getSRID() + size = self.coverage.getSize() + resolution = ((extent[2]-extent[0]) / float(size[0]), + (extent[1]-extent[3]) / float(size[1])) + + layer.setProjection( crss.asProj4Str( srid ) ) + layer.setMetaData("ows_srs", crss.asShortCode( srid ) ) + layer.setMetaData("wms_srs", crss.asShortCode( srid ) ) + + layer.setExtent(*self.coverage.getExtent()) + layer.setMetaData("wms_extent", "%.10g %.10g %.10g %.10g" % extent) + layer.setMetaData("wms_resolution", "%.10g %.10g" % resolution) + layer.setMetaData("wms_size", "%d %d" % size) + + layer.type = mapscript.MS_LAYER_RASTER + layer.setConnectionType(mapscript.MS_RASTER, '') + + return layer + +class WMSDatasetSeriesLayer(WMSLayer): + def __init__(self, dataset_series): + super(WMSDatasetSeriesLayer, self).__init__() + + self.dataset_series = dataset_series + + def getName(self): + return self.dataset_series.getEOID() + + def getMapServerLayer(self, req): + layer = super(WMSDatasetSeriesLayer, self).getMapServerLayer(req) + + coverages = self.dataset_series.getEOCoverages() + + layer.setMetaData("wms_extent", "%.10g %.10g %.10g %.10g" \ + % self.dataset_series.getWGS84Extent()) + layer.setExtent(*self.dataset_series.getWGS84Extent()) + + time_extent = ",".join( + sorted(set(isotime(coverage.getBeginTime()) for coverage in coverages)) + ) + layer.setMetaData("wms_timeextent", time_extent) + + srid = 4326 # TODO: source CRS of dataset series + layer.setProjection( crss.asProj4Str( srid ) ) + layer.setMetaData("ows_srs", crss.asShortCode( srid ) ) + layer.setMetaData("wms_srs", crss.asShortCode( srid ) ) + + + layer.type = mapscript.MS_LAYER_RASTER + + layer.setConnectionType(mapscript.MS_RASTER, '') + layer.setMetaData("wms_enable_request", "*") + layer.status = mapscript.MS_ON + + for key, value in self.dataset_series.getLayerMetadata(): + layer.setMetaData(key, value) + + # use a dummy coverage to connect to + + connector = System.getRegistry().findAndBind( + intf_id = "services.mapserver.MapServerDataConnectorInterface", + params = { + "services.mapserver.data_structure_type": \ + coverages[0].getDataStructureType() + } + ) + + layer = connector.configure(layer, coverages[0]) + + return layer + +class WMSCommonHandler(MapServerOperationHandler): + PARAM_SCHEMA = { + "service": {"xml_location": "/@service", "xml_type": "string", "kvp_key": "service", "kvp_type": "string"}, + "version": {"xml_location": "/@version", "xml_type": "string", "kvp_key": "version", "kvp_type": "string"}, + "operation": {"xml_location": "/", "xml_type": "localName", "kvp_key": "request", "kvp_type": "string"} + } + + def __init__(self): + super(WMSCommonHandler, self).__init__() + + self.req = None + + self.layers = [] + + self.temp_files = [] + + def _processRequest(self, req): + self.req = req + self.req.setSchema(self.PARAM_SCHEMA) + + try: + self.validateParams() + self.configureRequest() + self.configureMapObj() + self.createLayers() + self.addMapServerLayers() + response = self.postprocess(self.dispatch()) + finally: + self.cleanup() + + return response + + def validateParams(self): + pass + + def _setMapProjection(self): + # set the default EPSG:4326 projection + # TODO: check whether this is really correct + self.map.setProjection( crss.asProj4Str( 4326 ) ) + + def configureMapObj(self): + """ + This method configures the ``ms_req.map`` object (an + instance of ``mapscript.mapObj``) with parameters from the + config. This method can be overridden in order to implement more + sophisticated behaviour. + + @param ms_req An :class:`MapServerRequest` object + + @return None + """ + + self.map.setMetaData("ows_onlineresource", OWSCommonConfigReader().getHTTPServiceURL() + "?") + + # set (per-service) map projection + self._setMapProjection() + + # set the (global) list of supported CRSes + self.map.setMetaData("ows_srs", getMSWMSSRSMD()) + self.map.setMetaData("wms_srs", getMSWMSSRSMD()) + + # set all supported output formats + + # retrieve the format registry + FormatRegistry = getFormatRegistry() + + # define the supported formats + for sf in FormatRegistry.getSupportedFormatsWMS(): + # output format definition + of = mapscript.outputFormatObj( sf.driver, "custom" ) + of.name = sf.mimeType + of.mimetype = sf.mimeType + of.extension = _stripDot(sf.defaultExt) + #of.imagemode = mapscript.MS_IMAGEMODE_BYTE + of.imagemode = mapscript.MS_IMAGEMODE_RGBA + + #add the format + self.map.appendOutputFormat( of ) + + # set the formats supported by getMap WMS operation + self.map.setMetaData("wms_getmap_formatlist", + ",".join( + map(lambda f: f.mimeType, + FormatRegistry.getSupportedFormatsWMS()) + ) + ) + + + def createLayers(self): + pass + + def createCoverageLayer(self, coverage): + logger.debug("Adding WMS coverage layer for coverage '%s'." + % coverage.getCoverageId()) + + if coverage.getType() == "plain": + raise InternalError( + "Plain coverage WMS views are not yet implemented." + ) + elif coverage.getType() == "eo.rect_dataset": + return WMSRectifiedDatasetLayer(coverage) + elif coverage.getType() == "eo.ref_dataset": + return WMSReferenceableDatasetLayer(coverage) + elif coverage.getType() == "eo.rect_stitched_mosaic": + return WMSRectifiedStitchedMosaicLayer(coverage) + + def addLayer(self, layer): + self.layers.append(layer) + + def addMapServerLayers(self): + for layer in self.layers: + self.map.insertLayer(self.getMapServerLayer(layer)) + + def getMapServerLayer(self, layer): + return layer.getMapServerLayer(self.req) + + def postprocess(self, resp): + return resp + + def cleanup(self): + super(WMSCommonHandler, self).cleanup() + + for layer in self.layers: + layer.cleanup() + +class WMS1XGetCapabilitiesHandler(WMSCommonHandler): + + def configureMapObj(self): + super(WMS1XGetCapabilitiesHandler, self).configureMapObj() + + + + def createLayers(self): + visible_expr = System.getRegistry().getFromFactory( + "resources.coverages.filters.CoverageExpressionFactory", + {"op_name": "attr", "operands": ("visible", "=", True)} + ) + + cov_factory = System.getRegistry().bind("resources.coverages.wrappers.EOCoverageFactory") + + for coverage in cov_factory.find(filter_exprs=[visible_expr]): + self.addLayer(self.createCoverageLayer(coverage)) + + dss_factory = System.getRegistry().bind("resources.coverages.wrappers.DatasetSeriesFactory") + + # TODO: find a more efficient way to do this check + for dataset_series in dss_factory.find(): + if len(dataset_series.getEOCoverages()) > 0: + self.layers.append(WMSDatasetSeriesLayer(dataset_series)) + + def getMapServerLayer(self, layer): + ms_layer = super(WMS1XGetCapabilitiesHandler, self).getMapServerLayer(layer) + + ms_layer.status = mapscript.MS_ON + + return ms_layer + +class WMS1XGetMapHandler(WMSCommonHandler): + + def _setMapProjection(self): + self.map.setProjection( crss.asProj4Str( self.getSRID() ) ) + + def getSRSParameterName(self): + raise NotImplementedError() + + def getBoundedArea(self, srid, bbox): + return BoundedArea(srid, *bbox) + + def getTimeFilterExpr(self, time_param): + timestamps = time_param.split("/") + + if len(timestamps) == 1: + try: + timestamp = getDateTime(timestamps[0]) + except InvalidParameterException: + raise InvalidRequestException( + "Invalid 'TIME' parameter format.", + "InvalidParameterValue", + "time" + ) + + return System.getRegistry().getFromFactory( + "resources.coverages.filters.CoverageExpressionFactory", + { + "op_name": "time_slice", + "operands": (timestamp,) + } + ) + + elif len(timestamps) == 2: + try: + time_intv = TimeInterval( + getDateTime(timestamps[0]), + getDateTime(timestamps[1]) + ) + except InvalidParameterException: + raise InvalidRequestException( + "Invalid 'TIME' parameter format.", + "InvalidParameterValue", + "time" + ) + + return System.getRegistry().getFromFactory( + "resources.coverages.filters.CoverageExpressionFactory", + { + "op_name": "time_intersects", + "operands": (time_intv,) + } + ) + else: + raise InvalidRequestException( + "Invalid 'TIME' parameter format.", + "InvalidParameterValue", + "time" + ) + + def getFilterExpressions(self): + try: + bbox = self.req.getParamValue("bbox") + except InvalidParameterException: + raise InvalidRequestException( + "Invalid BBOX parameter value", + "InvalidParameterValue", + "bbox" + ) + + if len(bbox) != 4: + raise InvalidRequestException( + "Wrong number of arguments for 'BBOX' parameter", + "InvalidParameterValue", + "bbox" + ) + + srid = self.getSRID() + + area = self.getBoundedArea(srid, bbox) + + filter_exprs = [] + + # TODO sqlite assert ahead `GEOSCoordSeq_setOrdinate_r` + filter_exprs.append( + System.getRegistry().getFromFactory( + "resources.coverages.filters.CoverageExpressionFactory", + { + "op_name": "footprint_intersects_area", + "operands": (area,) + } + ) + ) + + time_param = self.req.getParamValue("time") + + if time_param is not None: + filter_exprs.append(self.getTimeFilterExpr(time_param)) + + return filter_exprs + + def createLayers(self): + layer_names = self.req.getParamValue("layers") + + if layer_names is None: + raise InvalidRequestException( + "Missing 'LAYERS' parameter", + "MissingParameterValue", + "layers" + ) + + filter_exprs = self.getFilterExpressions() + + for layer_name in layer_names: + self.createLayersForName(layer_name, filter_exprs) + + def createLayersForName(self, layer_name, filter_exprs): + dataset_series = System.getRegistry().getFromFactory( + "resources.coverages.wrappers.DatasetSeriesFactory", + {"obj_id": layer_name} + ) + if dataset_series is not None: + self.createDatasetSeriesLayers(dataset_series, filter_exprs) + else: + coverage = System.getRegistry().getFromFactory( + "resources.coverages.wrappers.EOCoverageFactory", + {"obj_id": layer_name} + ) + if coverage is not None: + if coverage.matches(filter_exprs): + # TODO: check if the coverage crosses the dateline + # if yes, add multiple layers + + if extent_crosses_dateline(coverage.getExtent(), coverage.getSRID()): + logger.debug("Coverage %s crosses the dateline. Special layer setup." % coverage.getCoverageId()) + if coverage.getType() == "eo.rect_dataset": + coverage_id = coverage.getCoverageId() + + unwrapped_coverage_layer = WMSRectifiedDatasetLayer(coverage) + unwrapped_coverage_layer.name = coverage_id + "_unwrapped" + + wrapped_extent = wrap_extent_around_dateline( + coverage.getExtent(), coverage.getSRID() + ) + vrt_path = str("/vsimem/%s/%s.vrt" % (str(uuid4()), coverage_id)) # str to avoid unicode issues + wrapped_coverage_layer = WMSWrappedRectifiedDatasetLayer(coverage, vrt_path, wrapped_extent) + wrapped_coverage_layer.name = coverage_id + "_wrapped" + + unwrapped_coverage_layer.setGroup(coverage_id) + wrapped_coverage_layer.setGroup(coverage_id) + + self.addLayer(unwrapped_coverage_layer) + self.addLayer(wrapped_coverage_layer) + else: + raise NotImplementedError( + "WMS for dateline crossing datasets are not " + "implemented." + ) + else: + self.addLayer(self.createCoverageLayer(coverage)) + else: + self.addLayer(WMSEmptyLayer(coverage.getCoverageId())) + else: + raise InvalidRequestException( + "No coverage or dataset series with EO ID '%s' found" % layer_name, + "LayerNotDefined", "layers" + ) + + def createDatasetSeriesLayers(self, dataset_series, filter_exprs): + def _get_begin_time(coverage): + return coverage.getBeginTime() + + coverages = dataset_series.getEOCoverages(filter_exprs) + eoid = dataset_series.getEOID() + + if len(coverages) == 0: + layer = WMSEmptyLayer(eoid) + + self.addLayer(layer) + + coverages.sort(key=_get_begin_time) + + for coverage in coverages: + if extent_crosses_dateline(coverage.getExtent(), coverage.getSRID()): + logger.debug("Coverage %s crosses the dateline. Special layer setup." % coverage.getCoverageId()) + if coverage.getType() == "eo.rect_dataset": + coverage_id = coverage.getCoverageId() + + unwrapped_coverage_layer = WMSRectifiedDatasetLayer(coverage) + unwrapped_coverage_layer.name = coverage_id + "_unwrapped" + + wrapped_extent = wrap_extent_around_dateline( + coverage.getExtent(), coverage.getSRID() + ) + vrt_path = str("/vsimem/%s/%s.vrt" % (str(uuid4()), coverage_id)) # str to avoid unicode issues + wrapped_coverage_layer = WMSWrappedRectifiedDatasetLayer(coverage, vrt_path, wrapped_extent) + wrapped_coverage_layer.name = coverage_id + "_wrapped" + + unwrapped_coverage_layer.setGroup(eoid) + wrapped_coverage_layer.setGroup(eoid) + + self.addLayer(unwrapped_coverage_layer) + self.addLayer(wrapped_coverage_layer) + else: + raise NotImplementedError( + "WMS for dateline crossing datasets are not " + "implemented." + ) + + else: + layer = self.createCoverageLayer(coverage) + layer.setGroup(eoid) + self.addLayer(layer) + + + def addLayer(self, layer): + # TODO: more performant solution based on hashes + for other_layer in self.layers: + if other_layer.getName() == layer.getName(): + return + + self.layers.append(layer) + + def getSRID(self): + + srs = self.req.getParamValue(self.getSRSParameterName()) + + if srs is None: + raise InvalidRequestException("Missing '%s' parameter" + % self.getSRSParameterName().upper(), "MissingParameterValue", + self.getSRSParameterName()) + + srid = crss.parseEPSGCode(srs,(crss.fromURL,crss.fromURN, + crss.fromShortCode)) + + if srid is None: + raise InvalidRequestException("Invalid '%s' parameter value" + % self.getSRSParameterName().upper(), "InvalidCRS" , + self.getSRSParameterName()) + + return srid + + def getMapServerLayer(self, layer): + ms_layer = super(WMS1XGetMapHandler, self).getMapServerLayer(layer) + + ms_layer.status = mapscript.MS_DEFAULT + + return ms_layer diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wms/exceptions.py eoxserver-0.3.2/eoxserver/services/ows/wms/exceptions.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wms/exceptions.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wms/exceptions.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,51 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2014 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -class LayerNotDefined(Exception): - def __init__(self, layer): - super(LayerNotDefined, self).__init__("No such layer '%s'." % layer) - - locator = "layers" - code = "LayerNotDefined" - -class InvalidCRS(Exception): - def __init__(self, value, crs_param_name): - super(InvalidCRS, self).__init__( - "Invalid '%s' parameter value: '%s'" - % (crs_param_name.upper(), value) - ) - self.locator = crs_param_name - code = "InvalidCRS" - -class InvalidFormat(Exception): - def __init__(self, value): - super(InvalidFormat, self).__init__( - "Unknown format name '%s'" % value - ) - locator = "format" - code = "InvalidFormat" diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wms/interfaces.py eoxserver-0.3.2/eoxserver/services/ows/wms/interfaces.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wms/interfaces.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wms/interfaces.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,86 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2011 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -class WMSCapabilitiesRendererInterface(object): - """ Interface for WMS compatible capabilities renderers. - """ - - def render(self, collections, coverages, request_values): - """ Render a capabilities document, containing metadata of the given - collections and coverages. - """ - - -class WMSMapRendererInterface(object): - """ Interface for WMS compatible map renderers. - """ - - @property - def suffixes(self): - """ Return a list of supported layer suffixes for this renderer. - """ - - def render(self, layer_groups, request_values, **options): - """ Render the given layer hierarchy with the provided request values - and further options. - - ``options`` contains relevant options such as specified bands. - """ - -class WMSFeatureInfoRendererInterface(object): - """ Interface for WMS compatible feature info renderers. - """ - - @property - def suffixes(self): - """ Return a list of supported layer suffixes for this renderer. - """ - - def render(self, layer_groups, request_values, **options): - """ Render the given layer hierarchy with the provided request values - and further options. - - ``options`` contains relevant options such as specified bands. - """ - -class WMSLegendGraphicRendererInterface(object): - """ Interface for WMS compatible legend graphic renderers. - """ - - @property - def suffixes(self): - """ Return a list of supported layer suffixes for this renderer. - """ - - def render(self, collection, eo_object, request_values, **options): - """ Render the given collection and coverage with the provided request - values and further options. - - ``options`` contains relevant options such as specified bands. - """ - diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wms/util.py eoxserver-0.3.2/eoxserver/services/ows/wms/util.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wms/util.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wms/util.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,213 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2011 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -import logging - -from eoxserver.resources.coverages import models -from eoxserver.core.decoders import InvalidParameterException -from eoxserver.core.util.timetools import parse_iso8601 -from eoxserver.services.subset import Trim, Slice -from eoxserver.services.ows.wms.exceptions import LayerNotDefined - - -logger = logging.getLogger(__name__) - -def parse_bbox(string): - try: - bbox = map(float, string.split(",")) - except ValueError: - raise InvalidParameterException("Invalid 'BBOX' parameter.", "bbox") - - try: - minx, miny, maxx, maxy = bbox - except ValueError: - raise InvalidParameterException( - "Wrong number of arguments for 'BBOX' parameter.", "bbox" - ) - - return bbox - - -def parse_time(string): - items = string.split("/") - - if len(items) == 1: - return Slice("t", parse_iso8601(items[0])) - elif len(items) in (2, 3): - # ignore resolution - return Trim("t", parse_iso8601(items[0]), parse_iso8601(items[1])) - - raise InvalidParameterException("Invalid TIME parameter.", "time") - - -def int_or_str(string): - try: - return int(string) - except ValueError: - return string - - -def lookup_layers(layers, subsets, suffixes=None): - """ Performs a layer lookup for the given layer names. Applies the given - subsets and looks up all layers with the given suffixes. Returns a - hierarchy of ``LayerSelection`` objects. - """ - suffix_related_ids = {} - root_group = LayerSelection(None) - suffixes = suffixes or (None,) - logger.debug(str(suffixes)) - - - for layer_name in layers: - for suffix in suffixes: - if not suffix: - identifier = layer_name - elif layer_name.endswith(suffix): - identifier = layer_name[:-len(suffix)] - else: - continue - - # TODO: nasty, nasty bug... dunno where - eo_objects = models.EOObject.objects.filter( - identifier=identifier - ) - if len(eo_objects): - eo_object = eo_objects[0] - break - else: - raise LayerNotDefined(layer_name) - - if models.iscollection(eo_object): - # recursively iterate over all sub-collections and collect all - # coverages - - used_ids = suffix_related_ids.setdefault(suffix, set()) - - def recursive_lookup(collection, suffix, used_ids, subsets): - # get all EO objects related to this collection, excluding - # those already searched - eo_objects = models.EOObject.objects.filter( - collections__in=[collection.pk] - ).exclude( - pk__in=used_ids - ).order_by("begin_time", "end_time") - # apply subsets - eo_objects = subsets.filter(eo_objects) - - selection = LayerSelection() - - # append all retrived EO objects, either as a coverage of - # the real type, or as a subgroup. - for eo_object in eo_objects: - used_ids.add(eo_object.pk) - - if models.iscoverage(eo_object): - selection.append(eo_object.cast(), eo_object.identifier) - elif models.iscollection(eo_object): - selection.extend(recursive_lookup( - eo_object, suffix, used_ids, subsets - )) - else: - pass - - return selection - - root_group.append( - LayerSelection( - eo_object, suffix, - recursive_lookup(eo_object, suffix, used_ids, subsets) - ) - ) - - elif models.iscoverage(eo_object): - # Add a layer selection for the coverage with the suffix - selection = LayerSelection(None, suffix=suffix) - if subsets.matches(eo_object): - selection.append(eo_object.cast(), eo_object.identifier) - else: - selection.append(None, eo_object.identifier) - - root_group.append(selection) - - return root_group - - -class LayerSelection(list): - """ Helper class for hierarchical layer selections. - """ - def __init__(self, collection=None, suffix=None, iterable=None): - self.collection = collection - self.suffix = suffix - if iterable: - super(LayerSelection, self).__init__(iterable) - - - def __contains__(self, eo_object): - for item in self: - try: - if eo_object in item: - return True - except TypeError: - pass - - try: - if eo_object == item[0]: - return True - except IndexError: - pass - - return False - - - def append(self, eo_object_or_selection, name=None): - if isinstance(eo_object_or_selection, LayerSelection): - super(LayerSelection, self).append(eo_object_or_selection) - else: - super(LayerSelection, self).append((eo_object_or_selection, name)) - - - def walk(self, depth_first=True): - """ Yields four-tuples (collections, coverage, name, suffix). - """ - - collection = (self.collection,) if self.collection else () - - for item in self: - try: - for collections, eo_object, name, suffix in item.walk(): - yield ( - collection + collections, - eo_object, name, - suffix or self.suffix - ) - - except AttributeError: - yield collection, item[0], item[1], self.suffix - - if not self: - yield collection, None, None, self.suffix diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wms/v10/getcapabilities.py eoxserver-0.3.2/eoxserver/services/ows/wms/v10/getcapabilities.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wms/v10/getcapabilities.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wms/v10/getcapabilities.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,42 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2011 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -from eoxserver.core import Component, implements -from eoxserver.services.ows.interfaces import ( - ServiceHandlerInterface, GetServiceHandlerInterface -) -from eoxserver.services.ows.wms.basehandlers import ( - WMSGetCapabilitiesHandlerBase -) - - -class WMS10GetCapabilitiesHandler(WMSGetCapabilitiesHandlerBase, Component): - implements(ServiceHandlerInterface) - implements(GetServiceHandlerInterface) - - versions = ("1.0", "1.0.0") diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wms/v10/getfeatureinfo.py eoxserver-0.3.2/eoxserver/services/ows/wms/v10/getfeatureinfo.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wms/v10/getfeatureinfo.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wms/v10/getfeatureinfo.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,88 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2011 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -from itertools import chain - -from eoxserver.core import Component, env, implements, UniqueExtensionPoint -from eoxserver.core.decoders import kvp, typelist, InvalidParameterException -from eoxserver.resources.coverages import models, crss -from eoxserver.services.subset import Subsets, Trim, Slice -from eoxserver.services.ows.interfaces import ( - ServiceHandlerInterface, GetServiceHandlerInterface -) -from eoxserver.services.ows.wms.util import ( - lookup_layers, parse_bbox -) -from eoxserver.services.ows.wms.interfaces import ( - WMSFeatureInfoRendererInterface -) -from eoxserver.services.result import to_http_response - - -class WMS10GetFeatureInfoHandler(Component): - implements(ServiceHandlerInterface) - implements(GetServiceHandlerInterface) - - renderer = UniqueExtensionPoint(WMSFeatureInfoRendererInterface) - - service = ("WMS", None) - versions = ("1.0", "1.0.0") - request = "GetFeatureInfo" - - def handle(self, request): - decoder = WMS10GetFeatureInfoDecoder(request.GET) - - bbox = decoder.bbox - srs = decoder.srs - layers = decoder.layers - - if not layers: - raise InvalidParameterException("No layers specified", "layers") - - minx, miny, maxx, maxy = bbox - - subsets = Subsets(( - Trim("x", minx, maxx), - Trim("y", miny, maxy), - ), crs=srs) - - root_group = lookup_layers(layers, subsets) - - result, _ = self.renderer.render( - root_group, request.GET.items(), request - ) - return to_http_response(result) - - -class WMS10GetFeatureInfoDecoder(kvp.Decoder): - layers = kvp.Parameter(type=typelist(str, ","), num=1) - styles = kvp.Parameter(num="?") - bbox = kvp.Parameter(type=parse_bbox, num=1) - srs = kvp.Parameter(num=1) - width = kvp.Parameter(num=1) - height = kvp.Parameter(num=1) - format = kvp.Parameter(num=1) diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wms/v10/getmap.py eoxserver-0.3.2/eoxserver/services/ows/wms/v10/getmap.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wms/v10/getmap.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wms/v10/getmap.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,93 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2011 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -from eoxserver.core import Component, implements, UniqueExtensionPoint -from eoxserver.core.decoders import kvp, typelist, InvalidParameterException -from eoxserver.resources.coverages import crss -from eoxserver.services.subset import Subsets, Trim -from eoxserver.services.ows.interfaces import ( - ServiceHandlerInterface, GetServiceHandlerInterface -) -from eoxserver.services.ows.wms.util import ( - lookup_layers, parse_bbox -) -from eoxserver.services.ows.wms.interfaces import WMSMapRendererInterface -from eoxserver.services.result import to_http_response -from eoxserver.services.ows.wms.exceptions import InvalidCRS - - -class WMS10GetMapHandler(Component): - implements(ServiceHandlerInterface) - implements(GetServiceHandlerInterface) - - renderer = UniqueExtensionPoint(WMSMapRendererInterface) - - service = ("WMS", None) - versions = ("1.0", "1.0.0") - request = "GetMap" - - def handle(self, request): - decoder = WMS10GetMapDecoder(request.GET) - - bbox = decoder.bbox - srs = decoder.srs - layers = decoder.layers - - if not layers: - raise InvalidParameterException("No layers specified", "layers") - - srid = crss.parseEPSGCode( - srs, (crss.fromShortCode, crss.fromURN, crss.fromURL) - ) - if srid is None: - raise InvalidCRS(srs, "srs") - - # WMS 1.1 knows no swapped axes - minx, miny, maxx, maxy = bbox - - subsets = Subsets(( - Trim("x", minx, maxx), - Trim("y", miny, maxy), - ), crs=srs) - - root_group = lookup_layers(layers, subsets) - - result, _ = self.renderer.render( - root_group, request.GET.items(), subsets=subsets, - width=int(decoder.width), height=int(decoder.height) - ) - return to_http_response(result) - - -class WMS10GetMapDecoder(kvp.Decoder): - layers = kvp.Parameter(type=typelist(str, ","), num=1) - styles = kvp.Parameter(num="?") - bbox = kvp.Parameter(type=parse_bbox, num=1) - srs = kvp.Parameter(num=1) - width = kvp.Parameter(num=1) - height = kvp.Parameter(num=1) - format = kvp.Parameter(num=1) diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wms/v11/getcapabilities.py eoxserver-0.3.2/eoxserver/services/ows/wms/v11/getcapabilities.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wms/v11/getcapabilities.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wms/v11/getcapabilities.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,42 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2011 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -from eoxserver.core import Component, implements -from eoxserver.services.ows.interfaces import ( - ServiceHandlerInterface, GetServiceHandlerInterface -) -from eoxserver.services.ows.wms.basehandlers import ( - WMSGetCapabilitiesHandlerBase -) - - -class WMS11GetCapabilitiesHandler(WMSGetCapabilitiesHandlerBase, Component): - implements(ServiceHandlerInterface) - implements(GetServiceHandlerInterface) - - versions = ("1.1", "1.1.0", "1.1.1") diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wms/v11/getfeatureinfo.py eoxserver-0.3.2/eoxserver/services/ows/wms/v11/getfeatureinfo.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wms/v11/getfeatureinfo.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wms/v11/getfeatureinfo.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,95 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2011 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -from itertools import chain - -from eoxserver.core import Component, env, implements, UniqueExtensionPoint -from eoxserver.core.decoders import kvp, typelist, InvalidParameterException -from eoxserver.resources.coverages import models, crss -from eoxserver.services.subset import Subsets, Trim, Slice -from eoxserver.services.ows.interfaces import ( - ServiceHandlerInterface, GetServiceHandlerInterface -) -from eoxserver.services.ows.wms.util import ( - lookup_layers, parse_bbox, parse_time, int_or_str -) -from eoxserver.services.ows.wms.interfaces import ( - WMSFeatureInfoRendererInterface -) -from eoxserver.services.result import to_http_response - - -class WMS11GetFeatureInfoHandler(Component): - implements(ServiceHandlerInterface) - implements(GetServiceHandlerInterface) - - renderer = UniqueExtensionPoint(WMSFeatureInfoRendererInterface) - - service = "WMS" - versions = ("1.1", "1.1.0", "1.1.1") - request = "GetFeatureInfo" - - def handle(self, request): - decoder = WMS11GetFeatureInfoDecoder(request.GET) - - bbox = decoder.bbox - time = decoder.time - srs = decoder.srs - layers = decoder.layers - - if not layers: - raise InvalidParameterException("No layers specified", "layers") - - minx, miny, maxx, maxy = bbox - - subsets = Subsets(( - Trim("x", minx, maxx), - Trim("y", miny, maxy), - ), crs=srs) - if time: - subsets.append(time) - - renderer = self.renderer - root_group = lookup_layers(layers, subsets, renderer.suffixes) - - result, _ = renderer.render( - root_group, request.GET.items(), request, - time=decoder.time, bands=decoder.dim_bands - ) - return to_http_response(result) - - -class WMS11GetFeatureInfoDecoder(kvp.Decoder): - layers = kvp.Parameter(type=typelist(str, ","), num=1) - styles = kvp.Parameter(num="?") - bbox = kvp.Parameter(type=parse_bbox, num=1) - time = kvp.Parameter(type=parse_time, num="?") - srs = kvp.Parameter(num=1) - width = kvp.Parameter(num=1) - height = kvp.Parameter(num=1) - format = kvp.Parameter(num=1) - dim_bands = kvp.Parameter(type=typelist(int_or_str, ","), num="?") diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wms/v11/getmap.py eoxserver-0.3.2/eoxserver/services/ows/wms/v11/getmap.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wms/v11/getmap.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wms/v11/getmap.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,107 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2011 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -from eoxserver.core import Component, implements, UniqueExtensionPoint -from eoxserver.core.decoders import kvp, typelist, InvalidParameterException -from eoxserver.resources.coverages import crss -from eoxserver.services.subset import Subsets, Trim -from eoxserver.services.ows.interfaces import ( - ServiceHandlerInterface, GetServiceHandlerInterface -) -from eoxserver.services.ows.wms.util import ( - lookup_layers, parse_bbox, parse_time, int_or_str -) -from eoxserver.services.ows.wms.interfaces import WMSMapRendererInterface -from eoxserver.services.result import to_http_response -from eoxserver.services.ows.wms.exceptions import InvalidCRS - - -class WMS11GetMapHandler(Component): - implements(ServiceHandlerInterface) - implements(GetServiceHandlerInterface) - - renderer = UniqueExtensionPoint(WMSMapRendererInterface) - - service = "WMS" - versions = ("1.1", "1.1.0", "1.1.1") - request = "GetMap" - - def handle(self, request): - decoder = WMS11GetMapDecoder(request.GET) - - bbox = decoder.bbox - time = decoder.time - srs = decoder.srs - layers = decoder.layers - - if not layers: - raise InvalidParameterException("No layers specified", "layers") - - srid = crss.parseEPSGCode( - srs, (crss.fromShortCode, crss.fromURN, crss.fromURL) - ) - if srid is None: - raise InvalidCRS(srs, "srs") - - # WMS 1.1 knows no swapped axes - minx, miny, maxx, maxy = bbox - - subsets = Subsets(( - Trim("x", minx, maxx), - Trim("y", miny, maxy), - ), crs=srs) - if time: - subsets.append(time) - - renderer = self.renderer - root_group = lookup_layers(layers, subsets, renderer.suffixes) - - result, _ = renderer.render( - root_group, request.GET.items(), - width=int(decoder.width), height=int(decoder.height), - time=decoder.time, bands=decoder.dim_bands, subsets=subsets, - elevation=decoder.elevation, - dimensions=dict( - (key[4:], values) for key, values in decoder.dimensions - ) - ) - return to_http_response(result) - - -class WMS11GetMapDecoder(kvp.Decoder): - layers = kvp.Parameter(type=typelist(str, ","), num=1) - styles = kvp.Parameter(num="?") - bbox = kvp.Parameter(type=parse_bbox, num=1) - time = kvp.Parameter(type=parse_time, num="?") - srs = kvp.Parameter(num=1) - width = kvp.Parameter(num=1) - height = kvp.Parameter(num=1) - format = kvp.Parameter(num=1) - dim_bands = kvp.Parameter(type=typelist(int_or_str, ","), num="?") - elevation = kvp.Parameter(type=float, num="?") - dimensions = kvp.MultiParameter(lambda s: s.startswith("dim_"), locator="dimension", num="*") diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wms/v13/exceptionhandler.py eoxserver-0.3.2/eoxserver/services/ows/wms/v13/exceptionhandler.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wms/v13/exceptionhandler.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wms/v13/exceptionhandler.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,161 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -from cStringIO import StringIO - -try: - from PIL import Image, ImageFont, ImageDraw - HAS_PIL = True -except ImportError: - HAS_PIL = False -from lxml.builder import ElementMaker - -from eoxserver.core import Component, implements -from eoxserver.core.decoders import kvp, lower -from eoxserver.core.util.xmltools import XMLEncoder, NameSpace, NameSpaceMap -from eoxserver.services.ows.interfaces import ExceptionHandlerInterface - - -class WMS13ExceptionHandler(Component): - implements(ExceptionHandlerInterface) - - service = "WMS" - versions = ("1.3.0", "1.3") - request = None - - def get_encoder(self, request): - decoder = WMS13Decoder(request.GET) - exceptions = decoder.exceptions - if exceptions in ("xml", "application/vnd.ogc.se_xml") or not HAS_PIL: - return WMS13ExceptionXMLEncoder() - elif exceptions in ("inimage", "blank"): - return WMS13ExceptionImageEncoder( - decoder.width, decoder.height, decoder.format, decoder.bgcolor, - exceptions=="blank" - ) - print decoder.exceptions - - def handle_exception(self, request, exception): - encoder = self.get_encoder(request) - - locator = getattr(exception, "locator", None) - code = getattr(exception, "code", None) or type(exception).__name__ - - return ( - encoder.serialize( - encoder.encode_exception( - str(exception), code, locator - ), - ), - encoder.content_type, - 400 - ) - - -class WMS13Decoder(kvp.Decoder): - width = kvp.Parameter(type=int, num="?") - height = kvp.Parameter(type=int, num="?") - format = kvp.Parameter(num="?") - bgcolor = kvp.Parameter(num="?") - exceptions = kvp.Parameter(num="?", type=lower, default="xml") - - -ns_ogc = NameSpace("http://www.opengis.net/ogc", "ogc") -nsmap = NameSpaceMap(ns_ogc) -OGC = ElementMaker(namespace=ns_ogc.uri, nsmap=nsmap) - -class WMS13ExceptionXMLEncoder(XMLEncoder): - def encode_exception(self, message, code, locator=None): - attributes = { - "code": code - } - if locator: - attributes["locator"] = locator - - return OGC("ServiceExceptionReport", - OGC("ServiceException", - str(message), - **attributes - ), - version="1.3.0" - ) - - @property - def content_type(self): - return "application/vnd.ogc.se_xml" - - def get_schema_locations(self): - return { - "http://www.opengis.net/ogc": "http://schemas.opengis.net/wms/1.3.0/exceptions_1_3_0.xsd" - } - - -class WMS13ExceptionImageEncoder(object): - def __init__(self, width=None, height=None, format=None, bgcolor=None, blank=False): - self.width = width if width > 0 else 256 - self.height = height if height > 0 else 256 - if "/" in format: - format = format[format.find("/") + 1:] - self.format = format or "jpeg" - self.bgcolor = bgcolor or "white" - self.blank = blank - - @property - def content_type(self): - return "image/%s" % self.format - - def encode_exception(self, message, code, locator=None): - width, height = self.width, self.height - image = Image.new("RGB", (width, height), self.bgcolor) - - # if requested draw the exception string in the image - if not self.blank: - font = ImageFont.load_default() - draw = ImageDraw.Draw(image) - yoffset = 0 - while len(message): - for i in xrange(len(message)): - part = message if i == 0 else message[:-i] - xsize, ysize = font.getsize(part) - print i, xsize, ysize, part - if xsize < width: - break - draw.text((0, yoffset), part, font=font, fill="red") - yoffset += ysize - message = message[-i:] - if i == 0: - break - return image - - def serialize(self, image): - f = StringIO() - try: - image.save(f, self.format) - except (IOError, KeyError): - image.save(f, "jpeg") # Fallback solution - return f.getvalue() diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wms/v13/getcapabilities.py eoxserver-0.3.2/eoxserver/services/ows/wms/v13/getcapabilities.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wms/v13/getcapabilities.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wms/v13/getcapabilities.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,42 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2011 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -from eoxserver.core import Component, implements -from eoxserver.services.ows.interfaces import ( - ServiceHandlerInterface, GetServiceHandlerInterface -) -from eoxserver.services.ows.wms.basehandlers import ( - WMSGetCapabilitiesHandlerBase -) - - -class WMS13GetCapabilitiesHandler(WMSGetCapabilitiesHandlerBase, Component): - implements(ServiceHandlerInterface) - implements(GetServiceHandlerInterface) - - versions = ("1.3", "1.3.0",) diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wms/v13/getfeatureinfo.py eoxserver-0.3.2/eoxserver/services/ows/wms/v13/getfeatureinfo.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wms/v13/getfeatureinfo.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wms/v13/getfeatureinfo.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,104 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2011 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -from itertools import chain - -from eoxserver.core import Component, env, implements, UniqueExtensionPoint -from eoxserver.core.decoders import kvp, typelist, InvalidParameterException -from eoxserver.resources.coverages import models, crss -from eoxserver.services.subset import Subsets, Trim, Slice -from eoxserver.services.ows.interfaces import ( - ServiceHandlerInterface, GetServiceHandlerInterface -) -from eoxserver.services.ows.wms.util import ( - lookup_layers, parse_bbox, parse_time, int_or_str -) -from eoxserver.services.ows.wms.interfaces import ( - WMSFeatureInfoRendererInterface -) -from eoxserver.services.result import to_http_response - - -class WMS13GetFeatureInfoHandler(Component): - implements(ServiceHandlerInterface) - implements(GetServiceHandlerInterface) - - renderer = UniqueExtensionPoint(WMSFeatureInfoRendererInterface) - - service = "WMS" - versions = ("1.3.0", "1.3") - request = "GetFeatureInfo" - - def handle(self, request): - decoder = WMS13GetFeatureInfoDecoder(request.GET) - - bbox = decoder.bbox - time = decoder.time - crs = decoder.crs - layers = decoder.layers - - if not layers: - raise InvalidParameterException("No layers specified", "layers") - - srid = crss.parseEPSGCode( - crs, (crss.fromShortCode, crss.fromURN, crss.fromURL) - ) - if srid is None: - raise InvalidParameterException("Invalid CRS specifier.", "crs") - - if crss.hasSwappedAxes(srid): - miny, minx, maxy, maxx = bbox - else: - minx, miny, maxx, maxy = bbox - - subsets = Subsets(( - Trim("x", minx, maxx), - Trim("y", miny, maxy), - ), crs=crs) - if time: - subsets.append(time) - - renderer = self.renderer - root_group = lookup_layers(layers, subsets, renderer.suffixes) - - result, _ = renderer.render( - root_group, request.GET.items(), request, - time=decoder.time, bands=decoder.dim_bands - ) - return to_http_response(result) - - -class WMS13GetFeatureInfoDecoder(kvp.Decoder): - layers = kvp.Parameter(type=typelist(str, ","), num=1) - styles = kvp.Parameter(num="?") - bbox = kvp.Parameter(type=parse_bbox, num=1) - time = kvp.Parameter(type=parse_time, num="?") - crs = kvp.Parameter(num=1) - width = kvp.Parameter(num=1) - height = kvp.Parameter(num=1) - format = kvp.Parameter(num=1) - dim_bands = kvp.Parameter(type=typelist(int_or_str, ","), num="?") diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wms/v13/getlegendgraphic.py eoxserver-0.3.2/eoxserver/services/ows/wms/v13/getlegendgraphic.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wms/v13/getlegendgraphic.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wms/v13/getlegendgraphic.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,125 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2011 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -from eoxserver.core import Component, implements, UniqueExtensionPoint -from eoxserver.core.decoders import kvp, InvalidParameterException -from eoxserver.resources.coverages import models -from eoxserver.services.ows.interfaces import ( - ServiceHandlerInterface, GetServiceHandlerInterface -) -from eoxserver.services.ows.wms.util import LayerSelection -from eoxserver.services.ows.wms.interfaces import ( - WMSLegendGraphicRendererInterface -) -from eoxserver.services.result import to_http_response - - -class WMS13GetLegendGraphicHandler(Component): - implements(ServiceHandlerInterface) - implements(GetServiceHandlerInterface) - - renderer = UniqueExtensionPoint(WMSLegendGraphicRendererInterface) - - service = "WMS" - versions = ("1.3.0", "1.3") - request = "GetLegendGraphic" - - def handle(self, request): - decoder = WMS13GetLegendGraphicDecoder(request.GET) - - layer_name = decoder.layer - coverage_id = decoder.coverage - - suffixes = self.renderer.suffixes - for suffix in suffixes: - try: - if len(suffix or "") == 0: - identifier = layer_name - else: - identifier = layer_name[-len(suffix):] - eo_object = models.EOObject.objects.get(identifier=identifier) - break - except models.EOObject.DoesNotExist: - pass - else: - raise InvalidParameterException( - "No such layer '%s'." % layer_name, "layer" - ) - - if models.iscollection(eo_object): - def recursive_lookup(collection, used_ids, suffix): - eo_objects = models.EOObject.objects.filter( - collections__in=[collection.pk] - ).exclude( - pk__in=used_ids - ) - - result = [] - for eo_object in eo_objects: - used_ids.add(eo_object.pk) - - if models.iscoverage(eo_object): - result.append((eo_object.cast(), suffix)) - elif models.iscollection(eo_object): - result.extend( - recursive_lookup(eo_object, used_ids, suffix) - ) - else: - pass - - return result - - used_ids = set() - coverages = recursive_lookup(eo_object, used_ids, suffix) - collection = eo_object - - if coverage_id: - for coverage in coverages: - if coverage.identifier == coverage_id: - coverages = ((coverage, suffix),) - break - else: - raise InvalidParameterException( - "Layer '%s' does not contain a coverage with ID '%s'.", - "coverage" - ) - else: - collection = None - coverages = ((eo_object.cast(), suffix),) - - layer_selection = LayerSelection( - collection.identifier if collection else None - ) - layer_selection.extend(coverages) - - result, _ = self.renderer.render(layer_selection, request.GET.items()) - return to_http_response(result) - - -class WMS13GetLegendGraphicDecoder(kvp.Decoder): - layer = kvp.Parameter(num=1) - coverage = kvp.Parameter(num="?") diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wms/v13/getmap.py eoxserver-0.3.2/eoxserver/services/ows/wms/v13/getmap.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wms/v13/getmap.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wms/v13/getmap.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,110 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2011 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -from eoxserver.core import Component, implements, UniqueExtensionPoint -from eoxserver.core.decoders import kvp, typelist, InvalidParameterException -from eoxserver.resources.coverages import crss -from eoxserver.services.subset import Subsets, Trim -from eoxserver.services.ows.interfaces import ( - ServiceHandlerInterface, GetServiceHandlerInterface -) -from eoxserver.services.ows.wms.util import ( - lookup_layers, parse_bbox, parse_time, int_or_str -) -from eoxserver.services.ows.wms.interfaces import WMSMapRendererInterface -from eoxserver.services.result import to_http_response -from eoxserver.services.ows.wms.exceptions import InvalidCRS - - -class WMS13GetMapHandler(Component): - implements(ServiceHandlerInterface) - implements(GetServiceHandlerInterface) - - renderer = UniqueExtensionPoint(WMSMapRendererInterface) - - service = ("WMS", None) - versions = ("1.3.0", "1.3") - request = "GetMap" - - def handle(self, request): - decoder = WMS13GetMapDecoder(request.GET) - - bbox = decoder.bbox - time = decoder.time - crs = decoder.crs - layers = decoder.layers - - if not layers: - raise InvalidParameterException("No layers specified", "layers") - - srid = crss.parseEPSGCode( - crs, (crss.fromShortCode, crss.fromURN, crss.fromURL) - ) - if srid is None: - raise InvalidCRS(crs, "crs") - - if crss.hasSwappedAxes(srid): - miny, minx, maxy, maxx = bbox - else: - minx, miny, maxx, maxy = bbox - - subsets = Subsets(( - Trim("x", minx, maxx), - Trim("y", miny, maxy), - ), crs=crs) - if time: - subsets.append(time) - - renderer = self.renderer - root_group = lookup_layers(layers, subsets, renderer.suffixes) - - result, _ = renderer.render( - root_group, request.GET.items(), - width=int(decoder.width), height=int(decoder.height), - time=decoder.time, bands=decoder.dim_bands, subsets=subsets, - elevation=decoder.elevation, - dimensions=dict( - (key[4:], values) for key, values in decoder.dimensions - ) - ) - - return to_http_response(result) - - -class WMS13GetMapDecoder(kvp.Decoder): - layers = kvp.Parameter(type=typelist(str, ","), num=1) - styles = kvp.Parameter(num="?") - bbox = kvp.Parameter(type=parse_bbox, num=1) - time = kvp.Parameter(type=parse_time, num="?") - crs = kvp.Parameter(num=1) - width = kvp.Parameter(num=1) - height = kvp.Parameter(num=1) - format = kvp.Parameter(num=1) - dim_bands = kvp.Parameter(type=typelist(int_or_str, ","), num="?") - elevation = kvp.Parameter(type=float, num="?") - dimensions = kvp.MultiParameter(lambda s: s.startswith("dim_"), locator="dimension", num="*") diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wms/wms1011.py eoxserver-0.3.2/eoxserver/services/ows/wms/wms1011.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wms/wms1011.py 1970-01-01 00:00:00.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wms/wms1011.py 2013-12-10 14:57:00.000000000 +0000 @@ -0,0 +1,217 @@ +#------------------------------------------------------------------------------- +# $Id$ +# +# Project: EOxServer +# Authors: Stephan Krause +# Stephan Meissl +# +#------------------------------------------------------------------------------- +# Copyright (C) 2011 EOX IT Services GmbH +# +# 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 of this Software or works derived from this 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. +#------------------------------------------------------------------------------- +import mapscript + +from eoxserver.core.system import System +from eoxserver.core.util.xmltools import XMLEncoder +from eoxserver.services.exceptions import InvalidRequestException +from eoxserver.services.base import BaseExceptionHandler +from eoxserver.services.interfaces import ( + ExceptionHandlerInterface, ExceptionEncoderInterface +) +from eoxserver.services.owscommon import OWSCommonVersionHandler +from eoxserver.services.ows.wms.common import WMS1XGetMapHandler + +class WMS10VersionHandler(OWSCommonVersionHandler): + REGISTRY_CONF = { + "name": "WMS 1.0 Version Handler", + "impl_id": "services.ows.wms1x.WMS10VersionHandler", + "registry_values": { + "services.interfaces.service": "wms", + "services.interfaces.version": "1.0.0", + } + } + + SERVICE = "wms" + VERSION = "1.0.0" + + def _handleException(self, req, exception): + return WMS10ExceptionHandler().handleException(req, exception) + +class WMS110VersionHandler(OWSCommonVersionHandler): + REGISTRY_CONF = { + "name": "WMS 1.1.0 Version Handler", + "impl_id": "services.ows.wms1x.WMS110VersionHandler", + "registry_values": { + "services.interfaces.service": "wms", + "services.interfaces.version": "1.1.0" + } + } + + SERVICE = "wms" + VERSIONS = "1.1.0" + + def _handleException(self, req, exception): + return WMS11ExceptionHandler().handleException(req, exception) + +class WMS111VersionHandler(OWSCommonVersionHandler): + REGISTRY_CONF = { + "name": "WMS 1.1.1 Version Handler", + "impl_id": "services.ows.wms1x.WMS111VersionHandler", + "registry_values": { + "services.interfaces.service": "wms", + "services.interfaces.version": "1.1.1" + } + } + + SERVICE = "wms" + VERSIONS = "1.1.1" + + def _handleException(self, req, exception): + return WMS11ExceptionHandler().handleException(req, exception) + +class WMS10_11GetMapHandler(WMS1XGetMapHandler): + PARAM_SCHEMA = { + "service": {"xml_location": "/@service", "xml_type": "string", "kvp_key": "service", "kvp_type": "string"}, + "version": {"xml_location": "/@version", "xml_type": "string", "kvp_key": "version", "kvp_type": "string"}, + "operation": {"xml_location": "/", "xml_type": "localName", "kvp_key": "request", "kvp_type": "string"}, + "srs": {"xml_location": "/srs", "xml_type": "string", "kvp_key": "srs", "kvp_type": "string"}, # TODO: check XML location + "layers": {"xml_location": "/layer", "xml_type": "string[]", "kvp_key": "layers", "kvp_type": "stringlist"}, # TODO: check XML location + "time": {"xml_location": "/time", "xml_type": "string", "kvp_key": "time", "kvp_type": "string"}, + "bbox": {"xml_location": "/bbox", "xml_type": "floatlist", "kvp_key": "bbox", "kvp_type": "floatlist"} + } + + def getSRSParameterName(self): + return "srs" + + def configureMapObj(self): + super(WMS10_11GetMapHandler, self).configureMapObj() + + self.map.setMetaData("wms_exceptions_format", "application/vnd.ogc.se_xml") + + def getMapServerLayer(self, layer): + ms_layer = super(WMS10_11GetMapHandler, self).getMapServerLayer(layer) + ms_layer.setMetaData("wms_exceptions_format","application/vnd.ogc.se_xml") + + return ms_layer + +class WMS10ExceptionHandler(BaseExceptionHandler): + REGISTRY_CONF = { + "name": "WMS 1.0 Exception Handler", + "impl_id": "services.ows.wms1x.WMS10ExceptionHandler", + "registry_values": { + "services.interfaces.exception_scheme": "wms_1.0" + } + } + + def _filterExceptions(self, exception): + if not isinstance(exception, InvalidRequestException): + raise + + def _getEncoder(self): + return WMS10ExceptionEncoder() + + def _getContentType(self, exception): + return "text/xml" + +WMS10ExceptionHandlerImplementation = ExceptionHandlerInterface.implement(WMS10ExceptionHandler) + +class WMS10ExceptionEncoder(XMLEncoder): + REGISTRY_CONF = { + "name": "WMS 1.0 Exception Report Encoder", + "impl_id": "services.ows.wms1x.WMS10ExceptionEncoder", + "registry_values": { + "services.interfaces.exception_scheme": "wms_1.0" + } + } + + def encodeExceptionReport(self, exception_text, exception_code): + return self._makeElement("", "WMTException", [ + ("", "@version", "1.0.0"), + ("", "@@", str(exception_text)) + ]) + + def encodeInvalidRequestException(self, exception): + return self.encodeExceptionReport(exception.msg, exception.error_code) + + def encodeVersionNegotiationException(self, exception): + return "" + + def encodeException(self, exception): + return self.encodeExceptionReport("Internal Server Error", "NoApplicableCode") + +WMS10ExceptionEncoderImplementation = ExceptionEncoderInterface.implement(WMS10ExceptionEncoder) + +class WMS11ExceptionHandler(BaseExceptionHandler): + REGISTRY_CONF = { + "name": "WMS 1.1 Exception Handler", + "impl_id": "services.ows.wms1x.WMS11ExceptionHandler", + "registry_values": { + "services.interfaces.exception_scheme": "wms_1.1" + } + } + + def _filterExceptions(self, exception): + if not isinstance(exception, InvalidRequestException): + raise + + def _getEncoder(self): + return WMS11ExceptionEncoder() + + def _getContentType(self, exception): + return "application/vnd.ogc.se_xml" + +WMS11ExceptionHandlerImplementation = ExceptionHandlerInterface.implement(WMS11ExceptionHandler) + +class WMS11ExceptionEncoder(XMLEncoder): + REGISTRY_CONF = { + "name": "WMS 1.0 Exception Report Encoder", + "impl_id": "services.ows.wms1x.WMS11ExceptionEncoder", + "registry_values": { + "services.interfaces.exception_scheme": "wms_1.1" + } + } + + def _initializeNamespaces(self): + return { + "ogc": "http://www.opengis.net/ogc", + "xsi": "http://www.w3.org/2001/XMLSchema-instance" + } + + def encodeExceptionReport(self, exception_text, exception_code): + return self._makeElement("", "ServiceExceptionReport", [ + ("", "@version", "1.1.1"), + ("xsi", "schemaLocation", "http://www.opengis.net/ogc http://schemas.opengis.net/wms/1.1.1/OGC-exception.xsd"), + ("", "ServiceException", [ + ("", "@code", exception_code), + ("", "@@", exception_text) + ]) + ]) + + def encodeInvalidRequestException(self, exception): + return self.encodeExceptionReport(exception.msg, exception.error_code) + + def encodeVersionNegotiationException(self, exception): + return "" + + def encodeException(self, exception): + return self.encodeExceptionReport("Internal Server Error", "NoApplicableCode") + +WMS11ExceptionEncoderImplementation = ExceptionEncoderInterface.implement(WMS11ExceptionEncoder) + diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wms/wms13.py eoxserver-0.3.2/eoxserver/services/ows/wms/wms13.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wms/wms13.py 1970-01-01 00:00:00.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wms/wms13.py 2013-12-10 14:57:00.000000000 +0000 @@ -0,0 +1,1187 @@ +#------------------------------------------------------------------------------- +# $Id$ +# +# Project: EOxServer +# Authors: Stephan Krause +# Stephan Meissl +# +#------------------------------------------------------------------------------- +# Copyright (C) 2011 EOX IT Services GmbH +# +# 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 of this Software or works derived from this 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. +#------------------------------------------------------------------------------- + +import os.path +from xml.dom import minidom +import logging + +import mapscript +from django.conf import settings + +from eoxserver.core.system import System +from eoxserver.core.util.timetools import getDateTime, isotime +from eoxserver.core.util.xmltools import XMLEncoder, DOMtoXML +from eoxserver.core.exceptions import InternalError, InvalidParameterException +from eoxserver.resources.coverages.filters import BoundedArea, TimeInterval +from eoxserver.resources.coverages import crss +from eoxserver.services.base import BaseExceptionHandler +from eoxserver.services.requests import Response +from eoxserver.services.interfaces import ( + ExceptionHandlerInterface, ExceptionEncoderInterface +) +from eoxserver.services.owscommon import OWSCommonVersionHandler +from eoxserver.services.ows.wms.common import ( + WMSLayer, WMSEmptyLayer, WMSCoverageLayer, WMSDatasetSeriesLayer, + WMSRectifiedDatasetLayer, WMSReferenceableDatasetLayer, + WMSRectifiedStitchedMosaicLayer, WMSCommonHandler, + WMS1XGetCapabilitiesHandler, WMS1XGetMapHandler +) +from eoxserver.services.exceptions import InvalidRequestException + + +logger = logging.getLogger(__name__) + +class EOWMSOutlinesLayer(WMSLayer): + STYLES = ( + ("red", 255, 0, 0), + ("green", 0, 128, 0), + ("blue", 0, 0, 255), + ("white", 255, 255, 255), + ("black", 0, 0, 0), + ("yellow", 255, 255, 0), + ("orange", 255, 165, 0), + ("magenta", 255, 0, 255), + ("cyan", 0, 255, 255), + ("brown", 165, 42, 42) + ) + + DEFAULT_STYLE = "red" + + def createOutlineClass(self, name, r, g, b): + outline_class = mapscript.classObj() + outline_style = mapscript.styleObj() + outline_style.outlinecolor = mapscript.colorObj(r, g, b) + outline_class.insertStyle(outline_style) + outline_class.group = name + + return outline_class + + def _get_sql_timestamp(self, timestamp): + return timestamp.strftime("%Y-%m-%d %H:%M:%S") + + def _get_base_query(self, time_clause=""): + raise InternalError("Not implemented.") + + def _get_query_with_timestamp(self, timestamp): + time_clause = " AND '%s' BETWEEN eomd.timestamp_begin AND eomd.timestamp_end" % isotime(timestamp) + + return self._get_base_query(time_clause) + + def _get_query_with_time_interval(self, begin_time, end_time): + time_clause = " AND NOT (eomd.timestamp_begin > '%s' OR eomd.timestamp_end < '%s')" % ( + isotime(end_time), isotime(begin_time) + ) + + return self._get_base_query(time_clause) + + def _get_query_without_time(self): + return self._get_base_query() + + + def getSubQuery(self, req): + if req.getParamValue("operation").lower() in ("getmap", "getfeatureinfo"): + time_param = req.getParamValue("time") + + if time_param: + timestamps = time_param.split("/") + + if len(timestamps) == 1: + try: + timestamp = getDateTime(timestamps[0]) + except InvalidParameterException: + raise InvalidRequestException( + "Invalid TIME parameter format.", + "InvalidParameterValue", + "time" + ) + + return self._get_query_with_timestamp(timestamp) + elif len(timestamps) == 2: + try: + begin_time = getDateTime(timestamps[0]) + end_time = getDateTime(timestamps[1]) + except InvalidParameterException: + raise InvalidRequestException( + "Invalid TIME parameter format.", + "InvalidParameterValue", + "time" + ) + + return self._get_query_with_time_interval(begin_time, end_time) + else: + return self._get_query_without_time() + + else: + return self._get_query_without_time() + + + def configureConnection(self, layer, req): + db_conf = settings.DATABASES["default"] + + if db_conf["ENGINE"] == "django.contrib.gis.db.backends.postgis": + layer.setConnectionType(mapscript.MS_POSTGIS, "") + + conn_params = [] + + if db_conf["HOST"]: + conn_params.append("host=%s" % db_conf["HOST"]) + + if db_conf["PORT"]: + conn_params.append("port=%s" % db_conf["PORT"]) + + conn_params.append("dbname=%s" % db_conf["NAME"]) + + conn_params.append("user=%s" % db_conf["USER"]) + + if db_conf["PASSWORD"]: + conn_params.append("password=%s" % db_conf["PASSWORD"]) + + layer.connection = " ".join(conn_params) + + layer.data = "geometry from (%s) sq USING SRID=4326 USING UNIQUE oid" % self.getSubQuery(req) + + layer.addProcessing("CLOSE_CONNECTION=DEFER") + + elif db_conf["ENGINE"] == "django.contrib.gis.db.backends.spatialite": + layer.setConnectionType(mapscript.MS_OGR, "") + + layer.connection = db_conf["NAME"] + + layer.data = self.getSubQuery(req) + + def getMapServerLayer(self, req): + layer = super(EOWMSOutlinesLayer, self).getMapServerLayer(req) + + layer.setMetaData("wms_enable_request", "getcapabilities getmap getfeatureinfo") + + # NOTE: outline projection always set to WSG84 + srid = 4326 # TODO: Is that really correct? Check it. + layer.setProjection( crss.asProj4Str( srid ) ) + layer.setMetaData("ows_srs", crss.asShortCode( srid ) ) + layer.setMetaData("wms_srs", crss.asShortCode( srid ) ) + + layer.type = mapscript.MS_LAYER_POLYGON + + self.configureConnection(layer, req) + + # TODO: make this configurable + layer.header = os.path.join(settings.PROJECT_DIR, "conf", "outline_template_header.html") + layer.template = os.path.join(settings.PROJECT_DIR, "conf", "outline_template_dataset.html") + layer.footer = os.path.join(settings.PROJECT_DIR, "conf", "outline_template_footer.html") + + layer.setMetaData("gml_include_items", "all") + layer.setMetaData("wms_include_items", "all") + layer.dump = True + + #layer.tolerance = 10.0 + #layer.toleranceunits = mapscript.MS_PIXELS + + layer.offsite = mapscript.colorObj(0, 0, 0) + + for style_info in self.STYLES: + layer.insertClass(self.createOutlineClass(*style_info)) + + layer.classgroup = self.DEFAULT_STYLE + + return layer + +class EOWMSRectifiedStitchedMosaicOutlinesLayer(EOWMSOutlinesLayer): + def __init__(self, mosaic): + super(EOWMSRectifiedStitchedMosaicOutlinesLayer, self).__init__() + + self.mosaic = mosaic + + def getName(self): + return "%s_outlines" % self.mosaic.getCoverageId() + + def _get_base_query(self, time_clause=""): + return "SELECT eomd.id AS oid, eomd.footprint AS geometry, cov.coverage_id AS coverage_id FROM coverages_eometadatarecord AS eomd, coverages_coveragerecord AS cov, coverages_rectifieddatasetrecord AS rd, coverages_rectifiedstitchedmosaicrecord_rect_datasets AS rsm2rd WHERE rsm2rd.rectifiedstitchedmosaicrecord_id = %d AND rsm2rd.rectifieddatasetrecord_id = rd.coveragerecord_ptr_id AND cov.resource_ptr_id = rd.coveragerecord_ptr_id AND rd.eo_metadata_id = eomd.id%s" % (self.mosaic.getModel().pk, time_clause) + + def getMapServerLayer(self, req): + layer = super(EOWMSRectifiedStitchedMosaicOutlinesLayer, self).getMapServerLayer(req) + + layer.setMetaData("wms_extent", "%f %f %f %f" % self.mosaic.getWGS84Extent()) + + return layer + +class EOWMSDatasetSeriesOutlinesLayer(EOWMSOutlinesLayer): + def __init__(self, dataset_series): + super(EOWMSDatasetSeriesOutlinesLayer, self).__init__() + + self.dataset_series = dataset_series + + def getName(self): + return "%s_outlines" % self.dataset_series.getEOID() + + def _get_base_query(self, time_clause=""): + return "SELECT eomd.id AS oid, eomd.footprint AS geometry, cov.coverage_id AS coverage_id FROM coverages_eometadatarecord AS eomd, coverages_coveragerecord AS cov, coverages_rectifieddatasetrecord AS rectd, coverages_datasetseriesrecord_rect_datasets AS ds2rectd WHERE ds2rectd.datasetseriesrecord_id = %d AND ds2rectd.rectifieddatasetrecord_id = rectd.coveragerecord_ptr_id AND cov.resource_ptr_id = rectd.coveragerecord_ptr_id AND rectd.eo_metadata_id = eomd.id %s UNION SELECT eomd.id AS oid, eomd.footprint AS geometry, cov.coverage_id FROM coverages_eometadatarecord AS eomd, coverages_coveragerecord AS cov, coverages_referenceabledatasetrecord AS refd, coverages_datasetseriesrecord_ref_datasets AS ds2refd WHERE ds2refd.datasetseriesrecord_id = %d AND ds2refd.referenceabledatasetrecord_id = refd.coveragerecord_ptr_id AND cov.resource_ptr_id = refd.coveragerecord_ptr_id AND refd.eo_metadata_id = eomd.id%s" % (self.dataset_series.getModel().pk, time_clause, self.dataset_series.getModel().pk, time_clause) + + def getMapServerLayer(self, req): + layer = super(EOWMSDatasetSeriesOutlinesLayer, self).getMapServerLayer(req) + + layer.setMetaData("wms_extent", "%f %f %f %f" % self.dataset_series.getWGS84Extent()) + + return layer + + +class EOWMSCollectionOutlinesLayer(WMSLayer): + """ + """ + + STYLES = ( + ("red", 255, 0, 0), + ("green", 0, 128, 0), + ("blue", 0, 0, 255), + ("white", 255, 255, 255), + ("black", 0, 0, 0), + ("yellow", 255, 255, 0), + ("orange", 255, 165, 0), + ("magenta", 255, 0, 255), + ("cyan", 0, 255, 255), + ("brown", 165, 42, 42) + ) + + DEFAULT_STYLE = "red" + + def __init__(self, collection, filter_exprs=None): + super(EOWMSCollectionOutlinesLayer, self).__init__() + self.collection = collection + self.filter_exprs = filter_exprs + + def createOutlineClass(self, name, r, g, b): + outline_class = mapscript.classObj() + outline_style = mapscript.styleObj() + outline_style.outlinecolor = mapscript.colorObj(r, g, b) + outline_class.insertStyle(outline_style) + outline_class.group = name + + return outline_class + + + def getName(self): + return "%s_outlines" % self.collection.getEOID() + + def getMapServerLayer(self, req): + layer = super(EOWMSCollectionOutlinesLayer, self).getMapServerLayer(req) + layer.setMetaData("wms_extent", "%f %f %f %f" % self.collection.getWGS84Extent()) + layer.setMetaData("wms_enable_request", "getcapabilities getmap getfeatureinfo") + + # NOTE: outline projection always set to WSG84 + srid = 4326 # TODO: Is that really correct? Check it. + layer.setProjection( crss.asProj4Str( srid ) ) + layer.setMetaData("ows_srs", crss.asShortCode( srid ) ) + layer.setMetaData("wms_srs", crss.asShortCode( srid ) ) + + layer.type = mapscript.MS_LAYER_POLYGON + + # TODO: make this configurable + layer.header = os.path.join(settings.PROJECT_DIR, "conf", "outline_template_header.html") + layer.template = os.path.join(settings.PROJECT_DIR, "conf", "outline_template_dataset.html") + layer.footer = os.path.join(settings.PROJECT_DIR, "conf", "outline_template_footer.html") + + layer.setMetaData("gml_include_items", "all") + layer.setMetaData("wms_include_items", "all") + layer.dump = True + + #layer.tolerance = 10.0 + #layer.toleranceunits = mapscript.MS_PIXELS + + layer.offsite = mapscript.colorObj(0, 0, 0) + + for style_info in self.STYLES: + layer.insertClass(self.createOutlineClass(*style_info)) + + layer.classgroup = self.DEFAULT_STYLE + + # TODO: recursively add datasets for stitched mosaics + datasets = self.collection.getDatasets(self.filter_exprs) + + # add a shape object for each coverage in the collection + for dataset in datasets: + # TODO: maybe using the direct API is more efficient. requires performance test + wkt = dataset.getFootprint().wkt + shape = mapscript.shapeObj.fromWKT(wkt) + + # set the features values + shape.initValues(1) + shape.setValue(0, dataset.getCoverageId()) + + layer.addFeature(shape) + + if not len(datasets): + # add "null" shape + shape = mapscript.shapeObj(mapscript.MS_SHAPE_POLYGON) + line = mapscript.lineObj() + for x, y in [(2147483645, 2147483645), (2147483644, 2147483644), (2147483640, 2147483644), (2147483645, 2147483645)]: + line.add(mapscript.pointObj(x, y)) + shape.add(line) + shape.initValues(1) + shape.setValue(0, "none") + layer.addFeature(shape) + + # tell the layer what features are available + layer.addProcessing("ITEMS=coverage_id") + + return layer + + +class EOWMSBandsLayerMixIn(object): + def isRGB(self): + return False + + def isGrayscale(self): + return False + + def getName(self): + return "%s_bands" % self.coverage.getCoverageId() + + def _get_band_index(self, band_name): + c = 0 + for band in self.coverage.getRangeType().bands: + c += 1 + if normalize_band_name(band.name) == band_name: + return c + + raise InvalidRequestException( + "Unknown band name '%s'." % band_name, + "InvalidDimensionValue", + "dim_band" + ) + + def _get_band(self, band_name): + for band in self.coverage.getRangeType().bands: + if normalize_band_name(band.name) == band_name: + return band + + raise InvalidRequestException( + "Unknown band name '%s'." % band_name, + "InvalidDimensionValue", + "dim_band" + ) + + def getBandIndices(self, req): + band_list = req.getParamValue("dim_band") + + if not band_list or len(band_list) not in (1, 3): + raise InvalidRequestException( + "Exactly one or three band names need to be provided in DIM_BAND parameter.", + "InvalidDimensionValue", + "dim_band" + ) + + if len(band_list) == 1: + c = self._get_band_index(band_list[0]) + + return [c, c, c] + + elif len(band_list) == 3: + band_indices = [] + + for band_name in band_list: + band_indices.append(self._get_band_index(band_name)) + + return band_indices + + def getBandSelection(self, req): + band_list = req.getParamValue("dim_band") + + if not band_list or len(band_list) not in (1, 3): + raise InvalidRequestException( + "Exactly one or three band names need to be provided in DIM_BAND parameter.", + "InvalidDimensionValue", + "dim_band" + ) + + if len(band_list) == 1: + return [self._get_band(band_list[0])] + elif len(band_list) == 3: + bands = [] + + for band_name in band_list: + bands.append(self._get_band(band_name)) + + return bands + +class EOWMSRectifiedDatasetBandsLayer(EOWMSBandsLayerMixIn, WMSRectifiedDatasetLayer): + pass + +class EOWMSReferenceableDatasetBandsLayer(EOWMSBandsLayerMixIn, WMSReferenceableDatasetLayer): + pass + +class EOWMSRectifiedStitchedMosaicBandsLayer(EOWMSBandsLayerMixIn, WMSRectifiedStitchedMosaicLayer): + pass + +class WMS13VersionHandler(OWSCommonVersionHandler): + REGISTRY_CONF = { + "name": "WMS 1.3.0 Version Handler", + "impl_id": "services.ows.wms1x.WMS130VersionHandler", + "registry_values": { + "services.interfaces.service": "wms", + "services.interfaces.version": "1.3.0" + } + } + + SERVICE = "wms" + VERSION = "1.3.0" + + def _handleException(self, req, exception): + schemas = { + "http://www.opengis.net/ogc": "http://schemas.opengis.net/wms/1.3.0/exceptions_1_3_0.xsd" + } + return WMS13ExceptionHandler(schemas).handleException(req, exception) + +class WMS13GetCapabilitiesHandler(WMS1XGetCapabilitiesHandler): + REGISTRY_CONF = { + "name": "WMS 1.3 GetCapabilities Handler", + "impl_id": "services.ows.wms1x.WMS13GetCapabilitiesHandler", + "registry_values": { + "services.interfaces.service": "wms", + "services.interfaces.version": "1.3.0", + "services.interfaces.operation": "getcapabilities" + } + } + + def configureMapObj(self): + super(WMS13GetCapabilitiesHandler, self).configureMapObj() + + self.map.setMetaData("wms_enable_request", "getcapabilities getmap getfeatureinfo") + self.map.setMetaData("wms_feature_info_mime_type", "text/html") + + def createLayers(self): + visible_expr = System.getRegistry().getFromFactory( + "resources.coverages.filters.CoverageExpressionFactory", + {"op_name": "attr", "operands": ("visible", "=", True)} + ) + + cov_factory = System.getRegistry().bind("resources.coverages.wrappers.EOCoverageFactory") + + for coverage in cov_factory.find(filter_exprs=[visible_expr]): + layer = self.createCoverageLayer(coverage) + if coverage.getType() == "eo.rect_stitched_mosaic": + layer.setGroup("%s_group" % coverage.getCoverageId()) + self.addLayer(layer) + + outlines_layer = EOWMSRectifiedStitchedMosaicOutlinesLayer(coverage) + outlines_layer.setGroup("%s_group" % coverage.getCoverageId()) + self.addLayer(outlines_layer) + else: + self.addLayer(layer) + + dss_factory = System.getRegistry().bind("resources.coverages.wrappers.DatasetSeriesFactory") + + # TODO: find a more efficient way to do this check + for dataset_series in dss_factory.find(): + if len(dataset_series.getEOCoverages()) > 0: + logger.debug("Adding WMS Dataset Series Layer for Series '%s'." + % dataset_series.getEOID()) + + layer = WMSDatasetSeriesLayer(dataset_series) + layer.setGroup("%s_group" % dataset_series.getEOID()) + self.addLayer(layer) + + outlines_layer = EOWMSDatasetSeriesOutlinesLayer(dataset_series) + outlines_layer.setGroup("%s_group" % dataset_series.getEOID()) + self.addLayer(outlines_layer) + + def postprocess(self, resp): + # if the content cannot be parsed, return the response unchanged + try: + xml = minidom.parseString(resp.getContent()) + except: + return resp + + # Exception reports are not changed + if xml.documentElement.localName == "ExceptionReport": + return resp + + layer_els = xml.getElementsByTagNameNS( + "http://www.opengis.net/wms", "Layer" + ) + + encoder = EOWMSEncoder() + + # add _bands layers + # TODO: this solution really is not nice + for layer in self.layers: + if not isinstance(layer, EOWMSOutlinesLayer) and not isinstance(layer, WMSDatasetSeriesLayer): + layer_el = self._get_layer_element( + layer_els, layer.getName() + ) + + layer_el.appendChild(encoder.encodeBandsLayer( + "%s_bands" % layer.getName(), + layer.coverage.getRangeType() + )) + + return Response( + content = DOMtoXML(xml), + content_type = resp.getContentType(), + status = 200, + headers = resp.getHeaders() + ) + + def _get_layer_element(self, layer_els, layer_name): + for layer_el in layer_els: + for node in layer_el.childNodes: + if node.localName == "Name": + if node.firstChild.data == layer_name: + return layer_el + else: + break + + raise InternalError( + "Could not find 'Layer' element with name '%s' in WMS 1.3 GetCapabilities response" %\ + layer_name + ) + + +class WMS13GetMapHandler(WMS1XGetMapHandler): + REGISTRY_CONF = { + "name": "WMS 1.3 GetMap Handler", + "impl_id": "services.ows.wms1x.WMS13GetMapHandler", + "registry_values": { + "services.interfaces.service": "wms", + "services.interfaces.version": "1.3.0", + "services.interfaces.operation": "getmap" + } + } + + PARAM_SCHEMA = { + "service": {"xml_location": "/@service", "xml_type": "string", "kvp_key": "service", "kvp_type": "string"}, + "version": {"xml_location": "/@version", "xml_type": "string", "kvp_key": "version", "kvp_type": "string"}, + "operation": {"xml_location": "/", "xml_type": "localName", "kvp_key": "request", "kvp_type": "string"}, + "crs": {"xml_location": "/crs", "xml_type": "string", "kvp_key": "crs", "kvp_type": "string"}, # TODO: check XML location + "layers": {"xml_location": "/layer", "xml_type": "string[]", "kvp_key": "layers", "kvp_type": "stringlist"}, # TODO: check XML location + "format": {"xml_location": "/format", "xml_type": "string", "kvp_key": "format", "kvp_type": "string"}, + "time": {"xml_location": "/time", "xml_type": "string", "kvp_key": "time", "kvp_type": "string"}, + "bbox": {"xml_location": "/bbox", "xml_type": "floatlist", "kvp_key": "bbox", "kvp_type": "floatlist"}, + "dim_band": {"kvp_key": "dim_band", "kvp_type": "stringlist"} + } + + def getSRSParameterName(self): + return "crs" + + def getBoundedArea(self, srid, bbox): + if crss.hasSwappedAxes(srid): + return BoundedArea(srid, bbox[1], bbox[0], bbox[3], bbox[2]) + else: + return BoundedArea(srid, *bbox) + + def configureRequest(self): + + # check if the format is known; if not MapServer will raise an + # exception instead of returning the correct service exception report + # (bug) + if not self.req.getParamValue("format"): + raise InvalidRequestException( + "Missing mandatory 'format' parameter", + "MissingParameterValue", + "format" + ) + else: + format_name = self.req.getParamValue("format") + + try: + output_format = self.map.getOutputFormatByName(format_name) + + if not output_format: + raise InvalidRequestException( + "Unknown format name '%s'" % format_name, + "InvalidFormat", + "format" + ) + except Exception, e: + raise InvalidRequestException( + str(e), + "InvalidFormat", + "format" + ) + + super(WMS13GetMapHandler, self).configureRequest() + + def configureMapObj(self): + super(WMS13GetMapHandler, self).configureMapObj() + + self.map.setMetaData("wms_exceptions_format", "xml") + self.map.setMetaData("wms_enable_request", "getcapabilities getmap getfeatureinfo") + self.map.setMetaData("wms_feature_info_mime_type", "text/html") + + + def createLayersForName(self, layer_name, filter_exprs): + # TODO: dateline wrapped as in WMS1XGetMapHandler.createLayersForName + if layer_name.endswith("_outlines"): + self.createOutlinesLayer(layer_name[:-9]) + elif layer_name.endswith("_bands"): + self.createBandsLayers(layer_name[:-6], filter_exprs) + else: + super(WMS13GetMapHandler, self).createLayersForName( + layer_name, filter_exprs + ) + + def createOutlinesLayer(self, base_name): + filter_exprs = self.getFilterExpressions() + + dataset_series = System.getRegistry().getFromFactory( + "resources.coverages.wrappers.DatasetSeriesFactory", + {"obj_id": base_name} + ) + + if dataset_series is not None: + if len(dataset_series.getDatasets(filter_exprs)) > 0: + outlines_layer = EOWMSCollectionOutlinesLayer(dataset_series, filter_exprs) + + self.addLayer(outlines_layer) + else: + self.addLayer(WMSEmptyLayer("%s_outlines" % base_name)) + + else: + coverage = System.getRegistry().getFromFactory( + "resources.coverages.wrappers.EOCoverageFactory", + {"obj_id": base_name} + ) + if coverage is not None and coverage.getType() == "eo.rect_stitched_mosaic": + if len(coverage.getDatasets(filter_exprs)) > 0: + outlines_layer = EOWMSCollectionOutlinesLayer(coverage, filter_exprs) + + self.addLayer(outlines_layer) + else: + self.addLayer(WMSEmptyLayer("%s_outlines" % base_name)) + else: + raise InvalidRequestException( + "No coverage or dataset series with EO ID '%s' found" % base_name, + "LayerNotDefined", + "layers" + ) + + def createBandsLayers(self, base_name, filter_exprs): + dataset_series = System.getRegistry().getFromFactory( + "resources.coverages.wrappers.DatasetSeriesFactory", + {"obj_id": base_name} + ) + if dataset_series is not None: + self.createDatasetSeriesBandsLayers(dataset_series, filter_exprs) + else: + coverage = System.getRegistry().getFromFactory( + "resources.coverages.wrappers.EOCoverageFactory", + {"obj_id": base_name} + ) + if coverage is not None: + if coverage.matches(filter_exprs): + self.addLayer(self.createCoverageBandsLayer(coverage)) + else: + self.addLayer(WMSEmptyLayer("%s_bands" % base_name)) + else: + raise InvalidRequestException( + "No coverage or dataset series with EO ID '%s' found" % base_name, + "LayerNotDefined", + "layers" + ) + + def createCoverageBandsLayer(self, coverage): + if coverage.getType() == "plain": + raise InternalError( + "Plain coverages are not yet supported" + ) + elif coverage.getType() == "eo.rect_dataset": + return EOWMSRectifiedDatasetBandsLayer(coverage) + elif coverage.getType() == "eo.ref_dataset": + return EOWMSReferenceableDatasetBandsLayer(coverage) + elif coverage.getType() == "eo.rect_stitched_mosaic": + return EOWMSRectifiedStitchedMosaicBandsLayer(coverage) + + def createDatasetSeriesBandsLayers(self, dataset_series, filter_exprs): + def _get_begin_time(coverage): + return coverage.getBeginTime() + + coverages = dataset_series.getEOCoverages(filter_exprs) + + if len(coverages) == 0: + return # TODO: this will cause errors because of missing layers + + coverages.sort(key=_get_begin_time) + + for coverage in coverage: + layer = self.createCoverageBandsLayer(coverage) + + layer.setGroup("%s_bands" % dataset_series.getEOID()) + + self.addLayer(layer) + + def getMapServerLayer(self, layer): + ms_layer = super(WMS13GetMapHandler, self).getMapServerLayer(layer) + ms_layer.setMetaData("wms_exceptions_format","xml") + + return ms_layer + +# GetFeatureInfo is *not* GetMap, but the parameters are intersecting a lot +class WMS13GetFeatureInfoHandler(WMSCommonHandler): + REGISTRY_CONF = { + "name": "WMS 1.3 GetFeatureInfo Handler", + "impl_id": "services.ows.wms1x.WMS13GetFeatureInfoHandler", + "registry_values": { + "services.interfaces.service": "wms", + "services.interfaces.version": "1.3.0", + "services.interfaces.operation": "getfeatureinfo" + } + } + + PARAM_SCHEMA = { + "service": {"xml_location": "/@service", "xml_type": "string", "kvp_key": "service", "kvp_type": "string"}, + "version": {"xml_location": "/@version", "xml_type": "string", "kvp_key": "version", "kvp_type": "string"}, + "operation": {"xml_location": "/", "xml_type": "localName", "kvp_key": "request", "kvp_type": "string"}, + "crs": {"xml_location": "/crs", "xml_type": "string", "kvp_key": "crs", "kvp_type": "string"}, # TODO: check XML location + "query_layers": {"xml_location": "/query_layer", "xml_type": "string[]", "kvp_key": "query_layers", "kvp_type": "stringlist"}, # TODO: check XML location + "info_format": {"xml_location": "/info_format", "xml_type": "string", "kvp_key": "info_format", "kvp_type": "string"}, + "bbox": {"xml_location": "/bbox", "xml_type": "floatlist", "kvp_key": "bbox", "kvp_type": "floatlist"}, + "time": {"xml_location": "/time", "1xxml_type": "string", "kvp_key": "time", "kvp_type": "string"}, + "i": {"kvp_key": "i", "kvp_type": "int"}, + "j": {"kvp_key": "j", "kvp_type": "int"} + } + + def configureMapObj(self): + super(WMS13GetFeatureInfoHandler, self).configureMapObj() + + self.map.setMetaData("wms_exceptions_format", "xml") + self.map.setMetaData("wms_enable_request", "getcapabilities getmap getfeatureinfo") + self.map.setMetaData("wms_feature_info_mime_type", "text/html") + + def createLayers(self): + layer_names = self.req.getParamValue("query_layers") + + if layer_names is None: + raise InvalidRequestException( + "Missing 'QUERY_LAYERS' parameter", + "MissingParameterValue", + "layers" + ) + + filter_exprs = self.getFilterExpressions() + + for layer_name in layer_names: + self.createLayerForName(layer_name, filter_exprs) + + def createLayerForName(self, layer_name, filter_exprs): + if not layer_name.endswith("_outlines"): + raise InvalidRequestException( + "Cannot query layer '%s'" % layer_name, + "LayerNotDefined", + "query_layer" + ) + + base_name = layer_name[:-9] + + dataset_series = System.getRegistry().getFromFactory( + "resources.coverages.wrappers.DatasetSeriesFactory", + {"obj_id": base_name} + ) + if dataset_series is not None: + outlines_layer = EOWMSCollectionOutlinesLayer(dataset_series, filter_exprs) + + logger.debug("Found dataset series with ID %s"%base_name) + + self.addLayer(outlines_layer) + else: + coverage = System.getRegistry().getFromFactory( + "resources.coverages.wrappers.EOCoverageFactory", + {"obj_id": base_name} + ) + if coverage is not None and coverage.getType() == "eo.rect_stitched_mosaic": + outlines_layer = EOWMSRectifiedStitchedMosaicOutlinesLayer(coverage) + + self.addLayer(outlines_layer) + else: + raise InvalidRequestException( + "No coverage or dataset series with EO ID '%s' found" % base_name, + "LayerNotDefined", + "layers" + ) + + def getMapServerLayer(self, layer): + ms_layer = super(WMS13GetFeatureInfoHandler, self).getMapServerLayer(layer) + ms_layer.setMetaData("wms_exceptions_format","xml") + + return ms_layer + + + # TODO: copied from WMS13GetMapHandler. use common super class, or better, + # common parameter parser + def getBoundedArea(self, srid, bbox): + if crss.hasSwappedAxes(srid): + return BoundedArea(srid, bbox[1], bbox[0], bbox[3], bbox[2]) + else: + return BoundedArea(srid, *bbox) + + def getTimeFilterExpr(self, time_param): + timestamps = time_param.split("/") + + if len(timestamps) == 1: + try: + timestamp = getDateTime(timestamps[0]) + except InvalidParameterException: + raise InvalidRequestException( + "Invalid 'TIME' parameter format.", + "InvalidParameterValue", + "time" + ) + + return System.getRegistry().getFromFactory( + "resources.coverages.filters.CoverageExpressionFactory", + { + "op_name": "time_slice", + "operands": (timestamp,) + } + ) + + elif len(timestamps) == 2: + try: + time_intv = TimeInterval( + getDateTime(timestamps[0]), + getDateTime(timestamps[1]) + ) + except InvalidParameterException: + raise InvalidRequestException( + "Invalid 'TIME' parameter format.", + "InvalidParameterValue", + "time" + ) + + return System.getRegistry().getFromFactory( + "resources.coverages.filters.CoverageExpressionFactory", + { + "op_name": "time_intersects", + "operands": (time_intv,) + } + ) + else: + raise InvalidRequestException( + "Invalid 'TIME' parameter format.", + "InvalidParameterValue", + "time" + ) + + def getFilterExpressions(self): + try: + bbox = self.req.getParamValue("bbox") + except InvalidParameterException: + raise InvalidRequestException( + "Invalid BBOX parameter value", + "InvalidParameterValue", + "bbox" + ) + + if len(bbox) != 4: + raise InvalidRequestException( + "Wrong number of arguments for 'BBOX' parameter", + "InvalidParameterValue", + "bbox" + ) + + srid = self.getSRID() + + area = self.getBoundedArea(srid, bbox) + + filter_exprs = [] + + # TODO sqlite assert ahead `GEOSCoordSeq_setOrdinate_r` + filter_exprs.append( + System.getRegistry().getFromFactory( + "resources.coverages.filters.CoverageExpressionFactory", + { + "op_name": "footprint_intersects_area", + "operands": (area,) + } + ) + ) + + time_param = self.req.getParamValue("time") + + if time_param is not None: + filter_exprs.append(self.getTimeFilterExpr(time_param)) + + return filter_exprs + + + def getSRID(self): + srs = self.req.getParamValue(self.getSRSParameterName()) + + if srs is None: + raise InvalidRequestException("Missing '%s' parameter" + % self.getSRSParameterName().upper(), "MissingParameterValue", + self.getSRSParameterName()) + + srid = crss.parseEPSGCode(srs,(crss.fromURL,crss.fromURN, + crss.fromShortCode)) + + if srid is None: + raise InvalidRequestException("Invalid '%s' parameter value" + % self.getSRSParameterName().upper(), "InvalidCRS" , + self.getSRSParameterName()) + + return srid + + + def getSRSParameterName(self): + return "crs" + + +class WMS13GetLegendGraphicHandler(WMSCommonHandler): + REGISTRY_CONF = { + "name": "WMS 1.3 GetLegendGraphic Handler", + "impl_id": "services.ows.wms1x.WMS13GetLegendGraphicHandler", + "registry_values": { + "services.interfaces.service": "wms", + "services.interfaces.version": "1.3.0", + "services.interfaces.operation": "getlegendgraphic" + } + } + + # TODO: check XML locations + PARAM_SCHEMA = { + "service": {"xml_location": "/@service", "xml_type": "string", "kvp_key": "service", "kvp_type": "string"}, + "version": {"xml_location": "/@version", "xml_type": "string", "kvp_key": "version", "kvp_type": "string"}, + "operation": {"xml_location": "/", "xml_type": "localName", "kvp_key": "request", "kvp_type": "string"}, + "layer": {"xml_location": "/layer", "xml_type": "string", "kvp_key": "layer", "kvp_type": "string"}, + "format": {"xml_location": "/format", "xml_type": "string", "kvp_key": "format", "kvp_type": "string"}, + "width": {"kvp_key": "width", "kvp_type": "int"}, + "height": {"kvp_key": "height", "kvp_type": "int"}, + "style": {"kvp_key": "style", "kvp_type": "string"}, + # TODO SLD, SLD_BODY, SLD_VERSION, SCALE, STYLE, RULE + } + + def configureMapObj(self): + super(WMS13GetLegendGraphicHandler, self).configureMapObj() + + self.map.setMetaData("wms_exceptions_format", "xml") + self.map.setMetaData("wms_enable_request", "getcapabilities getmap getfeatureinfo,getlegendgraphic") + + def createLayers(self): + layer_name = self.req.getParamValue("layer") + + if layer_name is None: + raise InvalidRequestException( + "Missing 'LAYER' parameter", + "MissingParameterValue", + "layer" + ) + + obj = System.getRegistry().getFromFactory( + "resources.coverages.wrappers.DatasetSeriesFactory", + {"obj_id": layer_name} + ) + + if obj is None: + obj = System.getRegistry().getFromFactory( + "resources.coverages.wrappers.EOCoverageFactory", + {"obj_id": layer_name} + ) + + # TODO find layer + + if obj is None: + raise InvalidRequestException( + "No coverage or dataset series with EO ID '%s' found" % layer_name, + "LayerNotDefined", + "layer" + ) + + self.addLayer(WMSEmptyLayer(obj.getEOID())) + + #self.createLayersForName(layer_name, []) + + def createLayersForName(self, layer_name, filter_exprs): + dataset_series = System.getRegistry().getFromFactory( + "resources.coverages.wrappers.DatasetSeriesFactory", + {"obj_id": layer_name} + ) + if dataset_series is not None: + self.createDatasetSeriesLayers(dataset_series, filter_exprs) + else: + coverage = System.getRegistry().getFromFactory( + "resources.coverages.wrappers.EOCoverageFactory", + {"obj_id": layer_name} + ) + if coverage is not None: + if coverage.matches(filter_exprs): + self.addLayer(self.createCoverageLayer(coverage)) + else: + self.addLayer(WMSEmptyLayer(coverage.getCoverageId())) + else: + raise InvalidRequestException( + "No coverage or dataset series with EO ID '%s' found" % layer_name, + "LayerNotDefined", + "layer" + ) + + def createDatasetSeriesLayers(self, dataset_series, filter_exprs): + def _get_begin_time(coverage): + return coverage.getBeginTime() + + coverages = dataset_series.getEOCoverages(filter_exprs) + + if len(coverages) == 0: + layer = WMSEmptyLayer(dataset_series.getEOID()) + + self.addLayer(layer) + + coverages.sort(key=_get_begin_time) + + for coverage in coverages: + layer = self.createCoverageLayer(coverage) + + layer.setGroup(dataset_series.getEOID()) + + self.addLayer(layer) + + def getMapServerLayer(self, layer): + sld_enabled = System.getConfig().getConfigValue( + "services.ows.wms13", "enable_sld" + ) or "false" + style = self.req.getParamValue("style") + + if sld_enabled.lower() == "true" and style is not None: + sld_dir = System.getConfig().getConfigValue( + "services.ows.wms13", "sld_dir" + ) or "." + + pth = os.path.join(sld_dir, style + ".sld") + + if os.path.exists(pth): + with open(pth) as f: + layer.addSLD(f.read()) + + ms_layer = super(WMS13GetLegendGraphicHandler, self).getMapServerLayer(layer) + ms_layer.setMetaData("wms_exceptions_format","xml") + + return ms_layer + + +class WMS13ExceptionHandler(BaseExceptionHandler): + REGISTRY_CONF = { + "name": "OGC Namespace Exception Handler", + "impl_id": "services.ogc.WMS13ExceptionHandler", + "registry_values": { + "services.interfaces.exception_scheme": "ogc" + } + } + + def _filterExceptions(self, exception): + if not isinstance(exception, InvalidRequestException): + raise + + def _getEncoder(self): + return WMS13ExceptionEncoder(self.schemas) + + def _getContentType(self, exception): + return "application/vnd.ogc.se_xml" + +WMS13ExceptionHandlerImplementation = ExceptionHandlerInterface.implement(WMS13ExceptionHandler) + +class WMS13ExceptionEncoder(XMLEncoder): + REGISTRY_CONF = { + "name": "OGC Namespace Exception Report Encoder", + "impl_id": "services.ogc.WMS13ExceptionEncoder", + "registry_values": { + "services.interfaces.exception_scheme": "ogc" + } + } + + def _initializeNamespaces(self): + return { + "ogc": "http://www.opengis.net/ogc", + "xsi": "http://www.w3.org/2001/XMLSchema-instance" + } + + def encodeExceptionReport(self, exception_text, exception_code, locator=None): + if locator is None: + element = self._makeElement("ogc", "ServiceExceptionReport", [ + ("", "@version", "1.3.0"), + ("ogc", "ServiceException", [ + ("", "@code", exception_code), + ("", "@@", exception_text) + ]) + ]) + else: + element = self._makeElement("ogc", "ServiceExceptionReport", [ + ("", "@version", "1.3.0"), + ("ogc", "ServiceException", [ + ("", "@code", exception_code), + ("", "@locator", locator), + ("", "@@", exception_text) + ]) + ]) + + if self.schemas is not None: + schemas_location = " ".join(["%s %s"%(ns, location) for ns, location in self.schemas.iteritems()]) + element.setAttributeNS(self.ns_dict["xsi"], "%s:%s" % ("xsi", "schemaLocation"), schemas_location) + + return element + + def encodeInvalidRequestException(self, exception): + return self.encodeExceptionReport( + exception.msg, + exception.error_code, + exception.locator + ) + + def encodeVersionNegotiationException(self, exception): + return "" # TODO: check against OWS Common + + def encodeException(self, exception): + return self.encodeExceptionReport("Internal Server Error", "NoApplicableCode") + +WMS13ExceptionEncoderImplementation = ExceptionEncoderInterface.implement(WMS13ExceptionEncoder) + +class EOWMSEncoder(XMLEncoder): + def _initializeNamespaces(self): + ns_dict = super(EOWMSEncoder, self)._initializeNamespaces() + + ns_dict.update({ + "wms": "http://www.opengis.net/wms" + }) + + return ns_dict + + def encodeBandsLayer(self, layer_name, range_type): + band_name_list = ",".join( + [normalize_band_name(band.name) for band in range_type.bands] + ) + + return self._makeElement("wms", "Layer", [ + ("wms", "Name", layer_name), + ("wms", "Title", layer_name), + ("wms", "Dimension", [ + ("", "@name", "band"), + ("", "@units", ""), + ("", "@multipleValues", "1"), + ("", "@@", band_name_list) + ]) + ]) + +def normalize_band_name(band_name): + return band_name.replace(" ", "_") diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wms1x.py eoxserver-0.3.2/eoxserver/services/ows/wms1x.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wms1x.py 1970-01-01 00:00:00.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wms1x.py 2013-12-10 14:57:00.000000000 +0000 @@ -0,0 +1,156 @@ +#------------------------------------------------------------------------------- +# $Id$ +# +# Project: EOxServer +# Authors: Stephan Krause +# Stephan Meissl +# +#------------------------------------------------------------------------------- +# Copyright (C) 2011 EOX IT Services GmbH +# +# 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 of this Software or works derived from this 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. +#------------------------------------------------------------------------------- + +from eoxserver.services.interfaces import ( + ServiceHandlerInterface, VersionHandlerInterface, + OperationHandlerInterface +) +from eoxserver.services.owscommon import OWSCommonServiceHandler + +from eoxserver.services.ows.wms.common import WMS1XGetCapabilitiesHandler +from eoxserver.services.ows.wms.wms1011 import ( + WMS10VersionHandler, + WMS110VersionHandler, + WMS111VersionHandler, + WMS10_11GetMapHandler +) +from eoxserver.services.ows.wms.wms13 import ( + WMS13VersionHandler, + WMS13GetCapabilitiesHandler, + WMS13GetMapHandler, + WMS13GetFeatureInfoHandler, + WMS13GetLegendGraphicHandler +) + +class WMSServiceHandler(OWSCommonServiceHandler): + REGISTRY_CONF = { + "name": "WMS Service Handler", + "impl_id": "services.ows.wms1x.WMSServiceHandler", + "registry_values": { + "services.interfaces.service": "wms" + } + } + + SERVICE = "wms" + +WMSServiceHandlerImplementation = ServiceHandlerInterface.implement(WMSServiceHandler) + +WMS10VersionHandlerImplementation = VersionHandlerInterface.implement(WMS10VersionHandler) + +WMS110VersionHandlerImplementation = VersionHandlerInterface.implement(WMS110VersionHandler) + +WMS111VersionHandlerImplementation = VersionHandlerInterface.implement(WMS111VersionHandler) + +WMS13VersionHandlerImplementation = VersionHandlerInterface.implement(WMS13VersionHandler) + +class WMS10GetCapabilitiesHandler(WMS1XGetCapabilitiesHandler): + REGISTRY_CONF = { + "name": "WMS 1.0 GetCapabilities Handler", + "impl_id": "services.ows.wms1x.WMS10GetCapabilitiesHandler", + "registry_values": { + "services.interfaces.service": "wms", + "services.interfaces.version": "1.0.0", + "services.interfaces.operation": "getcapabilities" + } + } + +WMS10GetCapabilitiesHandlerImplementation = OperationHandlerInterface.implement(WMS10GetCapabilitiesHandler) + +class WMS110GetCapabilitiesHandler(WMS1XGetCapabilitiesHandler): + REGISTRY_CONF = { + "name": "WMS 1.1.0 GetCapabilities Handler", + "impl_id": "services.ows.wms1x.WMS110GetCapabilitiesHandler", + "registry_values": { + "services.interfaces.service": "wms", + "services.interfaces.version": "1.1.0", + "services.interfaces.operation": "getcapabilities" + } + } + +WMS110GetCapabilitiesHandlerImplementation = OperationHandlerInterface.implement(WMS110GetCapabilitiesHandler) + +class WMS111GetCapabilitiesHandler(WMS1XGetCapabilitiesHandler): + REGISTRY_CONF = { + "name": "WMS 1.1.1 GetCapabilities Handler", + "impl_id": "services.ows.wms1x.WMS111GetCapabilitiesHandler", + "registry_values": { + "services.interfaces.service": "wms", + "services.interfaces.version": "1.1.1", + "services.interfaces.operation": "getcapabilities" + } + } + +WMS111GetCapabilitiesHandlerImplementation = OperationHandlerInterface.implement(WMS111GetCapabilitiesHandler) + +WMS13GetCapabilitiesHandlerImplementation = OperationHandlerInterface.implement(WMS13GetCapabilitiesHandler) + +class WMS10GetMapHandler(WMS10_11GetMapHandler): + REGISTRY_CONF = { + "name": "WMS 1.0 GetMap Handler", + "impl_id": "services.ows.wms1x.WMS10GetMapHandler", + "registry_values": { + "services.interfaces.service": "wms", + "services.interfaces.version": "1.0.0", + "services.interfaces.operation": "getmap" + } + } + +WMS10GetMapHandlerImplementation = OperationHandlerInterface.implement(WMS10GetMapHandler) + +class WMS110GetMapHandler(WMS10_11GetMapHandler): + REGISTRY_CONF = { + "name": "WMS 1.1.0 GetMap Handler", + "impl_id": "services.ows.wms1x.WMS110GetMapHandler", + "registry_values": { + "services.interfaces.service": "wms", + "services.interfaces.version": "1.1.0", + "services.interfaces.operation": "getmap" + } + } + +WMS110GetMapHandlerImplementation = OperationHandlerInterface.implement(WMS110GetMapHandler) + +class WMS111GetMapHandler(WMS10_11GetMapHandler): + REGISTRY_CONF = { + "name": "WMS 1.1.1 GetMap Handler", + "impl_id": "services.ows.wms1x.WMS111GetMapHandler", + "registry_values": { + "services.interfaces.service": "wms", + "services.interfaces.version": "1.1.1", + "services.interfaces.operation": "getmap" + } + } + +WMS111GetMapHandlerImplementation = OperationHandlerInterface.implement(WMS111GetMapHandler) + +WMS13GetMapHandlerImplementation = OperationHandlerInterface.implement(WMS13GetMapHandler) + +WMS13GetFeatureInfoHandlerImplementation = OperationHandlerInterface.implement(WMS13GetFeatureInfoHandler) + +WMS13GetLegendGraphicHandlerImplementation = OperationHandlerInterface.implement(WMS13GetLegendGraphicHandler) diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wps/exceptions.py eoxserver-0.3.2/eoxserver/services/ows/wps/exceptions.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wps/exceptions.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wps/exceptions.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,120 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# Martin Paces -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013-2014 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -class OWS10Exception(Exception): - """ Base OWS 1.0 exception of the WPS 1.0.0 exceptionss """ - def __init__(self, code, locator, message): - self.code = code - self.locator = locator - Exception.__init__(self, message) - -#------------------------------------------------------------------------------- -# All possible WPS 1.0.0 exceptions. For list of OWS exception used by WPS -# see OGC 05-007r7 Table 38 and Table 62 - -class NoApplicableCode(OWS10Exception): - def __init__(self, message, locator=None): - OWS10Exception.__init__(self, "NoApplicableCode", locator, message) - -class MissingParameterValue(OWS10Exception): - def __init__(self, message, locator): - OWS10Exception.__init__(self, "MissingParameterValue", locator, message) - -class InvalidParameterValue(OWS10Exception): - def __init__(self, message, locator): - OWS10Exception.__init__(self, "InvalidParameterValue", locator, message) - -class NotEnoughStorage(OWS10Exception): - def __init__(self, message): - OWS10Exception.__init__(self, "NotEnoughStorage", None, message) - -class ServerBusy(OWS10Exception): - def __init__(self, message): - OWS10Exception.__init__(self, "ServerBusy", None, message) - -class FileSizeExceeded(OWS10Exception): - def __init__(self, message, locator): - OWS10Exception.__init__(self, "FileSizeExceeded", locator, message) - -class StorageNotSupported(OWS10Exception): - def __init__(self, message): - OWS10Exception.__init__(self, "StorageNotSupported", None, message) - -class VersionNegotiationFailed(OWS10Exception): - def __init__(self, message, locator): - OWS10Exception.__init__(self, "VersionNegotiationFailed", locator, message) - -#------------------------------------------------------------------------------- -# Derived specific exceptions. -# -# Note that WPS 1.0.0 allows use of "vendor specific exception code" as locator -# for the default "NoApplicableCode" exceptions. - -class NoSuchProcessError(InvalidParameterValue): - def __init__(self, identifier): - msg = "No such process: %s" % identifier - InvalidParameterValue.__init__(self, msg, "process identifier") - -class InvalidOutputError(InvalidParameterValue): - def __init__(self, output_id): - message = "Invalid output '%s'!"%(output_id) - InvalidParameterValue.__init__(self, message, output_id) - -class InvalidOutputValueError(NoApplicableCode): - def __init__(self, output_id, message=""): - message = "Invalid output value of '%s'! %s"%(output_id, message) - NoApplicableCode.__init__(self, message, output_id) - -class InvalidOutputDefError(InvalidParameterValue): - def __init__(self, output_id, message=""): - message = "Invalid output definition of '%s'! %s"%(output_id, message) - InvalidParameterValue.__init__(self, message, output_id) - -class InvalidInputError(InvalidParameterValue): - def __init__(self, input_id): - message = "Invalid input '%s'!"%(input_id) - InvalidParameterValue.__init__(self, message, input_id) - -class InvalidInputValueError(InvalidParameterValue): - def __init__(self, input_id, message=""): - message = "Invalid input value of '%s'! %s"%(input_id, message) - InvalidParameterValue.__init__(self, message, input_id) - -class InvalidInputReferenceError(InvalidParameterValue): - def __init__(self, input_id, message=""): - message = "Invalid input '%s' reference! %s"%(input_id, message) - InvalidParameterValue.__init__(self, message, input_id) - -class MissingRequiredInputError(InvalidParameterValue): - def __init__(self, input_id): - message = "Missing required input '%s'!"%(input_id) - InvalidParameterValue.__init__(self, message, input_id) - -class ExecuteError(NoApplicableCode): - def __init__(self, message="", locator="process.execute()"): - NoApplicableCode.__init__(self, message, locator) diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wps/interfaces.py eoxserver-0.3.2/eoxserver/services/ows/wps/interfaces.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wps/interfaces.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wps/interfaces.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,126 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -class AsyncBackendInterface(object): - """ Interface class for asynchronous WPS backends. - NOTE: Only one async. backend at time is allowed to be configured. - """ - - @property - def supported_versions(self): - """ A list of versions of the WPS standard supported by the backend. - """ - - def enqueue(self, identifier, raw_inputs, response_document, version): - """ Enqueue the WPS request for asynchronous processing. - - Once enqued the process shall be managed exclusively by the async. - backend. - - The request is defined by the process's ``identifier``, - ``raw_inputs`` (before the decoding and resolution - of the references), and the ``response_document`` (holding - the outputs' parameters). The ``version`` of the WPS standard - to be used. The method returns a URL (string) of the asynchronous - execute response. - """ - - -class ProcessInterface(object): - """ Interface class for processes offered, described and executed by - the WPS. - """ - - @property - def version(self): - """ The version of the process, if applicable. Optional. - When omitted it defaults to '1.0.0'. - """ - - @property - def identifier(self): - """ An identifier (URI) of the process. Optional. - When omitted it defaults to the process' class-name. - """ - - @property - def title(self): - """ A human-readable title of the process. Optional. When omitted it - defaults to the process identifier. - """ - - @property - def description(self): - """ A human-readable detailed description of the process. Optional. - (Content of the the abstract in the WPS process description.) - """ - - @property - def profiles(self): - """ A iterable of URNs of WPS application profiles this process - adheres to. Optional. - """ - - @property - def metadata(self): - """ A dict of title/URL meta-data pairs associated with the process. - Optional. - """ - - @property - def wsdl(self): - """ A URL of WSDL document describing this process. Optional. - """ - - @property - def inputs(self): - """ A dict mapping the inputs' identifiers to their respective types. - The type can be either one of the supported native python types - (automatically converted to a ``LiteralData`` object) or an instance - of one of the data-specification classes (``LiteralData``, - ``BoundingBoxData``, or ``ComplexData``). Mandatory. - """ - - @property - def outputs(self): - """ A dict mapping the outputs' identifiers to their respective types. - The type can be either one of the supported native python types - (automatically converted to a ``LiterData`` object) or an instance - of one of the data-specification classes (``LiterData``, - ``BoundingBoxData``, or ``ComplexData``). Mandatory. - """ - - def execute(self, **kwargs): - """ The main execution function for the process. The ``kwargs`` are the - parsed input inputs (using the keys as defined by the ``inputs``) - and the Complex Data format requests (using the keys as defined by - the ``outputs``). - The method is expected to return a dictionary of the output values - (using the keys as defined by the ``outputs``). In case of only - one output item defined by the ``outputs``, one output value - is allowed to be returned directly. - """ diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wps/parameters/allowed_values.py eoxserver-0.3.2/eoxserver/services/ows/wps/parameters/allowed_values.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wps/parameters/allowed_values.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wps/parameters/allowed_values.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,259 +0,0 @@ -#------------------------------------------------------------------------------- -# -# WPS Literal Data - allowed values -# -# Project: EOxServer -# Authors: Martin Paces -# -#------------------------------------------------------------------------------- -# Copyright (C) 2014 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -from itertools import chain -from .data_types import BaseType, Double, DTYPES - -class TypedMixIn(object): - """ adding type to a allowed value range """ - - def __init__(self, dtype): - if issubclass(dtype, BaseType): - self._dtype = dtype - elif dtype in DTYPES: - self._dtype = DTYPES[dtype] - else: - raise TypeError("Non-supported data type %s!" % dtype) - - @property - def dtype(self): - return self._dtype - - -class BaseAllowed(object): - """ allowed values base class """ - - def check(self, value): - """ check validity """ - raise NotImplementedError - - def verify(self, value): - """ Verify the value.""" - raise NotImplementedError - - -class AllowedAny(BaseAllowed): - """ dummy allowed values class """ - - # TODO: NaN check - def check(self, value): - return True - - def verify(self, value): - return value - - -class AllowedByReference(BaseAllowed): - """ class of allowed values defined by a reference """ - - def __init__(self, url): - self._url = url - - @property - def url(self): - return self._url - - # TODO: implement proper checking - def check(self, value): - return True - - def verify(self, value): - return value - - -class AllowedEnum(BaseAllowed, TypedMixIn): - """ enumerated set of allowed values class """ - - def __init__(self, values, dtype=Double): - TypedMixIn.__init__(self, dtype) - self._values = set(self._dtype.parse(v) for v in values) - - @property - def values(self): - return self._values - - def check(self, value): - return self._dtype.parse(value) in self._values - - def verify(self, value): - if self.check(value): - return value - raise ValueError("The value is not in the set of the allowed values.") - - -class AllowedRange(BaseAllowed, TypedMixIn): - """ range of allowed values class """ - - ALLOWED_CLOSURES = ['closed', 'open', 'open-closed', 'closed-open'] - - # NOTE: Use of spacing with float discrete range is not recommended. - def __init__(self, minval, maxval, closure='closed', - spacing=None, spacing_rtol=1e-9, dtype=Double): - """ Range constructor. - - parameters: - minval range lower bound - set to None if unbound - maxval range upper bound - set to None if unbound - closure *'closed'|'open'|'open-closed'|'closed-open' - spacing uniform spacing of discretely sampled ranges - spacing_rtol relative tolerance of the spacing match - """ - TypedMixIn.__init__(self, dtype) - - if not self._dtype.comparable: - raise ValueError("Non-supported range data type '%s'!"%self._dtype) - - if closure not in self.ALLOWED_CLOSURES: - raise ValueError("Invalid closure specification!") - - if minval is None and maxval is None: - raise ValueError("Invalid range bounds!") - - if spacing_rtol < 0.0 or spacing_rtol > 1.0: - raise ValueError("Invalid spacing relative tolerance!") - - self._closure = self.ALLOWED_CLOSURES.index(closure) - self._minval = None if minval is None else self._dtype.parse(minval) - self._maxval = None if maxval is None else self._dtype.parse(maxval) - self._base = self._maxval if self._minval is None else self._minval - - # verify the spacing - if spacing is not None: - ddtype = self._dtype.get_diff_dtype() - - # check wehther the type has difference operation defined - if ddtype is None or ddtype.zero is None: - raise TypeError( - "Spacing is not applicable for type '%s'!" % dtype - ) - spacing = ddtype.parse(spacing) - if spacing <= ddtype.zero: - raise ValueError("Invalid spacing '%s'!" % spacing) - - self._spacing = spacing - self._rtol = spacing_rtol - - @property - def minval(self): - return self._minval - - @property - def maxval(self): - return self._maxval - - @property - def spacing(self): - return self._spacing - - @property - def closure(self): - return self.ALLOWED_CLOSURES[self._closure] - - def _out_of_spacing(self, value): - if self._spacing is None: - return False - ddtype = self._dtype.get_diff_dtype() - tmp0 = ddtype.as_number(self._dtype.sub(value, self._base)) - tmp1 = ddtype.as_number(self.spacing) - tmp2 = float(tmp0) / float(tmp1) - return not self._rtol >= abs(tmp2 - round(tmp2)) - - def _out_of_bounds(self, value): - if value != value: # cheap type-safe NaN check (sucks for Python<=2.5) - return True - below = self._minval is not None and (value < self._minval or - (value == self._minval and self._closure in (1, 2))) - above = self._maxval is not None and (value > self._maxval or - (value == self._maxval and self._closure in (1, 3))) - return below or above - - def check(self, value): - value = self._dtype.parse(value) - return not (self._out_of_bounds(value) or self._out_of_spacing(value)) - - def verify(self, value): - parsed_value = self._dtype.parse(value) - if self._out_of_bounds(parsed_value): - raise ValueError("The value is not within the allowed range.") - if self._out_of_spacing(parsed_value): - raise ValueError("The value does not fit the requested spacing.") - return value - - -class AllowedRangeCollection(BaseAllowed, TypedMixIn): - """ allowed values resctriction class combined of multiple - AllowedEnum and AllowedRange objects. - """ - - def __init__(self, *objs): - if not objs: - raise ValueError("At least one AllowedEnum or AllowedRange object" - " must be provided!") - TypedMixIn.__init__(self, objs[0].dtype) - - values = set() - ranges = [] - - for obj in objs: - # Collect enumarated values to a one set. - if isinstance(obj, AllowedEnum): - values.update(obj.values) - # Collect ranges. - elif isinstance(obj, AllowedRange): - ranges.append(obj) - else: - raise ValueError("An object which is neither AllowedEnum" - " nor AllowedRange instance! OBJ=%r"%obj) - - # Check that all ranges and value sets are of the same type. - if self.dtype != obj.dtype: - raise TypeError("Data type mismatch!") - - self._enum = AllowedEnum(values, dtype=self.dtype) - self._ranges = ranges - - @property - def enum(self): - return self._enum - - @property - def ranges(self): - return self._ranges - - def check(self, value): - for obj in chain([self._enum], self._ranges): - if obj.check(value): - return True - return False - - def verify(self, value): - if self.check(value): - return value - raise ValueError("The value does not match the range of the allowed" - " values!") diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wps/parameters/base.py eoxserver-0.3.2/eoxserver/services/ows/wps/parameters/base.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wps/parameters/base.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wps/parameters/base.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,81 +0,0 @@ -#------------------------------------------------------------------------------- -# -# WPS input and output parameters' base class -# -# Project: EOxServer -# Authors: Fabian Schindler -# Martin Paces -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -# NOTE: Currently, the inputs parameters are not allowed to be present -# more that once (maxOccurs=1) per request. These input parameters -# are, by default, mandatory (minOccur=1). Unpon explicit requests -# the parameters can be made optional (minOccur=0). -# -# Although not explicitely mentioned by the WPS 1.0.0 standard -# it is a common practice that the outputs do not appear more than -# once per output (maxOccurs=1). When the exlicit specification -# of the outputs is omitted in the request all process output are -# contained in the default respose. - -class BaseParamMetadata(object): - """ Common metadata base of all parameter classes.""" - - def __init__(self, identifier, title=None, abstract=None): - self.identifier = identifier - self.title = title - self.abstract = abstract - - -class ParamMetadata(BaseParamMetadata): - """ Common metadata of the execute request parameters.""" - - def __init__(self, identifier, title=None, abstract=None, uom=None, - crs=None, mime_type=None, encoding=None, schema=None): - super(ParamMetadata, self).__init__(identifier, title, abstract) - self.uom = uom - self.crs = crs - self.mime_type = mime_type - self.encoding = encoding - self.schema = schema - - -class Parameter(BaseParamMetadata): - """ Base parameter class used by the process definition.""" - - def __init__(self, identifier=None, title=None, abstract=None, - metadata=None, optional=False): - """ Object constructor. - - Parameters: - identifier idetnfier of the parameter. - title optional human-readable name (defaults to idetfier). - abstract optional human-readable verbose description. - metadata optional metadata (title/URL dictionary). - optional optional boolean flag indicating whether the input - parameter is optional or not. - """ - super(Parameter, self).__init__(identifier, title, abstract) - self.metadata = metadata or {} - self.is_optional = optional diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wps/parameters/bboxdata.py eoxserver-0.3.2/eoxserver/services/ows/wps/parameters/bboxdata.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wps/parameters/bboxdata.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wps/parameters/bboxdata.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,205 +0,0 @@ -#------------------------------------------------------------------------------- -# -# WPS Bounding-Box Data type -# -# Project: EOxServer -# Authors: Fabian Schindler -# Martin Paces -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -import re -from itertools import chain - -from eoxserver.core.util.rect import Rect -from .data_types import Double -from .crs import CRSType -from .base import Parameter - - -# precompiled reg.ex. used to eliminate repeated white-spaces -_RE_MULTIWS = re.compile(r"\s+") - -#------------------------------------------------------------------------------- - -class BoundingBox(tuple): - """ Bounding Box representation. """ - - def __new__(cls, bbox, crs=None): - if isinstance(bbox, Rect): - lower, upper = bbox.offset, bbox.upper - else: - lower, upper = bbox - lower = tuple(float(v) for v in lower) - upper = tuple(float(v) for v in upper) - if len(lower) != len(upper): - raise ValueError("Dimension mismatch! Both the lower and uppper " - "corners must have the same dimension!") - return tuple.__new__(cls, (lower, upper)) - - def __init__(self, bbox, crs=None): - """ bounding box constructor - - Parameters: - bbox n-dimensional bounding box definition: - ((xmin,),(xmax,)) - ((xmin,ymin),(xmax,ymax)) - ((xmin,ymin,zmin),(xmax,ymax,zmax)) - or instance of the ``Rect`` class. - crs optional crs identifier (URI) - """ - tuple.__init__(self) - self._crs = crs if crs is not None else getattr(bbox, "crs", None) - - @property - def crs(self): - return self._crs - - @property - def lower(self): - return self[0] - - @property - def upper(self): - return self[1] - - @property - def dimension(self): - return len(self[0]) - - @property - def as_rect(self): - """Cast to a Rect object.""" - if self.dimension != 2: - raise RuntimeError("Only 2D bounding-box can be cast to " - "a rectangle object!") - return Rect(self[0][0], self[0][1], None, None, self[1][0], self[1][1]) - - def __str__(self): - crs = ", crs=%s" % (self.crs if self.crs is not None else "") - return "BoundingBox((%s, %s)%s)" % (self.lower, self.upper, crs) - -#------------------------------------------------------------------------------- - -class BoundingBoxData(Parameter): - """ bunding box parameter class """ - dtype = Double - dtype_crs = CRSType - - def __init__(self, identifier, crss=None, dimension=2, default=None, - *args, **kwargs): - """ Object constructor. - - Parameters: - identifier identifier of the parameter. - title optional human-raedable name (defaults to identifier). - abstract optional human-redable verbose description. - metadata optional metadata (title/URL dictionary). - optional optional boolean flag indicating whether the input - parameter is optional or not. - default optional default input value. Presence of the - default value sets the parameter optional. - crss list of accepted CRSs (Coordinate Reference Systems). - The CRSs shall be given in form of the integer EPSG - codes. Defaults to WGS84 (EPSG:4326). - dimension optional dimension of the bounding box coordinates. - Defaults to 2. - """ - super(BoundingBoxData, self).__init__(identifier, *args, **kwargs) - self.dimension = int(dimension) - self.crss = tuple(self.parse_crs(crs) for crs in crss or (4326,)) - self.default = self.parse(default) if default is not None else None - if self.dimension < 1: - raise ValueError("Invalid bounding box dimension %s!" % dimension) - - @property - def default_crs(self): - return self.crss[0] - - def _encode(self, bbox): - """ Common low-level encoding method.""" - if self.dimension != bbox.dimension: - raise ValueError("Invalid dimension %s of the encoded bounding " - "box!"%bbox.dimension) - crs = bbox.crs if bbox.crs is not None else self.default_crs - if crs not in self.crss: - raise ValueError("Invalid crs %s of the encoded bounding box!"%crs) - - return ( - (self.dtype.encode(v) for v in bbox.lower), - (self.dtype.encode(v) for v in bbox.upper), - (self.encode_crs(crs),) - ) - - def encode_kvp(self, bbox): - """ Encode KVP bounding box.""" - return ",".join(chain(*self._encode(bbox))) - - def encode_xml(self, bbox): - """ Encode XML bounding box.""" - lower, upper, crs = self._encode(bbox) - return (" ".join(lower), " ".join(upper), crs[0]) - - def parse(self, raw_bbox): - if isinstance(raw_bbox, BoundingBox): - bbox = BoundingBox((raw_bbox.lower, raw_bbox.upper), - raw_bbox.crs if raw_bbox.crs is not None else self.default_crs) - elif isinstance(raw_bbox, basestring): - items = raw_bbox.split(',') - dim = len(items)/2 - lower = [self.dtype.parse(item) for item in items[0:dim]] - upper = [self.dtype.parse(item) for item in items[dim:2*dim]] - if len(items) > 2*dim: - crs = self.parse_crs(items[2*dim]) - else: - crs = self.default_crs - bbox = BoundingBox((lower, upper), crs) - else: # assuming XML decoded tuple - lower = _RE_MULTIWS.sub(",", raw_bbox[0].strip()).split(",") - upper = _RE_MULTIWS.sub(",", raw_bbox[1].strip()).split(",") - if len(lower) != len(upper): - raise ValueError("Dimension mismatch of the bounding box's" - " corner coordinates! %d != %d"%(len(lower), len(upper))) - lower = [self.dtype.parse(item) for item in lower] - upper = [self.dtype.parse(item) for item in upper] - if raw_bbox[2] is not None: - crs = self.parse_crs(raw_bbox[2]) - else: - crs = self.default_crs - bbox = BoundingBox((lower, upper), crs) - if bbox.dimension != self.dimension: - raise ValueError("Invalid dimenstion %d of the parsed bounding" - " box!"%(bbox.dimension)) - if bbox.crs not in self.crss: - raise ValueError("Invalid CRS %r of the parsed bounding" - " box!"%(bbox.crs)) - return bbox - - @classmethod - def parse_crs(cls, raw_crs): - return cls.dtype_crs.parse(raw_crs) - - @classmethod - def encode_crs(cls, crs): - return cls.dtype_crs.encode(crs) - diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wps/parameters/codecs.py eoxserver-0.3.2/eoxserver/services/ows/wps/parameters/codecs.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wps/parameters/codecs.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wps/parameters/codecs.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,90 +0,0 @@ -#------------------------------------------------------------------------------- -# -# WPS Complex Data codecs (encoders/decoders) -# -# Project: EOxServer -# Authors: Martin Paces -# -#------------------------------------------------------------------------------- -# Copyright (C) 2014 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -import base64 - - -class Codec(object): - """ Base complex data codec.""" - encoding = None - - @staticmethod - def encode(file_in, **opt): - """ Encoding generator.""" - raise NotImplementedError - - @staticmethod - def decode(file_in, **opt): - """ Encoding generator.""" - raise NotImplementedError - - -class CodecBase64(Codec): - """ BASE64 codec """ - encoding = 'base64' - - @staticmethod - def encode(file_in, **opt): - """ Encoding generator.""" - if opt.get('urlsafe', False): - _encode = base64.urlsafe_b64encode - else: - _encode = base64.standard_b64encode - dlm = "" - for data in iter(lambda: file_in.read(57), ''): - yield dlm - yield _encode(data) - dlm = "\r\n" - - @staticmethod - def decode(file_in, **opt): - """ Decoding generator.""" - if opt.get('urlsafe', False): - _decode = base64.urlsafe_b64decode - else: - _decode = base64.standard_b64decode - for data in file_in: - yield _decode(data) - - -class CodecRaw(Codec): - """ Data encoder class.""" - encoding = None - - @staticmethod - def encode(file_in, **opt): - """ Encoding generator.""" - for data in iter(lambda: file_in.read(65536), ''): - yield data - - @staticmethod - def decode(file_in, **opt): - """ Decoding generator.""" - for data in iter(lambda: file_in.read(65536), ''): - yield data diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wps/parameters/complexdata.py eoxserver-0.3.2/eoxserver/services/ows/wps/parameters/complexdata.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wps/parameters/complexdata.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wps/parameters/complexdata.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,367 +0,0 @@ -#------------------------------------------------------------------------------- -# -# WPS Complex Data type -# -# Project: EOxServer -# Authors: Fabian Schindler -# Martin Paces -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -import os -import os.path -import json -from lxml import etree -from copy import deepcopy -from StringIO import StringIO - -try: - from cStringIO import StringIO as FastStringIO -except ImportError: - FastStringIO = StringIO - -try: - # available in Python 2.7+ - from collections import OrderedDict -except ImportError: - from django.utils.datastructures import SortedDict as OrderedDict - -from .base import Parameter -from .formats import Format - -#------------------------------------------------------------------------------- -# complex data - data containers - -class CDBase(object): - """ Base class of the complex data container. """ - def __init__(self, mime_type=None, encoding=None, schema=None, format=None): - if isinstance(format, Format): - self.mime_type = format.mime_type - self.encoding = format.encoding - self.schema = format.schema - else: - self.mime_type = mime_type - self.encoding = encoding - self.schema = schema - - -class CDObject(CDBase): - """ Complex data wraper arround an arbitraty python object. - - To be used to set custom format attributes for the XML - and JSON payload. - - NOTE: CDObject is not used for the input JSON and XML. - """ - def __init__(self, data, mime_type=None, encoding=None, schema=None, - format=None): - CDBase.__init__(self, mime_type, encoding, schema, format) - self.data = data - -class CDByteBuffer(StringIO, CDBase): - """ Complex data binary in-memory buffer (StringIO). - - To be used to hold a generic binary (byte-stream) payload. - """ - def __init__(self, data='', mime_type=None, encoding=None, schema=None, - format=None): - StringIO.__init__(self, str(data)) - CDBase.__init__(self, mime_type, encoding, schema, format) - - def write(self, data): - StringIO.write(self, str(data)) - - @property - def data(self): - self.seek(0) - return self.read() - - -class CDTextBuffer(StringIO, CDBase): - """ Complex data text (unicode) in-memory buffer (StringIO). - - To be used to hold generic text. The the text payload - is stored as a unicode-stream. - - Set the 'text_encoding' parameter is unicode encoding/decoding - shall be applied. - """ - def __init__(self, data=u'', mime_type=None, encoding=None, schema=None, - format=None, text_encoding=None): - StringIO.__init__(self, unicode(data)) - CDBase.__init__(self, mime_type, encoding, schema, format) - self.text_encoding = text_encoding - - @property - def data(self): - self.seek(0) - return self.read() - - def write(self, data): - if self.text_encoding is None: - return StringIO.write(self, unicode(data)) - else: - return StringIO.write(self, unicode(data, self.text_encoding)) - - def read(self, size=None): - if size is None: - data = StringIO.read(self) - else: - data = StringIO.read(self, size) - if self.text_encoding is None: - return data - else: - return data.encode(self.text_encoding) - - -class CDAsciiTextBuffer(CDByteBuffer): - """ Complex data text (ascii) in-memory buffer (StringIO). - - To be used to hold generic ascii text. The the text payload - is stored as a byte-stream and this class cannot hold - characters outside of the 7-bit ascii characters' range. - """ - def __init__(self, data='', mime_type=None, encoding=None, schema=None, - format=None, text_encoding=None): - CDByteBuffer.__init__(self, data, mime_type, encoding, schema, format) - self.text_encoding = text_encoding - - def write(self, data): - if not isinstance(data, basestring): - data = str(data) - StringIO.write(self, data.encode('ascii')) - - def read(self, size=None): - if size is None: - data = StringIO.read(self) - else: - data = StringIO.read(self, size) - if self.text_encoding is None: - return data - else: - return data.encode(self.text_encoding) - - -class CDFile(CDBase): - """ Complex data binary file. - - To be used to hold a generic binary (byte-stream) payload. - - NOTE: The file allows you to specify whether the file is - temporary (will be atomatically removed - by default) - or permanent (preserverved after object destruction). - """ - - def __init__(self, name, mode='r', buffering=-1, - mime_type=None, encoding=None, schema=None, format=None, - remove_file=True): - CDBase.__init__(self, mime_type, encoding, schema, format) - self._file = file(name, mode, buffering) - self._remove_file = remove_file - - def __del__(self): - name = self.name - self.close() - if self._remove_file: - os.remove(name) - - @property - def data(self): - self.seek(0) - return self.read() - - def __getattr__(self, attr): - return getattr(self._file, attr) - - -class CDPermanentFile(CDFile): - """ Complex data permanent binary file. - - To be used to hold a generic binary (byte-stream) payload. - - NOTE: This class preserves the actual file. - """ - - def __init__(self, remove_file, name, mode='r', buffering=-1, - mime_type=None, encoding=None, schema=None, format=None): - CDFile.__init__(name, mode, buffering, mime_type, encoding, schema, - format, False) - -#------------------------------------------------------------------------------- - -class ComplexData(Parameter): - """ complex-data parameter class """ - - def __init__(self, identifier, formats, *args, **kwargs): - """ Object constructor. - - Parameters: - identifier identifier of the parameter. - title optional human-readable name (defaults to identifier). - abstract optional human-readable verbose description. - metadata optional metadata (title/URL dictionary). - optional optional boolean flag indicating whether the input - parameter is optional or not. - formats List of supported formats. - """ - super(ComplexData, self).__init__(identifier, *args, **kwargs) - self.formats = OrderedDict() - if isinstance(formats, Format): - formats = (formats,) - for frm in formats: - self.formats[(frm.mime_type, frm.encoding, frm.schema)] = frm - - @property - def default_format(self): - return self.formats.itervalues().next() - - def get_format(self, mime_type, encoding=None, schema=None): - if mime_type is None: - return self.default_format - else: - return self.formats.get((mime_type, encoding, schema)) - -# def verify_format(self, format): -# """ Returns valid format or rise value error exception.""" -# if format is None: -# return self.default_format -# tmp = (format.mime_type, format.encoding, format.schema) -# if tmp in self.formats: -# return format -# raise ValueError("Invalid format %r"%format) - - def parse(self, data, mime_type, schema, encoding, **opt): - """ parse input complex data """ - format_ = self.get_format(mime_type, encoding, schema) - if format_ is None: - raise ValueError("Invalid format specification! mime_type=%r, " - "encoding=%r, schema=%r"%(mime_type, encoding, schema)) - text_encoding = getattr(format_, 'text_encoding', 'utf-8') - fattr = { - 'mime_type': format_.mime_type, - 'encoding': format_.encoding, - 'schema': format_.schema - } - if format_.is_xml: - parsed_data = CDObject(etree.fromstring(data), **fattr) - elif format_.is_json: - parsed_data = CDObject(json.loads(_unicode(data, text_encoding)), **fattr) - elif format_.is_text: - parsed_data = CDTextBuffer(_unicode(data, text_encoding), **fattr) - parsed_data.seek(0) - else: # generic binary byte-stream - parsed_data = CDByteBuffer(data, **fattr) - if format_.encoding is not None: - data_out = FastStringIO() - for chunk in format_.decode(parsed_data, **opt): - data_out.write(chunk) - parsed_data = data_out - parsed_data.seek(0) - return parsed_data - - def encode_xml(self, data): - """ encode complex data to be embedded to an XML document""" - mime_type = getattr(data, 'mime_type', None) - encoding = getattr(data, 'encoding', None) - schema = getattr(data, 'schema', None) - format_ = self.get_format(mime_type, encoding, schema) - if format_ is None: - raise ValueError("Invalid format specification! mime_type=%r, " - "encoding=%r, schema=%r"%(mime_type, encoding, schema)) - if not format_.allows_xml_embedding: - raise ValueError("Selected format does not allows XML embedding! " - "mime_type=%r, encoding=%r, schema=%r"%( - mime_type, encoding, schema)) - if isinstance(data, CDObject): - data = data.data - if format_.is_xml: - if isinstance(data, etree._ElementTree): - data = data.getroot() - return deepcopy(data) - elif format_.is_json: - return json.dumps(data, ensure_ascii=False) - elif format_.is_text: - if not isinstance(data, basestring): - data.seek(0) - data = data.read() - return data - else: # generic binary byte-stream - if format_.encoding is not None: - data.seek(0) - data_out = FastStringIO() - for chunk in format_.encode(data): - data_out.write(chunk) - data = data_out - data.seek(0) - return data.read() - - def encode_raw(self, data): - """ encode complex data for raw output """ - def _rewind(fid): - if hasattr(fid, 'seek'): - fid.seek(0) - return data - mime_type = getattr(data, 'mime_type', None) - encoding = getattr(data, 'encoding', None) - schema = getattr(data, 'schema', None) - format_ = self.get_format(mime_type, encoding, schema) - text_encoding = getattr(format_, 'text_encoding', 'utf-8') - if format_ is None: - raise ValueError("Invalid format specification! mime_type=%r, " - "encoding=%r, schema=%r"%(mime_type, encoding, schema)) - if isinstance(data, CDObject): - data = data.data - if format_.is_xml: - data = FastStringIO(etree.tostring(data, pretty_print=False, - xml_declaration=True, encoding=text_encoding)) - content_type = "%s; charset=%s"%(format_.mime_type, text_encoding) - elif format_.is_json: - data = FastStringIO(json.dumps(data, ensure_ascii=False).encode(text_encoding)) - content_type = "%s; charset=%s"%(format_.mime_type, text_encoding) - elif format_.is_text: - if isinstance(data, (CDTextBuffer, CDAsciiTextBuffer)): - data.text_encoding = text_encoding - else: - data = FastStringIO(_rewind(data).read().encode(text_encoding)) - content_type = "%s; charset=%s"%(format_.mime_type, text_encoding) - else: # generic binary byte-stream - if format_.encoding is not None: - data_out = FastStringIO() - for chunk in format_.encode(_rewind(data)): - data_out.write(chunk) - data = data_out - content_type = format_.mime_type - return _rewind(data), content_type - - -def _bytestring(data): - if isinstance(data, str): - return data - raise TypeError("Byte string expected, %s received!"%type(data)) - -def _unicode(data, encoding): - if isinstance(data, unicode): - return data - elif isinstance(data, str): - return unicode(data, encoding) - raise TypeError("Byte od unicode string expected, %s received!"%type(data)) diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wps/parameters/crs.py eoxserver-0.3.2/eoxserver/services/ows/wps/parameters/crs.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wps/parameters/crs.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wps/parameters/crs.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,71 +0,0 @@ -#------------------------------------------------------------------------------- -# -# CRS data type. -# -# Project: EOxServer -# Authors: Martin Paces -# -#------------------------------------------------------------------------------- -# Copyright (C) 2014 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -from .data_types import BaseType - -from eoxserver.resources.coverages.crss import ( - asURL, fromURL, fromURN, fromShortCode, validateEPSGCode, parseEPSGCode, -) - -class CRSType(BaseType): - """ CRS data-type. - CRS are preresented by the EPSG codes + 0 meaning the ImageCRC. - """ - name = "anyURI" - dtype = int - zero = None - comparable = False - - @classmethod - def parse(cls, raw_value): - """ Cast or parse input to its proper represenation.""" - if isinstance(raw_value, cls.dtype): - if raw_value == 0 or validateEPSGCode(raw_value): - return raw_value - else: - if raw_value == "ImageCRS": - return 0 - else: - value = parseEPSGCode(raw_value, (fromURL, fromURN, fromShortCode)) - if value is not None: - return value - raise ValueError("Invalid CRS %r!" % raw_value) - - @classmethod - def encode(cls, value): - """ Encode value to a unicode string.""" - if value == 0: - return u'ImageCRS' - elif validateEPSGCode(value): - return unicode(asURL(value)) - raise ValueError("Invalid CRS %r!" % value) - - @classmethod - def get_diff_dtype(cls): # string has no difference - return None diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wps/parameters/data_types.py eoxserver-0.3.2/eoxserver/services/ows/wps/parameters/data_types.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wps/parameters/data_types.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wps/parameters/data_types.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,324 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Library of primitive data type classes. -# -# Project: EOxServer -# Authors: Martin Paces -# -#------------------------------------------------------------------------------- -# Copyright (C) 2014 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -from datetime import datetime, date, time, timedelta -from django.utils.dateparse import parse_date, parse_datetime, parse_time, utc -from django.utils.tzinfo import FixedOffset - -from eoxserver.core.util.timetools import isoformat, parse_duration - - -class BaseType(object): - """ Base literal data type class. - This class defines the class interface. - """ - name = None # to be replaced by a name - dtype = str # to be replaced by a proper type - zero = None # when applicable to be replaced by a proper zero value - comparable = True # indicate whether the type can be compared (<,>,==) - - @classmethod - def parse(cls, raw_value): - """ Cast or parse input to its proper represenation.""" - return cls.dtype(raw_value) - - @classmethod - def encode(cls, value): - """ Encode value to a unicode string.""" - return unicode(value) - - @classmethod - def get_diff_dtype(cls): # difference type - change if differs from the base - """ Get type of the differece of this type. - E.g., `timedelta` for a `datetime`. - """ - return cls - - @classmethod - def as_number(cls, value): - """ convert to a number (e.g., duration)""" - raise TypeError("Data type %s cannot be converted to a number!" % cls) - - @classmethod - def sub(cls, value0, value1): - """ substract value0 - value1 """ - raise TypeError("Data type %s cannot be substracted!" % cls) - - -class Boolean(BaseType): - name = "boolean" - dtype = bool - - @classmethod - def parse(cls, raw_value): - if isinstance(raw_value, basestring): - raw_value = unicode(raw_value.lower()) - if raw_value in ('1', 'true'): - return True - elif raw_value in ('0', 'false'): - return False - else: - raise ValueError("Cannot parse boolean value '%s'!"%raw_value) - else: - return bool(raw_value) - - @classmethod - def encode(cls, value): - return u'true' if value else u'false' - - @classmethod - def as_number(cls, value): - return int(value) - - @classmethod - def sub(cls, value0, value1): - """ substract value0 - value1 """ - return value0 - value1 - - -class Integer(BaseType): - name = "integer" - dtype = int - zero = 0 - - @classmethod - def encode(cls, value): - """ Encode value to a unicode string.""" - return unicode(int(value)) - - @classmethod - def as_number(cls, value): - return value - - @classmethod - def sub(cls, value0, value1): - """ substract value0 - value1 """ - return value0 - value1 - - -class Double(BaseType): - name = "double" - dtype = float - zero = 0.0 - - @classmethod - def encode(cls, value): - return u"%.15g"%cls.dtype(value) - - @classmethod - def as_number(cls, value): - return value - - @classmethod - def sub(cls, value0, value1): - """ substract value0 - value1 """ - return value0 - value1 - - -class String(BaseType): - name = "string" - dtype = unicode - encoding = 'utf-8' - comparable = False # disabled although Python implements comparable strings - - @classmethod - def encode(cls, value): - """ Encode value to a unicode string.""" - try: - return unicode(value) - except UnicodeDecodeError: - return unicode(value, cls.encoding) - - @classmethod - def parse(cls, raw_value): - return cls.dtype(raw_value) - - @classmethod - def get_diff_dtype(cls): # string has no difference - return None - - -class Duration(BaseType): - name = "duration" - dtype = timedelta - zero = timedelta(0) - - @classmethod - def parse(cls, raw_value): - if isinstance(raw_value, cls.dtype): - return raw_value - return parse_duration(raw_value) - - @classmethod - def encode(cls, value): - # NOTE: USE OF MONTH AND YEAR IS AMBIGUOUS! WE DO NOT ENCODE THEM! - if not isinstance(value, cls.dtype): - raise ValueError("Invalid value type '%s'!"%type(value)) - items = [] - if value.days < 0: - items.append('-') - value = -value - items.append('P') - if value.days != 0: - items.append('%dD'%value.days) - elif value.seconds == 0 and value.microseconds == 0: - items.append('T0S') # zero interaval - if value.seconds != 0 or value.microseconds != 0: - minutes, seconds = divmod(value.seconds, 60) - hours, minutes = divmod(minutes, 60) - items.append('T') - if hours != 0: - items.append('%dH'%hours) - if minutes != 0: - items.append('%dM'%minutes) - if value.microseconds != 0: - items.append("%.6fS"%(seconds+1e-6*value.microseconds)) - elif seconds != 0: - items.append('%dS'%seconds) - - return unicode("".join(items)) - - @classmethod - def as_number(cls, value): - return 86400.0*value.days + 1.0*value.seconds + 1e-6*value.microseconds - - @classmethod - def sub(cls, value0, value1): - """ substract value0 - value1 """ - return value0 - value1 - - -class Date(BaseType): - name = "date" - dtype = date - - @classmethod - def get_diff_dtype(cls): - return Duration - - @classmethod - def parse(cls, raw_value): - if isinstance(raw_value, cls.dtype): - return raw_value - value = parse_date(raw_value) - if value: - return value - raise ValueError("Could not parse ISO date from '%s'."%raw_value) - - @classmethod - def encode(cls, value): - if isinstance(value, cls.dtype): - return unicode(value.isoformat()) - raise ValueError("Invalid value type '%s'!"%type(value)) - - @classmethod - def sub(cls, value0, value1): - """ substract value0 - value1 """ - return value0 - value1 - - -class Time(BaseType): - name = "time" - dtype = time - # TODO: proper time-zone handling - - @classmethod - def get_diff_dtype(cls): - return Duration - - @classmethod - def parse(cls, raw_value): - if isinstance(raw_value, cls.dtype): - return raw_value - value = parse_time(raw_value) - if value is not None: - return value - raise ValueError("Could not parse ISO time from '%s'."%raw_value) - - @classmethod - def encode(cls, value): - if isinstance(value, cls.dtype): - return unicode(value.isoformat()) - raise ValueError("Invalid value type '%s'!"%type(value)) - - @classmethod - def sub(cls, value0, value1): - """ substract value0 - value1 """ - aux_date = datetime.now().date() - dt0 = datetime.combine(aux_date, value0) - dt1 = datetime.combine(aux_date, value1) - return dt0 - dt1 - - -class DateTime(BaseType): - name = "dateTime" - dtype = datetime - - # tzinfo helpers - UTC = utc # zulu-time TZ instance - TZOffset = FixedOffset # fixed TZ offset class, set mintues to instantiate - - @classmethod - def get_diff_dtype(cls): - return Duration - - @classmethod - def parse(cls, raw_value): - if isinstance(raw_value, cls.dtype): - return raw_value - value = parse_datetime(raw_value) - if value: - return value - raise ValueError("Could not parse ISO date-time from '%s'."%raw_value) - - @classmethod - def encode(cls, value): - if isinstance(value, cls.dtype): - return unicode(isoformat(value)) - raise ValueError("Invalid value type '%s'!"%type(value)) - - @classmethod - def sub(cls, value0, value1): - """ substract value0 - value1 """ - return value0 - value1 - - -# mapping of plain Python types to data type classes -DTYPES = { - str: String, - unicode: String, - bool: Boolean, - int: Integer, - long: Integer, - float: Double, - date: Date, - datetime: DateTime, - time: Time, - timedelta: Duration, -} diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wps/parameters/formats.py eoxserver-0.3.2/eoxserver/services/ows/wps/parameters/formats.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wps/parameters/formats.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wps/parameters/formats.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,106 +0,0 @@ -#------------------------------------------------------------------------------- -# -# WPS Complex Data formats -# -# Project: EOxServer -# Authors: Fabian Schindler -# Martin Paces -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -from .codecs import CodecBase64, CodecRaw - - -class Format(object): - """ Base complex data format. """ - - # boolean flag indicating whether the format allows the payload to be - # embedded to XML response or not. The XML embedding is disabled by default. - allows_xml_embedding = False - - def __init__(self, encoder, mime_type, schema=None, - is_text=False, is_xml=False, is_json=False): - """ Object constructor. - - Parameters: - encoder format's encoder object (defines the encoding) - mime_type mime-type of the format - schema optional schema of the document - is_text optioonal boolean flag indicating text-based data - format. - is_xml optional boolean flag indicatind XML-based format. - The flag enables is_text flag. - is_json optional boolean flag indicating JSON-bases format. - The flag enables is_text flag. - """ - if is_xml or is_json: - is_text = True - self.mime_type = mime_type - self.schema = schema - self.is_text = is_text - self.is_xml = is_xml - self.is_json = is_json - self._codec = encoder - - @property - def encoding(self): - return self._codec.encoding - - def encode(self, file_in, **opt): - """ Encoding generator.""" - return self._codec.encode(file_in, **opt) - - def decode(self, file_in, **opt): - """ Encoding generator.""" - return self._codec.decode(file_in, **opt) - -#------------------------------------------------------------------------------- - -class FormatText(Format): - allows_xml_embedding = True - def __init__(self, mime_type="text/plain", schema=None, text_encoding='utf-8'): - Format.__init__(self, CodecRaw, mime_type, schema, True, False, False) - self.text_encoding = text_encoding - -class FormatXML(Format): - allows_xml_embedding = True - def __init__(self, mime_type="application/xml", schema=None, text_encoding='utf-8'): - Format.__init__(self, CodecRaw, mime_type, schema, True, True, False) - self.text_encoding = text_encoding - -class FormatJSON(Format): - allows_xml_embedding = True - def __init__(self, mime_type="application/json", schema=None, text_encoding='utf-8'): - Format.__init__(self, CodecRaw, mime_type, schema, True, False, True) - self.text_encoding = text_encoding - -class FormatBinaryRaw(Format): - allows_xml_embedding = False - def __init__(self, mime_type="application/octet-stream"): - Format.__init__(self, CodecRaw, mime_type, None, False, False, False) - -class FormatBinaryBase64(Format): - allows_xml_embedding = True - def __init__(self, mime_type="application/octet-stream"): - Format.__init__(self, CodecBase64, mime_type, None, False, False, False) - diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wps/parameters/__init__.py eoxserver-0.3.2/eoxserver/services/ows/wps/parameters/__init__.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wps/parameters/__init__.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wps/parameters/__init__.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,61 +0,0 @@ -#------------------------------------------------------------------------------- -# -# WPS input and output parameters and data types -# -# Project: EOxServer -# Authors: Martin Paces -# -#------------------------------------------------------------------------------- -# Copyright (C) 2014 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -from .base import Parameter -from .literaldata import LiteralData -from .complexdata import ( - ComplexData, CDBase, CDObject, CDTextBuffer, CDByteBuffer, - CDAsciiTextBuffer, CDFile, CDPermanentFile, -) -from .formats import ( - Format, FormatText, FormatXML, FormatJSON, - FormatBinaryRaw, FormatBinaryBase64, -) -from .codecs import Codec, CodecBase64, CodecRaw -from .bboxdata import BoundingBox, BoundingBoxData -from .units import UnitOfMeasure, UnitLinear -from .allowed_values import ( - BaseAllowed, AllowedAny, AllowedEnum, AllowedRange, - AllowedRangeCollection, AllowedByReference -) -from .data_types import ( - DTYPES, BaseType, Boolean, Integer, Double, String, - Duration, Date, Time, DateTime -) -from .crs import CRSType -from .inputs import InputReference, InputData -from .response_form import ( - Output, ResponseForm, ResponseDocument, RawDataOutput -) - -def fix_parameter(name, prm): - """ Expand short-hand definition of the parameter.""" - if isinstance(prm, Parameter): - return prm - return LiteralData(name, dtype=prm) diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wps/parameters/inputs.py eoxserver-0.3.2/eoxserver/services/ows/wps/parameters/inputs.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wps/parameters/inputs.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wps/parameters/inputs.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,57 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Input objects used by the execute requests and responses -# -# Project: EOxServer -# Authors: Fabian Schindler -# Martin Paces -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -from .base import ParamMetadata - -class InputReference(ParamMetadata): - """ Input data reference.""" - - def __init__(self, href, identifier, title=None, abstract=None, - headers=None, body=None, method=None, mime_type=None, - encoding=None, schema=None, body_href=None): - ParamMetadata.__init__(self, identifier, title, abstract, None, None, - mime_type, encoding, schema) - self.href = href - self.headers = headers or () - self.body = body - self.body_href = body_href - self.method = method - - -class InputData(ParamMetadata): - """ Raw input data.""" - def __init__(self, identifier, title=None, abstract=None, - data=None, uom=None, crs=None, mime_type=None, - encoding=None, schema=None, asurl=False): - ParamMetadata.__init__(self, identifier, title, abstract, uom, crs, - mime_type, encoding, schema) - self.data = data - self.asurl = asurl # set to True if data are passed as HTTP/GET URL - diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wps/parameters/literaldata.py eoxserver-0.3.2/eoxserver/services/ows/wps/parameters/literaldata.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wps/parameters/literaldata.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wps/parameters/literaldata.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,190 +0,0 @@ -#------------------------------------------------------------------------------- -# -# WPS Literal Data type -# -# Project: EOxServer -# Authors: Fabian Schindler -# Martin Paces -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -try: - # available in Python 2.7+ - from collections import OrderedDict -except ImportError: - from django.utils.datastructures import SortedDict as OrderedDict - -from .base import Parameter -from .data_types import BaseType, String, DTYPES -from .allowed_values import BaseAllowed, AllowedAny, AllowedEnum -from .units import UnitOfMeasure, UnitLinear - - -class LiteralData(Parameter): - """ literal-data parameter class """ - - def __init__(self, identifier, dtype=String, uoms=None, default=None, - allowed_values=None, *args, **kwargs): - """ Object constructor. - - Parameters: - identifier identifier of the parameter. - title optional human-raedable name (defaults to identifier). - abstract optional human-redable verbose description. - metadata optional metadata (title/URL dictionary). - optional optional boolean flag indicating whether the input - parameter is optional or not. - dtype optional data type of the parameter. String type - ``str`` is set by default. For list of supported - types see ``LiteralData.SUPPORTED_TYPES``). - uoms optional sequence of the supported units. - default optional default input value. Presence of the - default value sets the parameter optional. - allowed_values optional restriction on the accepted values. - By default any value of the given type is - supported. The allowed value can be specified by an - an enumerated list (iterable) of values or by - instance of one of the following classes: - ``AllowedAny``, ``AllowedEnum``, ``AllowedRange``, - or ``AllowedByReference``. - """ - super(LiteralData, self).__init__(identifier, *args, **kwargs) - - if issubclass(dtype, BaseType): - self._dtype = dtype - elif dtype in DTYPES: - self._dtype = DTYPES[dtype] - else: - raise TypeError("Non-supported data type %s! "%dtype) - - if isinstance(allowed_values, BaseAllowed): - if (hasattr(allowed_values, 'dtype') - and self._dtype != allowed_values.dtype): - raise TypeError("The allowed values vs. literal data type" - " mismatch! %s != %s", allowed_values.dtype, self._dtype) - self._allowed_values = allowed_values - elif allowed_values is not None: - self._allowed_values = AllowedEnum(allowed_values, self._dtype) - else: - self._allowed_values = AllowedAny() - - if uoms: # the first uom is the default one - tmp = OrderedDict() - for uom in uoms: - if not isinstance(uom, UnitOfMeasure): - uom = UnitLinear(uom[0], uom[1]) - tmp[uom.name] = uom - self._uoms = tmp - else: - self._uoms = None - - if default is None: - self.default = None - else: - self.default = self.parse(default) - self.is_optional = True - - @property - def default_uom(self): - return self._uoms.keys()[0] if self._uoms else None - - @property - def uoms(self): - return self._uoms.keys() if self._uoms else None - - @property - def dtype(self): - """Data type class of the literal data object. (RO)""" - return self._dtype - - @property - def allowed_values(self): - """Allowed values object of the literal data object. (RO)""" - return self._allowed_values - - def check(self, value): - """Check whether the value is allowed (True) or not (False).""" - return self._allowed_values.check(value) - - def verify(self, value): - """Return the value if allowed or raise the ValueError exception.""" - return self._allowed_values.verify(value) - - def apply_uom(self, value, uom): - if uom is None: - return value - try: - return self._uoms[uom].apply(value) - except KeyError: - raise ValueError("Invalid UOM '%s'!"%uom) - - def strip_uom(self, value, uom): - if uom is None: - return value - try: - return self._uoms[uom].strip(value) - except KeyError: - raise ValueError("Invalid UOM '%s'!"%uom) - - def encode(self, value, uom=None, encoding=None): - """ Encode the output value to its string representation. - - The value is checked to match the defined allowed values - restriction and the UOM conversion is applied. - - Returns unicode or byte-string if the encoding is given. - """ - try: - _value = self._allowed_values.verify(value) - _value = self.apply_uom(_value, uom) - _value = self._dtype.encode(_value) - return _value.encode(encoding) if encoding else _value - except (ValueError, TypeError) as exc: - raise ValueError("Output encoding error: '%s' (value '%s')" - "" % (str(exc), value)) - - def parse(self, raw_value, uom=None, encoding="utf-8"): - """ Parse the input value from its string representation. - - The value is checked to match the defined allowed values - restriction and the UOM conversion is applied. - - Non-unicode raw_data are converted to unicode before parsing. - Byte strings are decoded using the profided encoding (utf8 by - default). - """ - try: - if isinstance(raw_value, unicode): - _value = raw_value - elif isinstance(raw_value, str): - _value = unicode(raw_value, encoding) - else: - _value = unicode(raw_value) - _value = self._dtype.parse(raw_value) - _value = self.strip_uom(_value, uom or self.default_uom) - _value = self._allowed_values.verify(_value) - return _value - except (ValueError, TypeError) as exc: - raise ValueError( - "Input parsing error: '%s' (raw value '%s')" % (exc, raw_value) - ) diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wps/parameters/response_form.py eoxserver-0.3.2/eoxserver/services/ows/wps/parameters/response_form.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wps/parameters/response_form.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wps/parameters/response_form.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,88 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Response form objects. -# -# Project: EOxServer -# Authors: Fabian Schindler -# Martin Paces -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -try: - # available in Python 2.7+ - from collections import OrderedDict -except ImportError: - from django.utils.datastructures import SortedDict as OrderedDict - -from .base import ParamMetadata - -#------------------------------------------------------------------------------- - -class Output(ParamMetadata): - """ Output request.""" - - def __init__(self, identifier, title=None, abstract=None, uom=None, - crs=None, mime_type=None, encoding=None, schema=None, - as_reference=False): - ParamMetadata.__init__(self, identifier, title, abstract, uom, crs, - mime_type, encoding, schema) - self.as_reference = as_reference - - -class ResponseForm(OrderedDict): - """ Response form defined as an ordered dict. of the output definitions.""" - - def __init__(self): - super(ResponseForm, self).__init__() - - def set_output(self, output): - """ Set (insert) a new definition output.""" - self[output.identifier] = output - - def get_output(self, identifier): - """ Get an output for the given output identifier. - An instance of the Output object is always returned. - """ - output = self.get(identifier, None) - if not output: - output = Output(identifier) - return output - - -class ResponseDocument(ResponseForm): - """ Object representation of the response document.""" - raw = False - - def __init__(self, lineage=False, status=False, store_response=False): - super(ResponseDocument, self).__init__() - self.lineage = lineage - self.status = status - self.store_response = store_response - - -class RawDataOutput(ResponseForm): - raw = True - - def __init__(self, output): - super(RawDataOutput, self).__init__() - self.set_output(output) diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wps/parameters/units.py eoxserver-0.3.2/eoxserver/services/ows/wps/parameters/units.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wps/parameters/units.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wps/parameters/units.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,74 +0,0 @@ -#------------------------------------------------------------------------------- -# -# WPS Literal Data - units of measure -# -# Project: EOxServer -# Authors: Martin Paces -# -#------------------------------------------------------------------------------- -# Copyright (C) 2014 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -class UnitOfMeasure(object): - """ Base unit class. """ - - def __init__(self, name): - self.name = name - - def apply(self, value): - """ Convert value from the common base to this uinit.""" - raise NotImplementedError - - def strip(self, value): - """ Convert value of this unit to the common base.""" - raise NotImplementedError - - -class UnitLinear(UnitOfMeasure): - """ Simple linear scale + offset unit of measure. - - value_uom = (value_base - offset)/scale - value_base = value_uom*scale + offset - - Set scale linear conversion from the base uinit E.g., - for 'F' UOM and base unit in 'K' set scale to 5.0/9.0 and - offset to 459.67*5.0/9.0 . - - In case of simple scale factor set scale to multiple of the base unit - and offset to zero E.g., for 'km' UOM base unit in 'm' set scale - factor to 1000.0 and offset to 0. - """ - - def __init__(self, name, scale, offset=0): - UnitOfMeasure.__init__(self, name) - self._scale = float(scale) - self._offset = float(offset) - if self._scale == 0: - raise ValueError("Invalid zero UOM scale!") - - def apply(self, value): - """ Convert value from the common base to this uinit.""" - return (value - self._offset)/self._scale - - def strip(self, value): - """ Convert value of this unit to the common base.""" - return value*self._scale + self._offset - diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wps/processes/get_time_data.py eoxserver-0.3.2/eoxserver/services/ows/wps/processes/get_time_data.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wps/processes/get_time_data.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wps/processes/get_time_data.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,131 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Martin Paces -# -#------------------------------------------------------------------------------- -# Copyright (C) 2014 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -import csv -from datetime import datetime - -from eoxserver.core import Component, implements -from eoxserver.core.util.timetools import isoformat - -from eoxserver.services.ows.wps.interfaces import ProcessInterface -from eoxserver.services.ows.wps.parameters import ( - LiteralData, ComplexData, CDAsciiTextBuffer, FormatText -) -from eoxserver.services.ows.wps.exceptions import InvalidInputValueError - -from eoxserver.resources.coverages import models - - -class GetTimeDataProcess(Component): - """ GetTimeDataProcess defines a WPS process needed by the EOxClient - time-slider componenet """ - - implements(ProcessInterface) - - identifier = "getTimeData" - title = "Get times of collection coverages." - decription = ("Query collection and get list of coverages and their times " - "and spatial extents. The process is used by the time-slider " - "of the EOxClient (web client).") - - metadata = {} - profiles = ['EOxServer:GetTimeData'] - - inputs = { - "collection": LiteralData("collection", - title="Collection name (a.k.a. dataset-series identifier)."), - "begin_time": LiteralData("begin_time", datetime, optional=True, - title="Optional start of the time interval."), - "end_time": LiteralData("end_time", datetime, optional=True, - title="Optional end of the time interval."), - } - - outputs = { - "times": ComplexData("times", - formats=(FormatText('text/csv'), FormatText('text/plain')), - title="Comma separated list of collection's coverages, " - "their extents and times.", - abstract="NOTE: The use of the 'text/plain' format is " - "deprecated! This format will be removed!'" - ) - } - - @staticmethod - def execute(collection, begin_time, end_time, **kwarg): - """ The main execution function for the process. - """ - - # get the dataset series matching the requested ID - try: - model = models.EOObject.objects.get(identifier=collection) - except models.EOObject.DoesNotExist: - raise InvalidInputValueError( - "collection", "Invalid collection name '%s'!" % collection - ) - - if models.iscollection(model): - model = model.cast() - - # recursive dataset series lookup - def _get_children_ids(ds): - ds_rct = ds.real_content_type - id_list = [ds.id] - for child in model.eo_objects.filter(real_content_type=ds_rct): - id_list.extend(_get_children_ids(child)) - return id_list - - collection_ids = _get_children_ids(model) - - # prepare coverage query set - coverages_qs = models.Coverage.objects.filter( - collections__id__in=collection_ids - ) - if end_time is not None: - coverages_qs = coverages_qs.filter(begin_time__lte=end_time) - if begin_time is not None: - coverages_qs = coverages_qs.filter(end_time__gte=begin_time) - coverages_qs = coverages_qs.order_by('begin_time', 'end_time') - - else: - coverages_qs = [model] - - # create the output - output = CDAsciiTextBuffer() - writer = csv.writer(output, quoting=csv.QUOTE_ALL) - header = ["starttime", "endtime", "bbox", "identifier"] - writer.writerow(header) - - for coverage in coverages_qs: - starttime = coverage.begin_time - endtime = coverage.end_time - identifier = coverage.identifier - bbox = coverage.extent_wgs84 - writer.writerow( - [isoformat(starttime), isoformat(endtime), bbox, identifier] - ) - - return output diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wps/test_allowed_values.py eoxserver-0.3.2/eoxserver/services/ows/wps/test_allowed_values.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wps/test_allowed_values.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wps/test_allowed_values.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,351 +0,0 @@ -#------------------------------------------------------------------------------- -# -# WPS Literal Data - allowed values - debuging unit-tests -# -# Project: EOxServer -# Authors: Martin Paces -# -#------------------------------------------------------------------------------- -# Copyright (C) 2014 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -import datetime as dt -import unittest -from parameters import (AllowedAny, AllowedEnum, AllowedRange, - AllowedRangeCollection, LiteralData) - -from parameters import (BaseType, Boolean, Integer, Double, String, - Duration, Date, Time, DateTime) - -#------------------------------------------------------------------------------ - -class BaseTestMixin: - def test(self): - for val in self.accepted: - self.assertTrue(self.domain.check(val)) - self.assertTrue(val is self.domain.verify(val)) - for val in self.rejected: - self.assertFalse(self.domain.check(val)) - def test(): - self.domain.verify(val) - self.assertRaises(ValueError,test) - -#------------------------------------------------------------------------------ - -class TestAllowedAny(unittest.TestCase, BaseTestMixin): - def setUp(self): - self.domain = AllowedAny() - self.accepted = [1.0, 10, dt.datetime.now(), dt.timedelta(days=10,seconds=100)] - self.rejected = [] - -#------------------------------------------------------------------------------ - -class TestAllowedEnumFloat(unittest.TestCase, BaseTestMixin): - def setUp(self): - self.domain = AllowedEnum([0.0,0.5,1.0]) - self.accepted = [1.0, 0] - self.rejected = [-1] - -class TestAllowedEnumFloat2(unittest.TestCase, BaseTestMixin): - def setUp(self): - self.domain = AllowedEnum([0.0,0.5,1.0],dtype=Double) - self.accepted = [1.0, 0] - self.rejected = [-1] - -class TestAllowedEnumFloat3(unittest.TestCase, BaseTestMixin): - def setUp(self): - self.domain = AllowedEnum([0.0,0.5,1.0],dtype=float) - self.accepted = [1.0, 0] - self.rejected = [-1] - -class TestAllowedEnumInt(unittest.TestCase, BaseTestMixin): - def setUp(self): - self.domain = AllowedEnum([0,2,3],dtype=Integer) - self.accepted = [2, 0] - self.rejected = [1] - -class TestAllowedEnumInt2(unittest.TestCase, BaseTestMixin): - def setUp(self): - self.domain = AllowedEnum([0,2,3],dtype=int) - self.accepted = [2, 0] - self.rejected = [1] - -class TestAllowedEnumString(unittest.TestCase, BaseTestMixin): - def setUp(self): - enum = ['John','James','Joffrey','Jacob','Jerry'] - self.domain = AllowedEnum(enum,dtype=String) - self.accepted = ['John','Jacob','Jerry'] - self.rejected = ['Alex',''] - -class TestAllowedEnumString2(unittest.TestCase, BaseTestMixin): - def setUp(self): - enum = ['John','James','Joffrey','Jacob','Jerry'] - self.domain = AllowedEnum(enum,dtype=str) - self.accepted = ['John','Jacob','Jerry'] - self.rejected = ['Alex',''] - -class TestAllowedEnumString3(unittest.TestCase, BaseTestMixin): - def setUp(self): - enum = ['John','James','Joffrey','Jacob','Jerry'] - self.domain = AllowedEnum(enum,dtype=unicode) - self.accepted = ['John','Jacob','Jerry'] - self.rejected = ['Alex',''] - -class TestAllowedEnumDate(unittest.TestCase, BaseTestMixin): - def setUp(self): - vlist = ['2014-01-01', '2014-02-01', '2014-03-01'] - self.domain = AllowedEnum(vlist, dtype=Date) - self.accepted = [ vlist[1], Date.parse(vlist[0]) ] - self.rejected = [ Date.parse('2014-01-02') ] - -class TestAllowedEnumDate2(unittest.TestCase, BaseTestMixin): - def setUp(self): - vlist = ['2014-01-01', '2014-02-01', '2014-03-01'] - self.domain = AllowedEnum(vlist, dtype=dt.date) - self.accepted = [ vlist[1], Date.parse(vlist[0]) ] - self.rejected = [ Date.parse('2014-01-02') ] - -class TestAllowedEnumTime(unittest.TestCase, BaseTestMixin): - def setUp(self): - vlist = ['05:30', '08:20', '16:18'] - self.domain = AllowedEnum(vlist, dtype=Time) - self.accepted = [ vlist[1], Time.parse(vlist[0]) ] - self.rejected = [ Time.parse('19:20') ] - -class TestAllowedEnumTime2(unittest.TestCase, BaseTestMixin): - def setUp(self): - vlist = ['05:30', '08:20', '16:18'] - self.domain = AllowedEnum(vlist, dtype=dt.time) - self.accepted = [ vlist[1], Time.parse(vlist[0]) ] - self.rejected = [ Time.parse('19:20') ] - -class TestAllowedEnumDateTime(unittest.TestCase, BaseTestMixin): - def setUp(self): - vlist = ['2014-01-01T09:30:21Z', '2014-02-01T18:20:15Z', '2014-03-01T12:15:02Z' ] - self.domain = AllowedEnum(vlist, dtype=DateTime) - self.accepted = [ vlist[1], DateTime.parse(vlist[0]) ] - self.rejected = [ DateTime.parse('2014-01-01T12:30:00Z') ] - -class TestAllowedEnumDateTime2(unittest.TestCase, BaseTestMixin): - def setUp(self): - vlist = ['2014-01-01T09:30:21Z', '2014-02-01T18:20:15Z', '2014-03-01T12:15:02Z' ] - self.domain = AllowedEnum(vlist, dtype=dt.datetime) - self.accepted = [ vlist[1], DateTime.parse(vlist[0]) ] - self.rejected = [ DateTime.parse('2014-01-01T12:30:00Z') ] - -class TestAllowedEnumDuration(unittest.TestCase, BaseTestMixin): - def setUp(self): - vlist = ['P1Y', 'P26DT1M', 'P25M16S'] - self.domain = AllowedEnum(vlist, dtype=Duration) - self.accepted = [ vlist[1], Duration.parse(vlist[0]) ] - self.rejected = [ Duration.parse('P7D15H8M') ] - -class TestAllowedEnumDuration(unittest.TestCase, BaseTestMixin): - def setUp(self): - vlist = ['P1Y', 'P26DT1M', 'P25M16S'] - self.domain = AllowedEnum(vlist, dtype=dt.timedelta) - self.accepted = [ vlist[1], Duration.parse(vlist[0]) ] - self.rejected = [ Duration.parse('P7D15H8M') ] - -#------------------------------------------------------------------------------ - -class TestAllowedRangeFloat(unittest.TestCase, BaseTestMixin): - def setUp(self): - self.domain = AllowedRange(0.0,1.0) - self.accepted = [0.5, 0.0, 1.0] - self.rejected = [-1.0, 2.0] - -class TestAllowedRangeFloat2(unittest.TestCase, BaseTestMixin): - def setUp(self): - self.domain = AllowedRange(0.0,1.0,dtype=Double) - self.accepted = [0.5, 0.0, 1.0] - self.rejected = [-1.0, 2.0] - -class TestAllowedRangeFloat3(unittest.TestCase, BaseTestMixin): - def setUp(self): - self.domain = AllowedRange(0.0,1.0,dtype=float) - self.accepted = [0.5, 0.0, 1.0] - self.rejected = [-1.0, 2.0] - -class TestAllowedRangeUnboundMin(unittest.TestCase, BaseTestMixin): - def setUp(self): - self.domain = AllowedRange(None,1.0) - self.accepted = ['-inf',-1.0, 0.5, 0.0, 1.0] - self.rejected = [2.0] - -class TestAllowedRangeUnboundMax(unittest.TestCase, BaseTestMixin): - def setUp(self): - self.domain = AllowedRange(0.0,None) - self.accepted = ['+inf',0.5, 0.0, 1.0, 2.0] - self.rejected = [-1.0] - -class TestAllowedRangeFloatClosed(unittest.TestCase, BaseTestMixin): - def setUp(self): - self.domain = AllowedRange(0.0,1.0,'closed') - self.accepted = [0.5, 0.0, 1.0] - self.rejected = [-1.0, 2.0] - -class TestAllowedRangeFloatOpen(unittest.TestCase, BaseTestMixin): - def setUp(self): - self.domain = AllowedRange(0.0,1.0,'open') - self.accepted = [0.5] - self.rejected = [0.0, 1.0, -1.0, 2.0] - -class TestAllowedRangeFloatOpenClosed(unittest.TestCase, BaseTestMixin): - def setUp(self): - self.domain = AllowedRange(0.0,1.0,'open-closed') - self.accepted = [0.5,1.0] - self.rejected = [0.0,-1.0, 2.0] - -class TestAllowedRangeFloatClosedOpen(unittest.TestCase, BaseTestMixin): - def setUp(self): - self.domain = AllowedRange(0.0,1.0,'closed-open') - self.accepted = [0.5,0.0] - self.rejected = [1.0,-1.0, 2.0] - -class TestAllowedRangeInt(unittest.TestCase, BaseTestMixin): - def setUp(self): - self.domain = AllowedRange(0,10,dtype=Integer) - self.accepted = [0, 5, 10] - self.rejected = [-1, 12] - -class TestAllowedRangeIntClosed(unittest.TestCase, BaseTestMixin): - def setUp(self): - self.domain = AllowedRange(0,10,'open',dtype=Integer) - self.accepted = [5] - self.rejected = [0, 10, -1, 12] - -class TestAllowedRangeDateClosed(unittest.TestCase, BaseTestMixin): - def setUp(self): - self.domain = AllowedRange('2014-02-01', '2014-03-01', 'closed', dtype=Date) - self.accepted = [ '2014-02-15', '2014-02-01', Date.parse('2014-03-01')] - self.rejected = [ Date.parse('2014-01-02') ] - -class TestAllowedRangeDateOpen(unittest.TestCase, BaseTestMixin): - def setUp(self): - self.domain = AllowedRange('2014-02-01', '2014-03-01', 'open', dtype=Date) - self.accepted = [ '2014-02-15' ] - self.rejected = [ '2014-02-01', Date.parse('2014-03-01'), Date.parse('2014-01-02')] - -class TestAllowedRangeDateClosedOpen(unittest.TestCase, BaseTestMixin): - def setUp(self): - self.domain = AllowedRange('10:00', '15:30', 'closed-open', dtype=Time) - self.accepted = [ '10:00', '12:15'] - self.rejected = [ Time.parse('09:00'), '15:30' , '18:12' ] - -class TestAllowedRangeDateOpenClosed(unittest.TestCase, BaseTestMixin): - def setUp(self): - self.domain = AllowedRange('10:00', '15:30', 'open-closed', dtype=Time) - self.accepted = [ '12:15', '15:30' ] - self.rejected = [ Time.parse('09:00'), '10:00', '18:12' ] - -class TestAllowedRangeDateTime(unittest.TestCase, BaseTestMixin): - def setUp(self): - self.domain = AllowedRange('2014-02-01T09:30:21Z', '2014-03-01T18:20:15Z', dtype=DateTime) - self.accepted = ['2014-02-01T09:30:21Z', '2014-02-15T00:00:00Z', '2014-03-01T18:20:15Z'] - self.rejected = ['2014-01-01T00:00:00Z', DateTime.parse('2014-04-01T00:00:00Z')] - -class TestAllowedRangeDuration(unittest.TestCase, BaseTestMixin): - def setUp(self): - self.domain = AllowedRange('-P1DT1H', 'P1M0DT30M', dtype=Duration) - self.accepted = [ Duration.parse('-P1DT1H'), 'P0D' , 'P1M0DT30M'] - self.rejected = [ Duration.parse('-P2D18H'), 'P1Y' ] - -#------------------------------------------------------------------------------ - -class TestAllowedRangeDiscrFloat(unittest.TestCase, BaseTestMixin): - def setUp(self): - self.domain = AllowedRange(0.0,1.0,spacing='0.1') - self.accepted = [0.5, 0.0, 1.0] - self.rejected = [0.55,-1.0, 2.0] - -class TestAllowedRangeDiscrInt(unittest.TestCase, BaseTestMixin): - def setUp(self): - self.domain = AllowedRange(0,10,spacing='2',dtype=int) - self.accepted = [4, 0, 10] - self.rejected = [5,-1, 12] - -class TestAllowedRangeDiscrDuration(unittest.TestCase, BaseTestMixin): - def setUp(self): - v0 = dt.timedelta(0,3600,0) - v1 = 'PT5H' - dv = 'PT1H' - cl ='open-closed' - self.domain = AllowedRange(v0,v1,cl,spacing=dv,dtype=dt.timedelta) - self.accepted = [Duration.parse('PT2H'),dt.timedelta(0,3*3600,0), - 'PT4H', 'PT5H','PT2H0.000001S'] # tolerance: step*1e-9 - self.rejected = ['PT30M','PT1H','PT2H30M',] - -class TestAllowedRangeDiscrTime(unittest.TestCase, BaseTestMixin): - def setUp(self): - v0 = dt.time(9,30,0,0) - v1 = '16:30' - dv = 'PT1H' - cl ='closed' - self.domain = AllowedRange(v0,v1,cl,spacing=dv,dtype=dt.time) - self.accepted = ['09:30','10:30','15:30','16:30'] - self.rejected = ['09:00','10:00','16:25','16:35'] - -class TestAllowedRangeDiscrDate(unittest.TestCase, BaseTestMixin): - def setUp(self): - v0 = dt.date(2014,1,1) - v1 = '2014-01-21' - dv = 'P5D' - cl ='closed' - self.domain = AllowedRange(v0,v1,cl,spacing=dv,dtype=dt.date) - self.accepted = ['2014-01-01','2014-01-06','2014-01-16','2014-01-21'] - self.rejected = ['2013-12-31','2014-01-10','2014-01-31'] - -class TestAllowedRangeDiscrDateTime(unittest.TestCase, BaseTestMixin): - def setUp(self): - v0 = dt.datetime(2014,1,1,10,30,0,0,DateTime.UTC) - v1 = '2014-01-10T10:30Z' - dv = 'P1D' - cl ='closed' - self.domain = AllowedRange(v0,v1,cl,spacing=dv,dtype=dt.datetime) - self.accepted = [ - '2014-01-01T10:30Z', '2014-01-02T10:30Z', - '2014-01-05T10:30Z', '2014-01-07T10:30Z', - '2014-01-09T10:30Z', '2014-01-10T10:30Z', - ] - self.rejected = [ - '2013-12-31T10:30Z', '2014-01-01T11:00Z', - '2014-01-05T00:00Z', '2014-01-11T10:30Z', - ] - -##------------------------------------------------------------------------------ - -class TestAllowedRangeCollectionFloat(unittest.TestCase, BaseTestMixin): - - def setUp(self): - self.domain = AllowedRangeCollection( - AllowedRange(None,-5.0,'open'), - AllowedRange(6.0,None,spacing=3.0), - AllowedEnum(xrange(-4,0)), - AllowedEnum(range(0,6,2)), - ) - self.accepted = ['-inf',-100.,-3,2,6,300] - self.rejected = ['nan','+inf',7,3,-0.5,-5] - -#------------------------------------------------------------------------------ - -if __name__ == '__main__': - unittest.main() diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wps/test_data_types.py eoxserver-0.3.2/eoxserver/services/ows/wps/test_data_types.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wps/test_data_types.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wps/test_data_types.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,250 +0,0 @@ -#------------------------------------------------------------------------------- -# -# WPS Literal Data - allowed values - debuging unit-tests -# -# Project: EOxServer -# Authors: Martin Paces -# -#------------------------------------------------------------------------------- -# Copyright (C) 2014 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -import datetime as dt -import unittest -from parameters import (BaseType, Boolean, Integer, Double, String, - Duration, Date, Time, DateTime, CRSType) - -#------------------------------------------------------------------------------ - -class BaseTestMixin(object): - def testGeneral(self): - self.assertTrue( self.name == self.dtype.name ) - self.assertTrue( self.dtype_diff is self.dtype.get_diff_dtype() ) - - def testParseOK(self): - for src, dst in self.parsed: - res = self.dtype.parse(src) - self.assertTrue( isinstance(res,type(dst)) ) - self.assertTrue( res == dst or (res!=res and dst!=dst) ) - - def testEncodeOK(self): - for src, dst in self.encoded: - res = self.dtype.encode(src) - self.assertTrue( isinstance(res,type(dst)) ) - self.assertTrue( res == dst ) - - def testParseFail(self): - for src in self.parsed_rejected: - def test(): - self.dtype.parse(src) - self.assertRaises(ValueError,test) - - def testEncodeFail(self): - for src in self.encoded_rejected: - def test(): - self.dtype.encode(src) - self.assertRaises(ValueError,test) - -#------------------------------------------------------------------------------ - -class TestDataTypeBool(unittest.TestCase, BaseTestMixin): - def setUp(self): - self.name = 'boolean' - self.dtype = Boolean - self.dtype_diff = self.dtype - self.encoded= [(True,u'true'), (1,u'true'), ('Anything',u'true'), - (False,u'false'), (0,u'false'), (None,u'false'), ([],u'false')] - self.encoded_rejected = [] - - self.parsed= [('true',True), ('1',True), ('false',False), ('0',False), - (True,True), (self,True), (False,False), (None,False), ([],False)] - self.parsed_rejected = ['string',u'unicode'] - - -class TestDataTypeInt(unittest.TestCase, BaseTestMixin): - def setUp(self): - self.name = 'integer' - self.dtype = Integer - self.dtype_diff = self.dtype - self.encoded= [ (1,u'1'), (-1,u'-1'), (False, u'0'), (True, u'1'), - (0xFFFFFFFFFFFFFFFFFF,u'4722366482869645213695'), - (-0xFFFFFFFFFFFFFFFFFF,u'-4722366482869645213695'), ] - self.encoded_rejected = [float('NaN'), 'anything'] - - self.parsed= [(u'+0',0), (u'-0',0), ('24',24), ('32145',32145), (-1,-1), - (u'4722366482869645213695',0xFFFFFFFFFFFFFFFFFF), - ('-4722366482869645213695',-4722366482869645213695L), ] - self.parsed_rejected = ['nan',u'-inf',u'24anything','2.5'] - - -class TestDataTypeFloat(unittest.TestCase, BaseTestMixin): - def setUp(self): - self.name = 'double' - self.dtype = Double - self.dtype_diff = self.dtype - self.encoded= [ - (1e250, u'1e+250'), (-1e-250, u'-1e-250'), - (-12345678.9012345678, u'-12345678.9012346'), - (0.6666666666666666, u'0.666666666666667'), (-0.0, u'-0'), - (float('-inf'), u'-inf'), (float('nan'), u'nan'), - ] - self.encoded_rejected = ['anything'] - self.parsed= [ (u'1e250', 1e+250), ('-1e-250', -1e-250), - ('16.25', 16.25), ('-inf',float('-inf')), ('nan',float('nan')),] - self.parsed_rejected = [u'24anything'] - - -class TestDataTypeString(unittest.TestCase, BaseTestMixin): - def setUp(self): - sample_unicode = u'P\u0159\xedli\u0161\u017elu\u0165ou\u010dk\xfd k' \ - u'\u016f\u0148 \xfap\u011bl\u010f\xe1belsk\xe9 \xf3dy.' - sample_str_utf8 = sample_unicode.encode('utf-8') - self.name = 'string' - self.dtype = String - self.dtype_diff = None - self.encoded= [('TEST',u'TEST'), (sample_unicode,sample_unicode)]#, (sample_str_utf8,sample_unicode) ] - self.encoded_rejected = [] - self.parsed= [(sample_unicode,sample_unicode)]#, (sample_str_utf8,sample_unicode) ] - self.parsed_rejected = [] - - -class TestDataTypeDuration(unittest.TestCase, BaseTestMixin): - def setUp(self): - self.name = 'duration' - self.dtype = Duration - self.dtype_diff = self.dtype - self.encoded= [ - (dt.timedelta(2,11911,654321),u'P2DT3H18M31.654321S'), - (-dt.timedelta(2,11911,654321),u'-P2DT3H18M31.654321S'), - (dt.timedelta(2,11911,0),u'P2DT3H18M31S'), - (-dt.timedelta(2,11911,0),u'-P2DT3H18M31S'), - (dt.timedelta(2,0,654321),u'P2DT0.654321S'), - (dt.timedelta(2,0,654321),u'P2DT0.654321S'), - (-dt.timedelta(561,0,0),u'-P561D'), - (-dt.timedelta(561,0,0),u'-P561D'), - (dt.timedelta(0,11911,654321),u'PT3H18M31.654321S'), - (-dt.timedelta(0,11911,654321),u'-PT3H18M31.654321S'), - (dt.timedelta(0,0,0),u'PT0S'), - (-dt.timedelta(0,0,0),u'PT0S'), - ] - self.encoded_rejected = ['anything'] - self.parsed= [ - (u'P4Y3M2DT3H18M31.654321S', dt.timedelta(1552,11911,654321)), - ('-P4Y3M2DT3H18M31.654321S', -dt.timedelta(1552,11911,654321)), - (u'P1.6Y1.6M1.6DT1.6H1.6M0S', dt.timedelta(633,57696,0)), - (u'-P1.6Y1.6M1.6DT1.6H1.6M0S', -dt.timedelta(633,57696,0)), - (u'PT0S', dt.timedelta(0,0,0)), - (u'P0Y', dt.timedelta(0,0,0)), - ] - self.parsed_rejected = [u'anything'] - - -class TestDataTypeDate(unittest.TestCase, BaseTestMixin): - def setUp(self): - self.name = 'date' - self.dtype = Date - self.dtype_diff = Duration - self.encoded= [ - (dt.date(1830,6,7),u'1830-06-07'), - (dt.date(2014,3,31),u'2014-03-31'), - ] - self.encoded_rejected = ['anything'] - self.parsed= [ - (dt.date(1830,6,7), dt.date(1830,6,7)), - (u'1830-06-07',dt.date(1830,6,7)), - ('2014-03-31',dt.date(2014,3,31)), - ] - self.parsed_rejected = [u'anything', u'2014-02-29', u'2014-13-01', - u'2014-02-00', u'2014-00-01'] - - -class TestDataTypeTime(unittest.TestCase, BaseTestMixin): - # TODO: time-zones - def setUp(self): - self.name = 'time' - self.dtype = Time - self.dtype_diff = Duration - self.encoded= [ - (dt.time(0,0,0,0), u'00:00:00'), - (dt.time(12,30,30,500000), u'12:30:30.500000'), - (dt.time(23,59,59,999999), u'23:59:59.999999'), - ] - self.encoded_rejected = ['anything'] - self.parsed= [ - (dt.time(12,30,30,500000),dt.time(12,30,30,500000)), - ( u'00:00:00Z', dt.time(0,0,0,0)), - ('12:30:30.5', dt.time(12,30,30,500000)), - ('23:59:59.999999+01:30', dt.time(23,59,59,999999)), - ] - self.parsed_rejected = [u'anything', - '24:00:00', '18:60:00', '18:30:60', - ] - - -class TestDataTypeTime(unittest.TestCase, BaseTestMixin): - def setUp(self): - UTC = DateTime.UTC - TZOffset = DateTime.TZOffset - self.name = 'dateTime' - self.dtype = DateTime - self.dtype_diff = Duration - # NOTE: The eoxserver isoformat tool localizes the time-zone unaware - # time by adding the UTC timezone! - self.encoded= [ - (dt.datetime(2014,6,1,12,30,14,123456,UTC), u'2014-06-01T12:30:14.123456Z'), - (dt.datetime(2014,6,1,12,30,14,500000,TZOffset(90)), - u'2014-06-01T12:30:14.500000+01:30'), - (dt.datetime(2014,6,1,12,30,0,0,UTC), u'2014-06-01T12:30:00Z'), - (dt.datetime(2014,6,1,12,30,0,0), u'2014-06-01T12:30:00Z'), - ] - self.encoded_rejected = ['anything'] - self.parsed= [ - (dt.datetime(2014,6,1,11,00,14,123456,UTC),dt.datetime(2014,6,1,11,00,14,123456,UTC)), - (u'2014-06-01T12:30:14.123456',dt.datetime(2014,6,1,12,30,14,123456)), - (u'2014-06-01T12:30:14.123456Z',dt.datetime(2014,6,1,12,30,14,123456,UTC)), - (u'2014-06-01T12:30:14.123456+01:30',dt.datetime(2014,6,1,11,00,14,123456,UTC)), - (u'2014-06-01 12:30:14',dt.datetime(2014,6,1,12,30,14,0)), - (u'2014-06-01T00:00Z',dt.datetime(2014,6,1,0,0,0,0,UTC)), - ] - self.parsed_rejected = [u'anything', - u'2014-06-01T12:30:60', u'2014-06-01T12:60:30', - u'2014-06-01T24:00:00', - u'2014-02-29T00:00', u'2014-13-01T00:00', - u'2014-02-00T00:00', u'2014-00-01T00:00', - ] - -class TestDataTypeCRS(unittest.TestCase, BaseTestMixin): - def setUp(self): - self.name = 'anyURI' - self.dtype = CRSType - self.dtype_diff = None - self.encoded= [(0,u'ImageCRS'), - (4326, u'http://www.opengis.net/def/crs/EPSG/0/4326'), ] - self.encoded_rejected = [-1] - self.parsed= [ ('ImageCRS',0), ('EPSG:4326',4326), - ('http://www.opengis.net/def/crs/EPSG/0/4326',4326), - ('urn:ogc:def:crs:epsg:6.2:4326',4326)] - self.parsed_rejected = ["anything", "EPSG:0"] - -#------------------------------------------------------------------------------ - -if __name__ == '__main__': - unittest.main() diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wps/v10/describeprocess.py eoxserver-0.3.2/eoxserver/services/ows/wps/v10/describeprocess.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wps/v10/describeprocess.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wps/v10/describeprocess.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,88 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -from eoxserver.core import Component, ExtensionPoint, implements -from eoxserver.core.decoders import kvp, xml, typelist -from eoxserver.services.ows.interfaces import ( - ServiceHandlerInterface, GetServiceHandlerInterface, - PostServiceHandlerInterface -) -from eoxserver.services.ows.wps.interfaces import ProcessInterface -from eoxserver.services.ows.wps.exceptions import NoSuchProcessError -from eoxserver.services.ows.wps.v10.encoders import ( - WPS10ProcessDescriptionsXMLEncoder -) -from eoxserver.services.ows.wps.v10.util import nsmap - - -class WPS10DescribeProcessHandler(Component): - - implements(ServiceHandlerInterface) - implements(GetServiceHandlerInterface) - implements(PostServiceHandlerInterface) - - service = "WPS" - versions = ("1.0.0",) - request = "DescribeProcess" - - processes = ExtensionPoint(ProcessInterface) - - @staticmethod - def get_decoder(request): - if request.method == "GET": - return WPS10DescribeProcessKVPDecoder(request.GET) - else: - return WPS10DescribeProcessXMLDecoder(request.body) - - - def handle(self, request): - decoder = self.get_decoder(request) - identifiers = set(decoder.identifiers) - - used_processes = [] - for process in self.processes: - if process.identifier in identifiers: - identifiers.remove(process.identifier) - used_processes.append(process) - - for identifier in identifiers: - raise NoSuchProcessError(identifier) - - encoder = WPS10ProcessDescriptionsXMLEncoder() - return encoder.serialize( - encoder.encode_process_descriptions(used_processes) - ), encoder.content_type - - -class WPS10DescribeProcessKVPDecoder(kvp.Decoder): - identifiers = kvp.Parameter("identifier", type=typelist(str, ",")) - - -class WPS10DescribeProcessXMLDecoder(xml.Decoder): - identifiers = xml.Parameter("ows:Identifier/text()", num="+") - namespaces = nsmap - diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wps/v10/encoders/base.py eoxserver-0.3.2/eoxserver/services/ows/wps/v10/encoders/base.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wps/v10/encoders/base.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wps/v10/encoders/base.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,36 +0,0 @@ -#------------------------------------------------------------------------------- -# -# WPS 1.0 base XML encoder -# -# Project: EOxServer -# Authors: Fabian Schindler -# Martin Paces -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -from eoxserver.core.util.xmltools import XMLEncoder -from eoxserver.services.ows.wps.v10.util import ns_wps - -class WPS10BaseXMLEncoder(XMLEncoder): - def get_schema_locations(self): - return {"wps": ns_wps.schema_location} diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wps/v10/encoders/capabilities.py eoxserver-0.3.2/eoxserver/services/ows/wps/v10/encoders/capabilities.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wps/v10/encoders/capabilities.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wps/v10/encoders/capabilities.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,123 +0,0 @@ -#------------------------------------------------------------------------------- -# -# WPS 1.0 Capabilities XML encoders -# -# Project: EOxServer -# Authors: Fabian Schindler -# Martin Paces -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -from eoxserver.core.config import get_eoxserver_config -from eoxserver.services.ows.component import ServiceComponent, env -from eoxserver.services.ows.common.config import CapabilitiesConfigReader -from eoxserver.services.ows.wps.v10.util import ( - OWS, WPS, ns_ows, ns_wps, ns_xlink, ns_xml -) - -from .process_description import encode_process_brief -from .base import WPS10BaseXMLEncoder - - -def _encode_operations_metadata(conf): - component = ServiceComponent(env) - versions = ("1.0.0",) - get_handlers = component.query_service_handlers( - service="WPS", versions=versions, method="GET" - ) - post_handlers = component.query_service_handlers( - service="WPS", versions=versions, method="POST" - ) - all_handlers = sorted( - set(get_handlers + post_handlers), key=lambda h: h.request - ) - url = conf.http_service_url - return OWS("OperationsMetadata", *[ - OWS("Operation", - OWS("DCP", - OWS("HTTP", - # TODO: only select available - OWS("Get", **{ns_xlink("href"): url}), - OWS("Post", **{ns_xlink("href"): url}), - ) - ), name=handler.request - ) - for handler in all_handlers - ]) - - -class WPS10CapabilitiesXMLEncoder(WPS10BaseXMLEncoder): - - @staticmethod - def encode_capabilities(processes): - conf = CapabilitiesConfigReader(get_eoxserver_config()) - return WPS("Capabilities", - OWS("ServiceIdentification", - OWS("Title", conf.title), - OWS("Abstract", conf.abstract), - OWS("Keywords", *(OWS("Keyword", kw) for kw in conf.keywords)), - OWS("ServiceType", "WPS"), - OWS("ServiceTypeVersion", "1.0.0"), - OWS("Fees", conf.fees), - OWS("AccessConstraints", conf.access_constraints), - ), - OWS("ServiceProvider", - OWS("ProviderName", conf.provider_name), - OWS("ProviderSite", **{ns_xlink("href"): conf.provider_site}), - OWS("ServiceContact", - OWS("IndividualName", conf.individual_name), - OWS("PositionName", conf.position_name), - OWS("ContactInfo", - OWS("Phone", - OWS("Voice", conf.phone_voice), - OWS("Facsimile", conf.phone_facsimile) - ), - OWS("Address", - OWS("DeliveryPoint", conf.delivery_point), - OWS("City", conf.city), - OWS("AdministrativeArea", conf.administrative_area), - OWS("PostalCode", conf.postal_code), - OWS("Country", conf.country), - OWS("ElectronicMailAddress", conf.electronic_mail_address) - ) - ) - ) - ), - _encode_operations_metadata(conf), - WPS("ProcessOfferings", *(encode_process_brief(p) for p in processes)), - WPS("Languages", - WPS("Default", - OWS("Language", "en-US") - ), - WPS("Supported", - OWS("Language", "en-US") - ) - ), - # TODO: WPS("WSDL") ? - **{ - "service": "WPS", - "version": "1.0.0", - ns_xml("lang"): "en-US", - "updateSequence": conf.update_sequence, - } - ) diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wps/v10/encoders/execute_response.py eoxserver-0.3.2/eoxserver/services/ows/wps/v10/encoders/execute_response.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wps/v10/encoders/execute_response.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wps/v10/encoders/execute_response.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,200 +0,0 @@ -#------------------------------------------------------------------------------- -# -# WPS 1.0 execute response XML encoder -# -# Project: EOxServer -# Authors: Fabian Schindler -# Martin Paces -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -from lxml import etree -from django.utils.timezone import now -from eoxserver.core.config import get_eoxserver_config -from eoxserver.services.ows.common.config import CapabilitiesConfigReader -from eoxserver.core.util.timetools import isoformat -from eoxserver.services.ows.wps.v10.util import WPS, OWS, ns_xlink, ns_xml - -from eoxserver.services.ows.wps.parameters import ( - Parameter, LiteralData, ComplexData, BoundingBoxData, - fix_parameter, InputReference -) - -from .process_description import encode_process_brief -from .parameters import ( - encode_input_exec, encode_output_exec, encode_output_def -) -from .base import WPS10BaseXMLEncoder - -from eoxserver.services.ows.wps.exceptions import InvalidOutputValueError - -#------------------------------------------------------------------------------- - -class WPS10ExecuteResponseXMLEncoder(WPS10BaseXMLEncoder): - - content_type = "application/xml; charset=utf-8" - - @staticmethod - def encode_response(process, results, resp_form, inputs, raw_inputs): - """Encode execute response (SUCCESS) including the output data.""" - status = WPS("ProcessSucceeded") - elem = _encode_common_response(process, status, inputs, raw_inputs, resp_form) - - outputs = [] - for result, prm, req in results.itervalues(): - outputs.append(_encode_output(result, prm, req)) - elem.append(WPS("ProcessOutputs", *outputs)) - - return elem - - #@staticmethod - #def encode_failure() - - #@staticmethod - #def encode_progress() - - #@staticmethod - #def encode_accepted() - -#------------------------------------------------------------------------------- - -def _encode_common_response(process, status_elem, inputs, raw_inputs, resp_doc): - """Encode common execute response part shared by all specific responses.""" - conf = CapabilitiesConfigReader(get_eoxserver_config()) - url = conf.http_service_url - dlm = "?" if url[-1] != "?" else "" - elem = WPS("ExecuteResponse", - encode_process_brief(process), - WPS("Status", status_elem, creationTime=isoformat(now())), - { - "service": "WPS", - "version": "1.0.0", - ns_xml("lang"): "en-US", - "serviceInstance": "%s%sservice=WPS&version=1.0.0&request="\ - "GetCapabilities"%(url, dlm) - }, - ) - - if resp_doc.lineage: - inputs_data = [] - for id_, prm in process.inputs: - prm = fix_parameter(id_, prm) - data = inputs.get(id_) - rawinp = raw_inputs.get(prm.identifier) - if rawinp is not None: - inputs_data.append(_encode_input(data, prm, rawinp)) - elem.append(WPS("DataInputs", *inputs_data)) - - outputs_def = [] - for id_, prm in process.outputs: - prm = fix_parameter(id_, prm) - outdef = resp_doc.get(prm.identifier) - if outdef is not None: - outputs_def.append(encode_output_def(outdef)) - elem.append(WPS("OutputDefinitions", *outputs_def)) - - return elem - -def _encode_input(data, prm, raw): - elem = encode_input_exec(raw) - - if isinstance(raw, InputReference): - elem.append(_encode_input_reference(raw)) - elif isinstance(prm, LiteralData): - elem.append(WPS("Data", _encode_literal(data, prm, raw))) - elif isinstance(prm, BoundingBoxData): - elem.append(WPS("Data", _encode_bbox(data, prm))) - elif isinstance(prm, ComplexData): - elem.append(WPS("Data", _encode_complex(data, prm))) - return elem - -def _encode_output(data, prm, req): - elem = encode_output_exec(Parameter(prm.identifier, - req.title or prm.title, req.abstract or prm.abstract)) - if isinstance(prm, LiteralData): - elem.append(WPS("Data", _encode_literal(data, prm, req))) - elif isinstance(prm, BoundingBoxData): - elem.append(WPS("Data", _encode_bbox(data, prm))) - elif isinstance(prm, ComplexData): - elem.append(WPS("Data", _encode_complex(data, prm))) - return elem - -def _encode_input_reference(ref): - #TODO proper input reference encoding - return WPS("Reference", **{ns_xlink("href"): ref.href}) - -def _encode_literal(data, prm, req): - attrib = {'dataType': prm.dtype.name} - uom = req.uom or prm.default_uom - if prm.uoms: - attrib['uom'] = uom - try: - encoded_data = prm.encode(data, uom) - except (ValueError, TypeError) as exc: - raise InvalidOutputValueError(prm.identifier, exc) - return WPS("LiteralData", encoded_data, **attrib) - -def _encode_bbox(data, prm): - try: - lower, upper, crs = prm.encode_xml(data) - except (ValueError, TypeError) as exc: - raise InvalidOutputValueError(prm.identifier, exc) - - return WPS("BoundingBoxData", - OWS("LowerCorner", lower), - OWS("UpperCorner", upper), - crs=crs, - #dimension="%d"%prm.dimension, - ) - #NOTE: Although derived from OWS BoundingBox the WPS (schema) does not - # allow the dimenstion attribute. - -def _encode_format_attr(data, prm): - mime_type = getattr(data, 'mime_type', None) - if mime_type is not None: - encoding = getattr(data, 'encoding', None) - schema = getattr(data, 'schema', None) - else: - deffrm = prm.default_format - mime_type = deffrm.mime_type - encoding = deffrm.encoding - schema = deffrm.schema - attr = {"mimeType": mime_type} - if encoding is not None: - attr['encoding'] = encoding - if schema is not None: - attr['schema'] = schema - return attr - -def _encode_complex(data, prm): - try: - payload = prm.encode_xml(data) - except (ValueError, TypeError) as exc: - raise InvalidOutputValueError(prm.identifier, exc) - - elem = WPS("ComplexData", **_encode_format_attr(data, prm)) - if isinstance(payload, etree._Element): - elem.append(payload) - else: - elem.text = etree.CDATA(payload) - return elem diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wps/v10/encoders/execute_response_raw.py eoxserver-0.3.2/eoxserver/services/ows/wps/v10/encoders/execute_response_raw.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wps/v10/encoders/execute_response_raw.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wps/v10/encoders/execute_response_raw.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,157 +0,0 @@ -#------------------------------------------------------------------------------- -# -# WPS 1.0 execute response raw encoder -# -# Project: EOxServer -# Authors: Fabian Schindler -# Martin Paces -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -import types - -from eoxserver.services.result import ( - to_http_response, ResultItem, #ResultFile, -) -from eoxserver.services.ows.wps.parameters import ( - LiteralData, ComplexData, BoundingBoxData, -) - -from eoxserver.services.ows.wps.exceptions import InvalidOutputValueError - -try: - from cStringIO import StringIO -except ImportError: - from StringIO import StringIO - - -#------------------------------------------------------------------------------- - -class WPS10ExecuteResponseRawEncoder(object): - - @staticmethod - def serialize(result_items, **kwargs): - return to_http_response(result_items) - - def __init__(self): - self.content_type = None - - def encode_response(self, process, results, resp_form, inputs, raw_inputs): - """Pack the raw execute response.""" - outputs = [] - for data, prm, req in results.itervalues(): - if prm.identifier in resp_form: - outputs.append(_encode_raw_output(data, prm, req)) - - if len(outputs) == 1: - self.content_type = outputs[0].content_type - else: - self.content_type = "multipart/related" - - return outputs - -#------------------------------------------------------------------------------- - -class ResultAlt(ResultItem): - """ Alternative implementation of the result buffer. The object can be - inifilized by a byte-string, by a sequence or generator of byte-string, - or by a seekable file(-like) object. - """ - - def __init__(self, buf, content_type=None, filename=None, identifier=None, - close=False): - ResultItem.__init__(self, content_type, filename, identifier) - if isinstance(buf, basestring): - self._file = StringIO(str(buf)) # make sure a byte string is passed - elif isinstance(buf, (tuple, list, types.GeneratorType)): - tmp = StringIO() - for chunk in buf: - tmp.write(chunk) - self._file = tmp - else: - self._file = buf - self._close = close - - def __del__(self): - if self._close: - self._file.close() - - def __len__(self): - self._file.seek(0, 2) - return self._file.tell() - - @property - def data_file(self): - self._file.seek(0) - return self._file - - @property - def data(self): - return self.data_file.read() - - def chunked(self, chunksize): - if chunksize < 0: - raise ValueError("Invalid chunk-size %d"%chunksize) - data_file = self.data_file - for chunk in iter(lambda: data_file.read(chunksize), ''): - yield chunk - -#------------------------------------------------------------------------------- - -def _encode_raw_output(data, prm, req): - """ Encode a raw output item.""" - if isinstance(prm, LiteralData): - return _encode_raw_literal(data, prm, req) - elif isinstance(prm, BoundingBoxData): - return _encode_raw_bbox(data, prm, req) - elif isinstance(prm, ComplexData): - return _encode_raw_complex(data, prm) - raise TypeError("Invalid output type! %r"%(prm)) - -def _encode_raw_literal(data, prm, req): - """ Encode a raw literal.""" - content_type = "text/plain" if req.mime_type is None else req.mime_type - content_type = "%s; charset=utf-8"%content_type - try: - encoded_data = prm.encode(data, req.uom or prm.default_uom, 'utf-8') - except (ValueError, TypeError) as exc: - raise InvalidOutputValueError(prm.identifier, exc) - return ResultAlt(encoded_data, identifier=prm.identifier, - content_type=content_type) - -def _encode_raw_bbox(data, prm, req): - """ Encode a raw bounding box.""" - content_type = "text/plain" if req.mime_type is None else req.mime_type - content_type = "%s; charset=utf-8"%content_type - try: - encoded_data = prm.encode_kvp(data).encode('utf-8') - except (ValueError, TypeError) as exc: - raise InvalidOutputValueError(prm.identifier, exc) - return ResultAlt(encoded_data, identifier=prm.identifier, - content_type="text/plain" if req.mime_type is None else req.mime_type) - -def _encode_raw_complex(data, prm): - """ Encode raw complex data.""" - payload, content_type = prm.encode_raw(data) - return ResultAlt(payload, identifier=prm.identifier, - content_type=content_type) diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wps/v10/encoders/__init__.py eoxserver-0.3.2/eoxserver/services/ows/wps/v10/encoders/__init__.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wps/v10/encoders/__init__.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wps/v10/encoders/__init__.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,35 +0,0 @@ -#------------------------------------------------------------------------------- -# -# WPS 1.0 XML encoders -# -# Project: EOxServer -# Authors: Martin Paces -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -from .base import WPS10BaseXMLEncoder -from .capabilities import WPS10CapabilitiesXMLEncoder -from .process_description import WPS10ProcessDescriptionsXMLEncoder -from .execute_response import WPS10ExecuteResponseXMLEncoder -from .execute_response_raw import WPS10ExecuteResponseRawEncoder - diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wps/v10/encoders/parameters.py eoxserver-0.3.2/eoxserver/services/ows/wps/v10/encoders/parameters.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wps/v10/encoders/parameters.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wps/v10/encoders/parameters.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,186 +0,0 @@ -#------------------------------------------------------------------------------- -# -# WPS 1.0 parameters' XML encoders -# -# Project: EOxServer -# Authors: Fabian Schindler -# Martin Paces -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -from eoxserver.services.ows.wps.parameters import ( - LiteralData, ComplexData, BoundingBoxData, - AllowedAny, AllowedEnum, AllowedRange, AllowedRangeCollection, - AllowedByReference, -) - -from eoxserver.services.ows.wps.v10.util import ( - OWS, WPS, NIL, ns_ows, -) - -#------------------------------------------------------------------------------- - -def encode_input_descr(prm): - """ Encode process description input.""" - elem = NIL("Input", *_encode_param_common(prm)) - elem.attrib["minOccurs"] = ("1", "0")[bool(prm.is_optional)] - elem.attrib["maxOccurs"] = "1" - if isinstance(prm, LiteralData): - elem.append(_encode_literal(prm, True)) - elif isinstance(prm, ComplexData): - elem.append(_encode_complex(prm, True)) - elif isinstance(prm, BoundingBoxData): - elem.append(_encode_bbox(prm, True)) - return elem - -def encode_output_descr(prm): - """ Encode process description output.""" - elem = NIL("Output", *_encode_param_common(prm)) - if isinstance(prm, LiteralData): - elem.append(_encode_literal(prm, False)) - elif isinstance(prm, ComplexData): - elem.append(_encode_complex(prm, False)) - elif isinstance(prm, BoundingBoxData): - elem.append(_encode_bbox(prm, False)) - return elem - -def encode_input_exec(prm): - """ Encode common part of the execure response data input.""" - return WPS("Input", *_encode_param_common(prm, False)) - -def encode_output_exec(prm): - """ Encode common part of the execure response data output.""" - return WPS("Output", *_encode_param_common(prm)) - -def encode_output_def(outdef): - """ Encode the execure response output definition.""" - attrib = {} - if outdef.uom is not None: - attrib['uom'] = outdef.uom - if outdef.crs is not None: - attrib['crs'] = outdef.crs - if outdef.mime_type is not None: - attrib['mimeType'] = outdef.mime_type - if outdef.encoding is not None: - attrib['encoding'] = outdef.encoding - if outdef.schema is not None: - attrib['schema'] = outdef.schema - if outdef.as_reference is not None: - attrib['asReference'] = 'true' if outdef.as_reference else 'false' - return WPS("Output", *_encode_param_common(outdef, False), **attrib) - -def _encode_param_common(prm, title_required=True): - """ Encode common sub-elements of all XML parameters.""" - elist = [OWS("Identifier", prm.identifier)] - if prm.title or title_required: - elist.append(OWS("Title", prm.title or prm.identifier)) - if prm.abstract: - elist.append(OWS("Abstract", prm.abstract)) - return elist - -#------------------------------------------------------------------------------- - -def _encode_literal(prm, is_input): - dtype = prm.dtype - elem = NIL("LiteralData" if is_input else "LiteralOutput") - - elem.append(OWS("DataType", dtype.name, **{ - ns_ows("reference"): "http://www.w3.org/TR/xmlschema-2/#%s"%dtype.name, - })) - - if prm.uoms: - elem.append(NIL("UOMs", - NIL("Default", OWS("UOM", prm.uoms[0])), - NIL("Supported", *[OWS("UOM", u) for u in prm.uoms]) - )) - - if is_input: - elem.append(_encode_allowed_value(prm.allowed_values)) - - if prm.default is not None: - elem.append(NIL("DefaultValue", str(prm.default))) - - return elem - -def _encode_allowed_value(avobj): - enum, ranges, elist = None, [], [] - - if isinstance(avobj, AllowedAny): - return OWS("AnyValue") - elif isinstance(avobj, AllowedByReference): - return WPS("ValuesReference", **{ - ns_ows("reference"): avobj.url, - "valuesForm": avobj.url, - }) - elif isinstance(avobj, AllowedEnum): - enum = avobj - elif isinstance(avobj, AllowedRange): - ranges = [avobj] - elif isinstance(avobj, AllowedRangeCollection): - enum, ranges = avobj.enum, avobj.ranges - else: - raise TypeError("Invalid allowed value object! OBJ=%r"%avobj) - - dtype = avobj.dtype - ddtype = dtype.get_diff_dtype() - - if enum is not None: - elist.extend(OWS("Value", dtype.encode(v)) for v in enum.values) - for range_ in ranges: - attr, elms = {}, [] - if range_.closure != 'closed': - attr = {ns_ows("rangeClosure"): range_.closure} - if range_.minval is not None: - elms.append(OWS("MinimumValue", dtype.encode(range_.minval))) - if range_.maxval is not None: - elms.append(OWS("MaximumValue", dtype.encode(range_.maxval))) - if range_.spacing is not None: - elms.append(OWS("Spacing", ddtype.encode(range_.spacing))) - elist.append(OWS("Range", *elms, **attr)) - - return OWS("AllowedValues", *elist) - -#------------------------------------------------------------------------------- - -def _encode_complex(prm, is_input): - return NIL("ComplexData" if is_input else "ComplexOutput", - NIL("Default", _encode_format(prm.default_format)), - NIL("Supported", *[_encode_format(f) for f in prm.formats.itervalues()]) - ) - -def _encode_format(frmt): - elem = NIL("Format", NIL("MimeType", frmt.mime_type)) - if frmt.encoding is not None: - elem.append(NIL("Encoding", frmt.encoding)) - if frmt.schema is not None: - elem.append(NIL("Schema", frmt.schema)) - return elem - -#------------------------------------------------------------------------------- - -def _encode_bbox(prm, is_input): - return NIL("BoundingBoxData" if is_input else "BoundingBoxOutput", - NIL("Default", NIL("CRS", prm.encode_crs(prm.default_crs))), - NIL("Supported", *[NIL("CRS", prm.encode_crs(crs)) for crs in prm.crss]) - ) - diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wps/v10/encoders/process_description.py eoxserver-0.3.2/eoxserver/services/ows/wps/v10/encoders/process_description.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wps/v10/encoders/process_description.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wps/v10/encoders/process_description.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,104 +0,0 @@ -#------------------------------------------------------------------------------- -# -# WPS 1.0 ProcessDescriptsions XML encoders -# -# Project: EOxServer -# Authors: Fabian Schindler -# Martin Paces -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -from eoxserver.services.ows.wps.v10.util import ( - OWS, WPS, NIL, ns_wps, ns_xlink, ns_xml -) -from .parameters import encode_input_descr, encode_output_descr -from .base import WPS10BaseXMLEncoder -from eoxserver.services.ows.wps.parameters import fix_parameter - - -def _encode_metadata(title, href): - return OWS("Metadata", **{ns_xlink("title"): title, ns_xlink("href"): href}) - -def _encode_process_brief(process, elem): - """ auxiliary shared brief process description encoder""" - id_ = getattr(process, 'identifier', process.__class__.__name__) - title = getattr(process, 'title', id_) - #abstract = getattr(process, 'abstract', process.__class__.__doc__) - abstract = getattr(process, 'description', process.__class__.__doc__) - version = getattr(process, "version", "1.0.0") - metadata = getattr(process, "metadata", {}) - profiles = getattr(process, "profiles", []) - wsdl = getattr(process, "wsdl", None) - - elem.append(OWS("Identifier", id_)) - elem.append(OWS("Title", title)) - elem.attrib[ns_wps("processVersion")] = version - if abstract: - elem.append(OWS("Abstract", abstract)) - elem.extend(_encode_metadata(k, metadata[k]) for k in metadata) - elem.extend(WPS("Profile", p) for p in profiles) - if wsdl: - elem.append(WPS("WSDL", **{ns_xlink("href"): wsdl})) - - return elem - -def encode_process_brief(process): - """ Encode brief process description used in GetCapabilities response.""" - return _encode_process_brief(process, WPS("Process")) - -def encode_process_full(process): - """ Encode full process description used in DescribeProcess response.""" - # TODO: support for async processes - supports_store = False - supports_update = False - - # TODO: remove backward compatibitity support for inputs/outputs dicts - if isinstance(process.inputs, dict): - process.inputs = process.inputs.items() - if isinstance(process.outputs, dict): - process.outputs = process.outputs.items() - - inputs = [encode_input_descr(fix_parameter(n, p)) for n, p in process.inputs] - outputs = [encode_output_descr(fix_parameter(n, p)) for n, p in process.outputs] - - elem = _encode_process_brief(process, NIL("ProcessDescription")) - if supports_store: - elem.attrib["storeSupported"] = "true" - if supports_update: - elem.attrib["statusSupported"] = "true" - elem.append(NIL("DataInputs", *inputs)) - elem.append(NIL("ProcessOutputs", *outputs)) - - return elem - - -class WPS10ProcessDescriptionsXMLEncoder(WPS10BaseXMLEncoder): - @staticmethod - def encode_process_descriptions(processes): - _proc = [encode_process_full(p) for p in processes] - _attr = { - "service": "WPS", - "version": "1.0.0", - ns_xml("lang"): "en-US", - } - return WPS("ProcessDescriptions", *_proc, **_attr) diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wps/v10/exceptionhandler.py eoxserver-0.3.2/eoxserver/services/ows/wps/v10/exceptionhandler.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wps/v10/exceptionhandler.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wps/v10/exceptionhandler.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,54 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -from eoxserver.core import Component, implements -from eoxserver.services.ows.interfaces import ExceptionHandlerInterface -from eoxserver.services.ows.common.v11.encoders import OWS11ExceptionXMLEncoder - - -class WPS10ExceptionHandler(Component): - implements(ExceptionHandlerInterface) - - service = "WPS" - versions = ("1.0.0", "1.0") - request = None - - - def handle_exception(self, request, exception): - locator = getattr(exception, "locator", None) - code = getattr(exception, "code", None) or type(exception).__name__ - - encoder = OWS11ExceptionXMLEncoder() - - return ( - encoder.serialize( - encoder.encode_exception( - str(exception), "1.1.0", code, locator - ) - ), - encoder.content_type, 400 - ) diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wps/v10/execute_decoder_kvp.py eoxserver-0.3.2/eoxserver/services/ows/wps/v10/execute_decoder_kvp.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wps/v10/execute_decoder_kvp.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wps/v10/execute_decoder_kvp.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,126 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Execute - KVP decoder -# -# Project: EOxServer -# Authors: Fabian Schindler -# Martin Paces -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -import urllib -from eoxserver.core.decoders import kvp - -from eoxserver.services.ows.wps.parameters import ( - InputData, InputReference, Output, ResponseDocument, RawDataOutput -) - -def _parse_inputs(raw_string): - inputs = {} - for item in raw_string.split(";"): - id_, value, param = _parse_param(item) - href = param.get("href") or param.get("xlink:href") - if href is not None: - input_ = InputReference( - href=href, - identifier=id_, - mime_type=param.get("mimeType"), - encoding=param.get("encoding"), - schema=param.get("schema") - ) - else: - #NOTE: KVP Bounding box cannot be safely detected and parsed. - input_ = InputData( - identifier=id_, - data=value, - uom=param.get("uom"), - mime_type=param.get("mimeType"), - encoding=param.get("encoding"), - schema=param.get("schema"), - asurl=True, - ) - inputs[id_] = input_ - return inputs - -def _parse_param(raw_string): - items = (item.partition('=') for item in raw_string.split("@")) - attr = {} - id_, dlm, data = items.next() - data = urllib.unquote_plus(data) if dlm else None - for key, dlm, val in items: - if dlm: - attr[urllib.unquote_plus(key)] = urllib.unquote_plus(val) - return id_, data, attr - - -def _parse_outputs(raw_string): - outputs = [] - for output in raw_string.split(";"): - outputs.append(_create_output(*_parse_param(output))) - return outputs - - -def _parse_raw_output(raw_string): - return RawDataOutput(_create_output(*_parse_param(raw_string))) - - -def _parse_bool(raw_string): - return raw_string == 'true' - - -def _create_output(identifier, _, attrs): - attr_as_reference = False - #attr_as_reference = attrs.get("asReference") - #if attr_as_reference is not None: - # attr_as_reference = attr_as_reference == true - - return Output(identifier, None, None, attrs.get("uom"), - attrs.get("crs"), attrs.get("mimeType"), attrs.get("encoding"), - attrs.get("schema"), attr_as_reference) - - -class WPS10ExecuteKVPDecoder(kvp.Decoder): - identifier = kvp.Parameter() - inputs = kvp.Parameter("DataInputs", type=_parse_inputs, num="?", default={}) - outputs = kvp.Parameter("ResponseDocument", type=_parse_outputs, num="?", default=[]) - raw_response = kvp.Parameter("RawDataOutput", type=_parse_raw_output, num="?") - status = kvp.Parameter("status", type=_parse_bool, num="?", default=False) - lineage = kvp.Parameter("lineage", type=_parse_bool, num="?", default=False) - store_response = kvp.Parameter("storeExecuteResponse= ", type=_parse_bool, num="?", default=False) - - @property - def response_form(self): - raw_response = self.raw_response - if raw_response: - return raw_response - - resp_doc = ResponseDocument( - lineage=self.lineage, - status=self.status, - store_response=self.store_response - ) - for output in self.outputs: - resp_doc.set_output(output) - return resp_doc - diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wps/v10/execute_decoder_xml.py eoxserver-0.3.2/eoxserver/services/ows/wps/v10/execute_decoder_xml.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wps/v10/execute_decoder_xml.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wps/v10/execute_decoder_xml.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,179 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Execute - XML decoder -# -# Project: EOxServer -# Authors: Fabian Schindler -# Martin Paces -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -from lxml import etree -from eoxserver.core.decoders import xml -from eoxserver.services.ows.wps.exceptions import InvalidParameterValue -from eoxserver.services.ows.wps.parameters import ( - InputData, InputReference, Output, ResponseDocument, RawDataOutput -) -from eoxserver.services.ows.wps.v10.util import nsmap, ns_wps, ns_ows, ns_xlink - -def _bool(raw_bool): - """ Parse WPS boolean string.""" - return raw_bool == "true" - - -def _parse_input(element): - id_ = element.findtext("./"+ns_ows("Identifier")) - title = element.findtext("./"+ns_ows("Title")) - abstract = element.findtext("./"+ns_ows("Abstract")) - - if id_ is None: - raise ValueError("Missing the mandatory input identifier!") - - elem_ref = element.find("./"+ns_wps("Reference")) - elem_data = element.find("./"+ns_wps("Data")) - - if elem_ref is not None: - value = _parse_input_reference(elem_ref, id_, title, abstract) - - elif elem_data is not None: - if len(elem_data) != 1: - raise ValueError("Invalid input content of the 'wps:Data' element!") - value = _parse_input_data(elem_data[0], id_, title, abstract) - - return id_, value - - -def _parse_response_form(elem_rform): - elem_rdoc = elem_rform.find("./"+ns_wps("ResponseDocument")) - if elem_rdoc is not None: - rdoc = ResponseDocument( - lineage=_bool(elem_rdoc.attrib.get("lineage")), - status=_bool(elem_rdoc.attrib.get("status")), - store_response=_bool(elem_rdoc.attrib.get("storeExecuteResponse")), - ) - for elem in elem_rdoc.iterfind("./"+ns_wps("Output")): - id_ = elem.findtext(ns_ows("Identifier")) - title = elem.findtext(ns_ows("Title")) - abstr = elem.findtext(ns_ows("Abstract")) - rdoc.set_output(_create_output(id_, elem.attrib, title, abstr)) - return rdoc - - elem_rawout = elem_rform.find("./"+ns_wps("RawDataOutput")) - if elem_rawout is not None: - id_ = elem_rawout.findtext(ns_ows("Identifier")) - return RawDataOutput(_create_output(id_, elem_rawout.attrib)) - - raise InvalidParameterValue('Invalid ResponseForm!', 'ResponseForm') - - -def _parse_input_reference(elem, identifier, title, abstract): - href = elem.attrib.get(ns_xlink("href")) - if href is None: - raise ValueError("Missing the mandatory 'xlink:href' attribute!") - - body = elem.findtext("./"+ns_wps("Body")) - elem_tmp = elem.find("./"+ns_wps("BodyReference")) - body_href = elem_tmp.attrib.get(ns_xlink("href")) if elem_tmp else None - - headers = dict( - (header.attrib["key"], header.attrib["value"]) - for header in elem.iterfind("./"+ns_wps("Header")) - ) - - return InputReference( - identifier, title, abstract, - href, headers, body, - elem.attrib.get("method", "GET"), - elem.attrib.get("mimeType"), - elem.attrib.get("encoding"), - elem.attrib.get("schema"), - body_href, - ) - - -def _parse_input_data(elem, identifier, title, abstract): - if elem.tag == ns_wps("LiteralData"): - args = _parse_input_literal(elem) - elif elem.tag == ns_wps("BoundingBoxData"): - args = _parse_input_bbox(elem) - elif elem.tag == ns_wps("ComplexData"): - args = _parse_input_complex(elem) - else: - raise ValueError("Invalid input content of the 'wps:Data' element!") - - return InputData(identifier, title, abstract, **args) - -def _parse_input_literal(elem): - args = {} - args['data'] = elem.text - args['uom'] = elem.attrib.get("uom") - return args - -def _parse_input_bbox(elem): - args = {} - lower_corner = elem.findtext("./"+ns_ows("LowerCorner")) - upper_corner = elem.findtext("./"+ns_ows("UpperCorner")) - if lower_corner is None or upper_corner is None: - raise ValueError("Invalid 'wps:BoundingBoxData' element!") - args['data'] = (lower_corner, upper_corner, elem.attrib.get("crs")) - return args - -def _parse_input_complex(elem): - args = {} - if len(elem): - args['data'] = etree.tostring(elem[0], pretty_print=False, - xml_declaration=True, encoding="utf-8") - else: - args['data'] = elem.text - args['mime_type'] = elem.attrib.get("mimeType") - args['encoding'] = elem.attrib.get("encoding") - args['schema'] = elem.attrib.get("schema") - return args - - -def _create_output(identifier, attrs, title=None, abstract=None): - attr_as_reference = attrs.get("asReference") - if attr_as_reference is not None: - attr_as_reference = _bool(attr_as_reference) - - return Output(identifier, title, abstract, attrs.get("uom"), - attrs.get("crs"), attrs.get("mimeType"), attrs.get("encoding"), - attrs.get("schema"), attr_as_reference) - - -class WPS10ExecuteXMLDecoder(xml.Decoder): - identifier = xml.Parameter("ows:Identifier/text()") - inputs_ = xml.Parameter("wps:DataInputs/wps:Input", type=_parse_input, num="*", default=[]) - _response_form = xml.Parameter("wps:ResponseForm", type=_parse_response_form, num="?") - - @property - def response_form(self): - resp_form = self._response_form - return resp_form if resp_form is not None else ResponseDocument() - - @property - def inputs(self): - return dict(self.inputs_) - - namespaces = nsmap - diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wps/v10/execute.py eoxserver-0.3.2/eoxserver/services/ows/wps/v10/execute.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wps/v10/execute.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wps/v10/execute.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,266 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# Martin Paces -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -try: - # available in Python 2.7+ - from collections import OrderedDict -except ImportError: - from django.utils.datastructures import SortedDict as OrderedDict - -import urllib2 -from urlparse import urlparse -import logging - -from eoxserver.core import Component, implements, ExtensionPoint -from eoxserver.core.util import multiparttools as mp -from eoxserver.services.ows.interfaces import ( - ServiceHandlerInterface, GetServiceHandlerInterface, - PostServiceHandlerInterface -) -from eoxserver.services.ows.wps.interfaces import ProcessInterface -from eoxserver.services.ows.wps.exceptions import ( - NoSuchProcessError, MissingRequiredInputError, - InvalidInputError, InvalidOutputError, InvalidOutputDefError, - InvalidInputReferenceError, InvalidInputValueError, -) -from eoxserver.services.ows.wps.parameters import ( - fix_parameter, LiteralData, BoundingBoxData, ComplexData, - InputReference, InputData, -) -from eoxserver.services.ows.wps.v10.encoders import ( - WPS10ExecuteResponseXMLEncoder, WPS10ExecuteResponseRawEncoder -) -from eoxserver.services.ows.wps.v10.execute_decoder_xml import ( - WPS10ExecuteXMLDecoder -) -from eoxserver.services.ows.wps.v10.execute_decoder_kvp import ( - WPS10ExecuteKVPDecoder -) - -LOGGER = logging.getLogger(__name__) - -class WPS10ExcecuteHandler(Component): - implements(ServiceHandlerInterface) - implements(GetServiceHandlerInterface) - implements(PostServiceHandlerInterface) - - service = "WPS" - versions = ("1.0.0",) - request = "Execute" - - processes = ExtensionPoint(ProcessInterface) - #result_storage = ExtensionPoint(ResultStorageInterface) - - @staticmethod - def get_decoder(request): - if request.method == "GET": - return WPS10ExecuteKVPDecoder(request.GET) - elif request.method == "POST": - # support for multipart items - if request.META["CONTENT_TYPE"].startswith("multipart/"): - _, data = next(mp.iterate(request.body)) - return WPS10ExecuteXMLDecoder(data) - return WPS10ExecuteXMLDecoder(request.body) - - def get_process(self, identifier): - for process in self.processes: - if process.identifier == identifier: - return process - raise NoSuchProcessError(identifier) - - def handle(self, request): - decoder = self.get_decoder(request) - process = self.get_process(decoder.identifier) - input_defs, input_ids = _normalize_params(process.inputs) - output_defs, output_ids = _normalize_params(process.outputs) - raw_inputs = decoder.inputs - resp_form = decoder.response_form - - _check_invalid_inputs(input_ids, raw_inputs) - _check_invalid_outputs(output_ids, resp_form) - - inputs = {} - inputs.update(prepare_process_output_requests(output_defs, resp_form)) - inputs.update(decode_process_inputs(input_defs, raw_inputs, request)) - - outputs = process.execute(**inputs) - - packed_outputs = pack_process_outputs(output_defs, outputs, resp_form) - - if resp_form.raw: - encoder = WPS10ExecuteResponseRawEncoder() - else: - encoder = WPS10ExecuteResponseXMLEncoder() - - response = encoder.encode_response( - process, packed_outputs, resp_form, inputs, raw_inputs) - - return encoder.serialize(response, encoding='utf-8'), encoder.content_type - - -def _normalize_params(param_defs): - if isinstance(param_defs, dict): - param_defs = param_defs.iteritems() - params, param_ids = [], [] - for name, param in param_defs: - param = fix_parameter(name, param) # short-hand def. expansion - param_ids.append(param.identifier) - params.append((name, param)) - return params, param_ids - -def _check_invalid_inputs(input_ids, inputs): - invalids = set(inputs) - set(input_ids) - if len(invalids): - raise InvalidInputError(invalids.pop()) - -def _check_invalid_outputs(output_ids, outputs): - invalids = set(outputs) - set(output_ids) - if len(invalids): - raise InvalidOutputError(invalids.pop()) - - -def decode_process_inputs(input_defs, raw_inputs, request): - """ Iterates over all input options stated in the process and parses - all given inputs. This also includes resolving references - """ - decoded_inputs = {} - for name, prm in input_defs: - raw_value = raw_inputs.get(prm.identifier) - if raw_value is not None: - if isinstance(raw_value, InputReference): - try: - raw_value = _resolve_reference(raw_value, request) - except ValueError as exc: - raise InvalidInputReferenceError(prm.identifier, exc) - try: - value = _decode_input(prm, raw_value) - except ValueError as exc: - raise InvalidInputValueError(prm.identifier, exc) - elif prm.is_optional: - value = getattr(prm, 'default', None) - else: - raise MissingRequiredInputError(prm.identifier) - decoded_inputs[name] = value - return decoded_inputs - -def prepare_process_output_requests(output_defs, response_form): - """ Complex data format selection (mimeType, encoding, schema) - is passed as an input to the process - """ - output_requests = {} - for name, prm in output_defs: - outreq = response_form.get_output(prm.identifier) - if isinstance(prm, ComplexData): - format_ = prm.get_format(outreq.mime_type, outreq.encoding, outreq.schema) - if format_ is None: - raise InvalidOutputDefError(prm.identifier, "Invalid " - "complex data format! mimeType=%r encoding=%r schema=%r" - ""%(outreq.mime_type, outreq.encoding, outreq.schema)) - output_requests[name] = { - "mime_type": format_.mime_type, - "encoding": format_.encoding, - "schema": format_.schema, - } - return output_requests - -def pack_process_outputs(output_defs, results, response_form): - """ Collect data, output declaration and output request for each item.""" - # Normalize the outputs to a dictionary. - if not isinstance(results, dict): - if len(output_defs) == 1: - results = {output_defs[0][0]: results} - else: - results = dict(results) - # Pack the results to a tuple containing: - # - the output data (before encoding) - # - the process output declaration (ProcessDescription/Output) - # - the output's requested form (RequestForm/Output) - packd_results = OrderedDict() - for name, prm in output_defs: - outreq = response_form.get_output(prm.identifier) - result = results.get(name) - # TODO: Can we silently skip the missing outputs? Check the standard! - if result is not None: - packd_results[prm.identifier] = (result, prm, outreq) - elif isinstance(prm, LiteralData) and prm.default is not None: - packd_results[prm.identifier] = (prm.default, prm, outreq) - return packd_results - -def _decode_input(prm, raw_value): - """ Decode raw input and check it agains the allowed values.""" - if isinstance(prm, LiteralData): - return prm.parse(raw_value.data, raw_value.uom) - elif isinstance(prm, BoundingBoxData): - return prm.parse(raw_value.data) - elif isinstance(prm, ComplexData): - return prm.parse(raw_value.data, raw_value.mime_type, - raw_value.schema, raw_value.encoding, urlsafe=raw_value.asurl) - else: - raise TypeError("Unsupported parameter type %s!"%(type(prm))) - -def _resolve_reference(iref, request): - """ Get the input passed as a reference.""" - # prepare HTTP/POST request - if iref.method == "POST": - if iref.body_href is not None: - iref.body = _resolve_url(iref.body_href, None, iref.headers, request) - if iref.body is not None: - ValueError("Missing the POST request body!") - else: - iref.body = None - data = _resolve_url(iref.href, iref.body, iref.headers, request) - return InputData(iref.identifier, iref.title, iref.abstract, data, - None, None, iref.mime_type, iref.encoding, iref.schema) - -def _resolve_url(href, body, headers, request): - """ Resolve the input reference URL.""" - LOGGER.debug("resolving: %s%s", href, "" if body is None else " (POST)") - url = urlparse(href) - if url.scheme == "cid": - return _resolve_multipart_related(url.path, request) - elif url.scheme in ('http', 'https'): - return _resolve_http_url(href, body, headers) - else: - raise ValueError("Unsupported URL scheme %r!"%(url.scheme)) - -def _resolve_http_url(href, body=None, headers=()): - """ Resolve the HTTP request.""" - try: - request = urllib2.Request(href, body, dict(headers)) - response = urllib2.urlopen(request) - return response.read() - except urllib2.URLError, exc: - raise ValueError(str(exc)) - -def _resolve_multipart_related(cid, request): - """ Resolve reference to another part of the multi-part request.""" - # iterate over the parts to find the correct one - for headers, data in mp.iterate(request): - if headers.get("Content-ID") == cid: - return data - raise ValueError("No part with content-id '%s'." % cid) diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wps/v10/getcapabilities.py eoxserver-0.3.2/eoxserver/services/ows/wps/v10/getcapabilities.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wps/v10/getcapabilities.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wps/v10/getcapabilities.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,66 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -from eoxserver.core import Component, ExtensionPoint, implements -from eoxserver.core.decoders import kvp, xml, typelist -from eoxserver.services.ows.interfaces import ( - ServiceHandlerInterface, GetServiceHandlerInterface, - PostServiceHandlerInterface, VersionNegotiationInterface -) -from eoxserver.services.ows.wps.interfaces import ProcessInterface -from eoxserver.services.ows.wps.v10.encoders import WPS10CapabilitiesXMLEncoder -from eoxserver.services.ows.wps.v10.util import nsmap - - -class WPS10GetCapabilitiesHandler(Component): - implements(ServiceHandlerInterface) - implements(GetServiceHandlerInterface) - implements(PostServiceHandlerInterface) - implements(VersionNegotiationInterface) - - service = "WPS" - versions = ("1.0.0",) - request = "GetCapabilities" - - processes = ExtensionPoint(ProcessInterface) - - def handle(self, request): - encoder = WPS10CapabilitiesXMLEncoder() - return encoder.serialize( - encoder.encode_capabilities(self.processes) - ), encoder.content_type - - -class WPS10GetCapabilitiesKVPDecoder(kvp.Decoder): - #acceptversions = kvp.Parameter(type=typelist(str, ","), num="?") - language = kvp.Parameter(num="?") - - -class WPS10GetCapabilitiesXMLDecoder(xml.Decoder): - #acceptversions = xml.Parameter("/ows:AcceptVersions/ows:Version/text()", num="*") - language = xml.Parameter("/ows:AcceptLanguages/ows:Language/text()", num="*") - namespaces = nsmap diff -Nru eoxserver-0.4.0beta2/eoxserver/services/ows/wps/v10/util.py eoxserver-0.3.2/eoxserver/services/ows/wps/v10/util.py --- eoxserver-0.4.0beta2/eoxserver/services/ows/wps/v10/util.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/ows/wps/v10/util.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,45 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -from lxml.builder import ElementMaker - -from eoxserver.core.util.xmltools import NameSpace, NameSpaceMap, ns_xsi -from eoxserver.services.ows.common.v11.encoders import ( - ns_xlink, ns_xml, ns_ows, OWS -) - -# namespace declarations -ns_wps = NameSpace("http://www.opengis.net/wps/1.0.0", "wps", - "http://schemas.opengis.net/wps/1.0.0/wpsAll.xsd") - -# namespace map -nsmap = NameSpaceMap(ns_xlink, ns_xml, ns_ows, ns_wps) - -# Element factories -WPS = ElementMaker(namespace=ns_wps.uri, nsmap=nsmap) -NIL = ElementMaker() # nil-name - diff -Nru eoxserver-0.4.0beta2/eoxserver/services/owscommon.py eoxserver-0.3.2/eoxserver/services/owscommon.py --- eoxserver-0.4.0beta2/eoxserver/services/owscommon.py 1970-01-01 00:00:00.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/owscommon.py 2013-12-10 14:57:00.000000000 +0000 @@ -0,0 +1,656 @@ +#------------------------------------------------------------------------------- +# $Id$ +# +# Project: EOxServer +# Authors: Stephan Krause +# Stephan Meissl +# +#------------------------------------------------------------------------------- +# Copyright (C) 2011 EOX IT Services GmbH +# +# 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 of this Software or works derived from this 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. +#------------------------------------------------------------------------------- + +import logging + +from eoxserver.core.system import System +from eoxserver.core.readers import ConfigReaderInterface +from eoxserver.core.exceptions import ( + ConfigError, ImplementationNotFound, ImplementationDisabled +) +from eoxserver.core.util.xmltools import XMLEncoder +from eoxserver.services.interfaces import ( + RequestHandlerInterface, ExceptionHandlerInterface, + ExceptionEncoderInterface +) +from eoxserver.services.base import ( + BaseRequestHandler, BaseExceptionHandler +) +from eoxserver.services.exceptions import ( + InvalidRequestException, VersionNegotiationException +) + + +logger = logging.getLogger(__name__) + +class OWSCommonHandler(BaseRequestHandler): + """ + This class is the entry point for all incoming OWS requests. + + It tries to determine the service the request and directed to and + invokes the appropriate service handler. An :exc:`~.InvalidRequestException` + is raised if the service is unknown. + + Due to a quirk in WMS where the service parameter is not mandatory, the + WMS service handler is called in the absence of an explicit service + parameter. + """ + REGISTRY_CONF = { + "name": "OWS Common base handler", + "impl_id": "services.owscommon.OWSCommon", + "registry_values": {} + } + + PARAM_SCHEMA = { + "service": {"xml_location": "/@service", "xml_type": "string", "kvp_key": "service", "kvp_type": "string"}, + "version": {"xml_location": "/@version", "xml_type": "string", "kvp_key": "version", "kvp_type": "string"}, + "operation": {"xml_location": "/", "xml_type": "localName", "kvp_key": "request", "kvp_type": "string"} + } + + def _handleException(self, req, exception): + schemas = { + "http://www.opengis.net/ows/2.0": "http://schemas.opengis.net/ows/2.0/owsAll.xsd" + } + return OWSCommonExceptionHandler(schemas).handleException(req, exception) + + def _processRequest(self, req): + req.setSchema(self.PARAM_SCHEMA) + + service = req.getParamValue("service") + + # WMS hack - allowing WMS operation without service identifier + if service is None: + op = req.getParamValue("operation") + if op and op.lower() in ( "getmap" , "getfeatureinfo" , "describelayer" , "getlegendgraphic" , "getstyles" ) : + service = "WMS" + # WMS hack - the end + + if service is None: + raise InvalidRequestException( + "Mandatory 'service' parameter missing.", + "MissingParameterValue", + "service" + ) + + try: + handler = System.getRegistry().findAndBind( + intf_id = "services.interfaces.ServiceHandler", + params = {"services.interfaces.service": service.lower()} + ) + except ImplementationNotFound: + raise InvalidRequestException( + "Service '%s' not supported." % service, + "InvalidParameterValue", + "service" + ) + except ImplementationDisabled: + raise InvalidRequestException( + "Service '%s' disabled." % service, + "InvalidParameterValue", + "service" + ) + + return handler.handle(req) + +OWSCommonHandlerImplementation = RequestHandlerInterface.implement(OWSCommonHandler) + + +class OWSCommonServiceHandler(BaseRequestHandler): + """ + This is the base class for OWS service handlers. It inherits from + :class:`~.BaseRequestHandler`. + + This handler parses the OWS request parameters for a service version. The + version parameter is mandatory for all OGC Web Services and operations + except for the respective "GetCapabilities" calls. So, if the request is + found to be "GetCapabilities" the version negotiation routines are started + in order to determine the actual OWS version handler to be called. + Otherwise the version parameter is read from the request or an + :exc:`~.InvalidRequestException` is raised if it is absent or relates to + an unknown or disabled version of the service. + + Version negotiation is implemented along the lines of OWS Common 2.0. This + means, the handler checks for the presence of an AcceptVersions parameter. + If it is present new-style version negotiation is triggered and old-style + version negotiation otherwise. + + New-style version negotiation will take the first version defined + in the AcceptVersion parameter that is implemented and raise an exception + if none of the versions is known. The version parameter is always + ignored. + + Old-style version negotiation will look for the version parameter and + chose the version indicated if it is implemented. If the version parameter + is lacking the highest implemented version of the service will be selected. + If the version parameter is present but refers to a version that is not + implemented, the highest version lower than that is selected. If that fails, + too, the lowest implemented version will be selected. + + Note that OWS Common 2.0 refers to old-style version negotiation as + deprecated and includes it only for backwards compatibility. But for + EOxServer which exhibits OWS versions relying on OWS Common as well as + versions prior to it, the fallback to old-style version negotiation is + always required. Binding to older versions would otherwise not be + possible. + """ + + SERVICE = "" + + PARAM_SCHEMA = { + "service": {"xml_location": "/@service", "xml_type": "string", "kvp_key": "service", "kvp_type": "string"}, + "version": {"xml_location": "/@version", "xml_type": "string", "kvp_key": "version", "kvp_type": "string"}, + "operation": {"xml_location": "/", "xml_type": "localName", "kvp_key": "request", "kvp_type": "string"}, + "acceptversions": {"xml_location": "/{http://www.opengis.net/ows/2.0}AcceptVersions/{http://www.opengis.net/ows/2.0}Version", "xml_type": "string[]", "kvp_key": "acceptversions", "kvp_type": "stringlist"} + } + + def _handleException(self, req, exception): + schemas = { + "http://www.opengis.net/ows/2.0": "http://schemas.opengis.net/ows/2.0/owsAll.xsd" + } + return OWSCommonExceptionHandler(schemas).handleException(req, exception) + + def _normalizeVersion(self, input_version): + if input_version is not None: + version_numbers = input_version.split(".") + + for version_number in version_numbers: + try: + int(version_number) + except: + raise InvalidRequestException( + "'%s' is not a valid OWS version identifier." % input_version, + "InvalidParameterValue", + "version" + ) + + if len(version_numbers) > 3: + raise InvalidRequestException( + "'%s' is not a valid OWS version identifier." % input_version, + "InvalidParameterValue", + "version" + ) + elif len(version_numbers) == 3: + return input_version + elif len(version_numbers) == 2: + return "%s.0" % input_version + elif len(version_numbers) == 1: + return "%s.0.0" % input_version + + def _versionSupported(self, version): + versions = System.getRegistry().getRegistryValues( + intf_id = "services.interfaces.VersionHandler", + registry_key = "services.interfaces.version", + filter = {"services.interfaces.service": self.SERVICE} + ) + + logger.debug("OWSCommonServiceHandler._versionSupported(): versions: %s" % str(versions)) + + return version in versions + + def _convertVersionNumber(self, version): + version_list = [int(i) for i in version.split(".")] + version_value = 0 + for i in range(0, min(3, len(version_list))): + version_value = version_value + version_list[i] * (100**(2-i)) + + return version_value + + def _getHighestVersion(self, lower_than=None): + versions = System.getRegistry().getRegistryValues( + intf_id = "services.interfaces.VersionHandler", + registry_key = "services.interfaces.version", + filter = {"services.interfaces.service": self.SERVICE} + ) + + max_version = "" + + for version in versions: + if max_version: + if lower_than and self._convertVersionNumber(version) < self._convertVersionNumber(lower_than) or not lower_than: + if self._convertVersionNumber(version) > self._convertVersionNumber(max_version): + max_version = version + else: + max_version = version + + return max_version + + def _getLowestVersion(self): + versions = System.getRegistry().getRegistryValues( + intf_id = "services.interfaces.VersionHandler", + registry_key = "services.interfaces.version", + filter = {"services.interfaces.service": self.SERVICE} + ) + + min_version = "" + + for version in versions: + if min_version: + if self._convertVersionNumber(version) < self._convertVersionNumber(min_version): + min_version = version + else: + min_version = version + + return min_version + + def _negotiateVersionOldStyle(self, input_version): + if input_version is None: + return self._getHighestVersion() + else: + nversion = self._normalizeVersion(input_version) + + if self._versionSupported(nversion): + return nversion + else: + highest_version = self._getHighestVersion(lower_than=nversion) + + if highest_version is not None: + return highest_version + else: + return self._getLowestVersion() + + def _negotiateVersionOWSCommon(self, accept_versions): + for accept_version in accept_versions: + if self._versionSupported(accept_version): + return accept_version + + raise VersionNegotiationException("Version negotiation failed! Highest supported version: %s; Lowest supported version: %s" % (self._getHighestVersion(), self._getLowestVersion())) + + def _processRequest(self, req): + req.setSchema(self.PARAM_SCHEMA) + + input_version = req.getParamValue("version") + operation = req.getParamValue("operation") + + if operation is None: + raise InvalidRequestException( + "Missing 'request' parameter", + "MissingParameterValue", + "request" + ) + elif operation.lower() == "getcapabilities": + accept_versions = req.getParamValue("acceptversions") + + if accept_versions: + version = self._negotiateVersionOWSCommon(accept_versions) + else: + version = self._negotiateVersionOldStyle(input_version) + else: + if input_version is None: + raise InvalidRequestException( + "Missing mandatory 'version' parameter", + "MissingParameterValue", + "version" + ) + else: + version = input_version + + req.setVersion(version) + + try: + handler = System.getRegistry().findAndBind( + intf_id = "services.interfaces.VersionHandler", + params = { + "services.interfaces.service": self.SERVICE, + "services.interfaces.version": version + } + ) + except ImplementationNotFound: + raise InvalidRequestException( + "Service '%s', version '%s' not supported." % (self.SERVICE, version), + "InvalidParameterValue", + "version" + ) + + return handler.handle(req) + + +class OWSCommonVersionHandler(BaseRequestHandler): + """ + This is the base class for OWS version handlers. It inherits from + :class:`~.BaseRequestHandler`. + + Based on the value of the request parameter, the appropriate operation + handler is chosen and invoked. An :exc:`~.InvalidRequestException` is + raised if the operation name is unknown or disabled. + + This class implements exception handling behaviour which is + common across the operations of each OWS version but not among + different versions of the same service. + """ + + SERVICE = "" + VERSION = "" + + PARAM_SCHEMA = { + "service": {"xml_location": "/@service", "xml_type": "string", "kvp_key": "service", "kvp_type": "string"}, + "version": {"xml_location": "/@version", "xml_type": "string", "kvp_key": "version", "kvp_type": "string"}, + "operation": {"xml_location": "/", "xml_type": "localName", "kvp_key": "request", "kvp_type": "string"} + } + + def _handleException(self, req, exception): + schemas = { + "http://www.opengis.net/ows/2.0": "http://schemas.opengis.net/ows/2.0/owsAll.xsd" + } + return OWSCommonExceptionHandler(schemas).handleException(req, exception) + + def _processRequest(self, req): + req.setSchema(self.PARAM_SCHEMA) + + version = req.getVersion() + operation = req.getParamValue("operation") + if operation is None: + raise InvalidRequestException( + "Mandatory 'request' parameter missing.", + "MissingParameterValue", + "request" + ) + + try: + handler = System.getRegistry().findAndBind( + intf_id = "services.interfaces.OperationHandler", + params = { + "services.interfaces.service": self.SERVICE, + "services.interfaces.version": version, + "services.interfaces.operation": operation.lower() + } + ) + except ImplementationNotFound: + raise InvalidRequestException( + "Service '%s', version '%s' does not support operation '%s'." % ( + self.SERVICE, version, operation + ), + "OperationNotSupported", + operation + ) + else: + return handler.handle(req) + + +class OWSCommonExceptionHandler(BaseExceptionHandler): + """ + This exception handler is intended for OWS Common 2.0 based exception + reports. Said standard defines a framework for exception reports that can + be extended by individual OWS standards with additional error codes, for + instance. + + This class inherits from :class:`~.BaseExceptionHandler`. + """ + REGISTRY_CONF = { + "name": "OWS Common Exception Handler", + "impl_id": "services.owscommon.OWSCommonExceptionHandler", + "registry_values": { + "services.interfaces.exception_scheme": "owscommon_2.0" + } + } + + OWS_COMMON_HTTP_STATUS_CODES = { + "_default": 400, + "OperationNotSupported": 501, + "OptionNotSupported": 501, + "NoApplicableCode": 500 + } + + def __init__(self, *args): + super(OWSCommonExceptionHandler, self).__init__(*args) + self.additional_http_status_codes = {} + + def setHTTPStatusCodes(self, additional_http_status_codes): + """ + In OWS Common 2.0 the HTTP status codes for exception reports can + differ depending on the error code. There are several exceptions + listed in the standard itself, but more can be added by OWS + standards relying on OWS Common 2.0. + + This method allows to configure the exception handler with + a dictionary of additional codes. The dictionary keys shall contain + the OWS error codes and the values the corresponding HTTP status + codes as integers. + """ + self.additional_http_status_codes = additional_http_status_codes + + def _filterExceptions(self, exception): + if not isinstance(exception, InvalidRequestException) and \ + not isinstance(exception, VersionNegotiationException): + raise + + def _getEncoder(self): + return OWSCommonExceptionEncoder(self.schemas) + + def _getHTTPStatus(self, exception): + if isinstance(exception, InvalidRequestException): + exception_code = exception.error_code + + if exception_code in self.OWS_COMMON_HTTP_STATUS_CODES: + return self.OWS_COMMON_HTTP_STATUS_CODES[exception_code] + elif exception_code in self.additional_http_status_codes: + return self.additional_http_status_codes[exception_code] + else: + return self.OWS_COMMON_HTTP_STATUS_CODES["_default"] + elif isinstance(exception, VersionNegotiationException): + return 400 + else: + return 500 + + def _getContentType(self, exception): + return "text/xml" + +OWSCommonExceptionHandlerImplementation = ExceptionHandlerInterface.implement(OWSCommonExceptionHandler) + + +class OWSCommon11ExceptionHandler(BaseExceptionHandler): + """ + This exception handler is intended for OWS Common 1.1 based exception + reports. Said standard defines a framework for exception reports that can + be extended by individual OWS standards with additional error codes, for + instance. + + This class inherits from :class:`~.BaseExceptionHandler`. + """ + REGISTRY_CONF = { + "name": "OWS Common Exception Handler", + "impl_id": "services.owscommon.OWSCommon11ExceptionHandler", + "registry_values": { + "services.interfaces.exception_scheme": "owscommon_1.1" + } + } + + def __init__(self, schemas, version): + super(OWSCommon11ExceptionHandler, self).__init__(schemas) + self.version = version + + def _filterExceptions(self, exception): + if not isinstance(exception, InvalidRequestException) and \ + not isinstance(exception, VersionNegotiationException): + raise + + def _getEncoder(self): + return OWSCommon11ExceptionEncoder(self.schemas, self.version) + + def _getHTTPStatus(self, exception): + if isinstance(exception, (InvalidRequestException, VersionNegotiationException)): + return 400 + else: + return 500 + + def _getContentType(self, exception): + return "text/xml" + +OWSCommon11ExceptionHandlerImplementation = ExceptionHandlerInterface.implement(OWSCommon11ExceptionHandler) + + +class OWSCommonExceptionEncoder(XMLEncoder): + """ + Encoder for OWS Common 2.0 compliant exception reports. Implements + :class:`~.ExceptionEncoderInterface`. + """ + REGISTRY_CONF = { + "name": "OWS Common 2.0 Exception Report Encoder", + "impl_id": "services.owscommon.OWSCommonExceptionEncoder", + "registry_values": { + "services.interfaces.exception_scheme": "owscommon_2.0" + } + } + + def _initializeNamespaces(self): + return { + "ows": "http://www.opengis.net/ows/2.0", + "xsi": "http://www.w3.org/2001/XMLSchema-instance" + } + + def encodeExceptionReport(self, exception_text, exception_code, locator=None): + if locator is None: + element = self._makeElement("ows", "ExceptionReport", [ + ("", "@version", "2.0.0"), + ("", "@xml:lang", "en"), + ("ows", "Exception", [ + ("", "@exceptionCode", exception_code), + ("ows", "ExceptionText", exception_text) + ]) + ]) + else: + element = self._makeElement("ows", "ExceptionReport", [ + ("", "@version", "2.0.0"), + ("", "@xml:lang", "en"), + ("ows", "Exception", [ + ("", "@exceptionCode", exception_code), + ("", "@locator", locator), + ("ows", "ExceptionText", exception_text) + ]) + ]) + + if self.schemas is not None: + schemas_location = " ".join(["%s %s"%(ns, location) for ns, location in self.schemas.iteritems()]) + element.setAttributeNS(self.ns_dict["xsi"], "%s:%s" % ("xsi", "schemaLocation"), schemas_location) + + return element + + def encodeInvalidRequestException(self, exception): + return self.encodeExceptionReport(exception.msg, exception.error_code, exception.locator) + + def encodeVersionNegotiationException(self, exception): + return self.encodeExceptionReport(exception.msg, "VersionNegotiationFailed") + + def encodeException(self, exception): + return self.encodeExceptionReport("Internal Server Error", "NoApplicableCode") + +OWSCommonExceptionEncoderImplementation = ExceptionEncoderInterface.implement(OWSCommonExceptionEncoder) + + +class OWSCommon11ExceptionEncoder(XMLEncoder): + """ + Encoder for OWS Common 1.1 compliant exception reports. Implements + :class:`~.ExceptionEncoderInterface`. + """ + REGISTRY_CONF = { + "name": "OWS Common 1.1 Exception Report Encoder", + "impl_id": "services.owscommon.OWSCommon11ExceptionEncoder", + "registry_values": { + "services.interfaces.exception_scheme": "owscommon_1.1" + } + } + + def __init__(self, schemas=None, version=None): + super(OWSCommon11ExceptionEncoder, self).__init__(schemas) + self.version = version or "1.1.0" + + def _initializeNamespaces(self): + return { + "ows": "http://www.opengis.net/ows/1.1", + "xsi": "http://www.w3.org/2001/XMLSchema-instance" + } + + def encodeExceptionReport(self, exception_text, exception_code, locator=None): + if locator is None: + element = self._makeElement("ows", "ExceptionReport", [ + ("", "@version", self.version), + ("", "@xml:lang", "en"), + ("ows", "Exception", [ + ("", "@exceptionCode", exception_code), + ("ows", "ExceptionText", exception_text) + ]) + ]) + else: + element = self._makeElement("ows", "ExceptionReport", [ + ("", "@version", self.version), + ("", "@xml:lang", "en"), + ("ows", "Exception", [ + ("", "@exceptionCode", exception_code), + ("", "@locator", locator), + ("ows", "ExceptionText", exception_text) + ]) + ]) + + if self.schemas is not None: + schemas_location = " ".join(["%s %s"%(ns, location) for ns, location in self.schemas.iteritems()]) + element.setAttributeNS(self.ns_dict["xsi"], "%s:%s" % ("xsi", "schemaLocation"), schemas_location) + + return element + + def encodeInvalidRequestException(self, exception): + return self.encodeExceptionReport(exception.msg, exception.error_code, exception.locator) + + def encodeVersionNegotiationException(self, exception): + return self.encodeExceptionReport(exception.msg, "VersionNegotiationFailed") + + def encodeException(self, exception): + return self.encodeExceptionReport("Internal Server Error", "NoApplicableCode") + +OWSCommon11ExceptionEncoderImplementation = ExceptionEncoderInterface.implement(OWSCommon11ExceptionEncoder) + + +class OWSCommonConfigReader(object): + """ + This class implements the :class:`~.ConfigReaderInterface`. It provides + convenience functions for reading OWS related settings from the instance + configuration. + """ + REGISTRY_CONF = { + "name": "OWS Common Config Reader", + "impl_id": "services.owscommon.OWSCommonConfigReader", + "registry_values": {} + } + + def validate(self, config): + """ + Raises :exc:`~.ConfigError` if the mandatory ``http_service_url`` + setting is missing in the ``services.owscommon`` section of the + instance configuration. + """ + if config.getInstanceConfigValue("services.owscommon", "http_service_url") is None: + raise ConfigError("Missing mandatory 'http_service_url' parameter") + + def getHTTPServiceURL(self): + """ + Returns the value of the `http_service_url`` in the + ``services.owscommon`` section. This is used for reporting the + correct service address in the OWS capabilities. + """ + return System.getConfig().getInstanceConfigValue("services.owscommon", "http_service_url") + +OWSCommonConfigReaderImplementation = ConfigReaderInterface.implement(OWSCommonConfigReader) diff -Nru eoxserver-0.4.0beta2/eoxserver/services/parameters.py eoxserver-0.3.2/eoxserver/services/parameters.py --- eoxserver-0.4.0beta2/eoxserver/services/parameters.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/parameters.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,89 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -class RenderParameters(object): - """ Abstract base class for render parameters - """ - - def __iter__(self): - """ Yields all kvps as key-value pairs (string, string). - """ - - -class VersionedParams(object): - def __init__(self, version): - self._version = version - - @property - def version(self): - return self._version - - def __iter__(self): - yield ("version", self.version) - - -class CapabilitiesRenderParams(object): - def __init__(self, coverages, version, sections=None, accept_languages=None, - accept_formats=None, updatesequence=None, request=None): - self._coverages = coverages - self._version = version - self._sections = sections or () - self._accept_languages = accept_languages or () - self._accept_formats = accept_formats or () - self._updatesequence = updatesequence - self._request = request - - coverages = property(lambda self: self._coverages) - version = property(lambda self: self._version) - sections = property(lambda self: self._sections) - accept_languages = property(lambda self: self._accept_languages) - accept_formats = property(lambda self: self._accept_formats) - updatesequence = property(lambda self: self._updatesequence) - - @property - def request(self): - return self._request - - @request.setter - def request(self, value): - self._request = value - - def __iter__(self): - yield "request", "GetCapabilities" - - if self.sections: - yield "sections", ",".join(self.sections) - - if self.accept_languages: - yield "acceptlanguages", ",".join(self.accept_languages) - - if self.accept_formats: - yield "acceptformats", ",".join(self.accept_formats) - - if self.updatesequence: - yield "updatesequence", self.updatesequence diff -Nru eoxserver-0.4.0beta2/eoxserver/services/requests.py eoxserver-0.3.2/eoxserver/services/requests.py --- eoxserver-0.4.0beta2/eoxserver/services/requests.py 1970-01-01 00:00:00.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/requests.py 2013-12-10 14:57:00.000000000 +0000 @@ -0,0 +1,188 @@ +#------------------------------------------------------------------------------- +# $Id$ +# +# Project: EOxServer +# Authors: Stephan Krause +# Stephan Meissl +# +#------------------------------------------------------------------------------- +# Copyright (C) 2011 EOX IT Services GmbH +# +# 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 of this Software or works derived from this 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. +#------------------------------------------------------------------------------- + +""" +This module defines basic classes for OWS requests and responses to +OWS requests. +""" + +from eoxserver.core.util.xmltools import XMLDecoder +from eoxserver.core.util.kvptools import KVPDecoder + +class OWSRequest(object): + """ + This class is used to encapsulate information about an OWS request. + + The constructor expects one required parameter, a Django + :class:`~django.http.HttpRequest` object ``http_req``. + + The ``params`` argument shall contain the parameters sent with the + request. For GET requests, this can either contain a Django + :class:`~django.http.QueryDict` object or the query string itself. For POST requests, + the argument shall contain the message body as a string. + + The ``param_type`` argument shall be set to ``kvp`` for GET + requests and ``xml`` for POST requests. + + Optionally, a decoder (either a :class:`~.KVPDecoder` or + :class:`~.XMLDecoder` instance initialized with the parameters) can already + be conveyed to the request. If it is not present, the appropriate decoder + type will be chosen and initialized based on the values of ``params`` and + ``param_type``. + """ + def __init__(self, http_req, params='', param_type='kvp', decoder=None): + super(OWSRequest, self).__init__() + + self.http_req = http_req + + self.params = params + + if decoder is None: + if param_type == 'kvp': + self.decoder = KVPDecoder(params) + elif param_type == 'xml': + self.decoder = XMLDecoder(params) + else: + self.decoder = decoder + + self.version = "" + + self.coverages = [] + + def setSchema(self, schema): + """ + Set the decoding schema for the parameter decoder (see + :mod:`eoxserver.core.util.decoders`) + """ + self.decoder.setSchema(schema) + + def getParamValue(self, key, default=None): + """ + Returns the value of a parameter named ``key``. The name relates to + the schema set for the decoder. You can provide a default value which + will be returned if the parameter is not present. + """ + return self.decoder.getValue(key, default) + + def getParamValueStrict(self, key): + """ + Returns the value of a parameter named ``key``. The name relates to + the schema set for the decoder. A :exc:`~.DecoderException` will be + raised if the parameter is not present. + """ + return self.decoder.getValueStrict(key) + + def getParams(self): + """ + Returns the parameters. This method calls the :class:`~.KVPDecoder` or + :class:`~.XMLDecoder` method of the same name. In case of KVP data, + this means that a dictionary with the parameter values will be returned + instead of the query string, even if the :class:`OWSRequest` object + was initially configured with the query string. + """ + return self.decoder.getParams() + + def getParamType(self): + """ + Returns ``kvp`` or ``xml``. + """ + return self.decoder.getParamType() + + def setVersion(self, version): + """ + Sets the version for the OGC Web Service. This method is used + for version negotiation, in which case the appropriate version cannot + simply be read from the request parameters. + """ + self.version = version + + def getVersion(self): + """ + Returns the version for the OGC Web Service. This method is used + for version negotiation, in which case the appropriate version cannot + simply be read from the request parameters. + """ + return self.version + + def getHeader(self, header_name): + """ + Returns the value of the HTTP header ``header_name``, or ``None`` if not + found. + """ + META_WITHOUT_HTTP = ( + "CONTENT_LENGTH", + "CONTENT_TYPE", + "QUERY_STRING", + "REMOTE_ADDR", + "REMOTE_HOST", + "REMOTE_USER", + "REQUEST_METHOD", + "SERVER_NAME", + "SERVER_PORT" + ) + + tmp = header_name.upper().replace('-', '_') + + if tmp in META_WITHOUT_HTTP: + header_key = tmp + else: + header_key = "HTTP_%s" % tmp + + return self.http_req.META.get(header_key) + + +class Response(object): + """ + This class encapsulates the data needed for an HTTP response to an OWS + request. + + The ``content`` argument contains the content of the response message. The + ``content_type`` argument is set to the MIME type of the response content. + The ``headers`` argument is expected to be a dictionary of additional + HTTP headers to be sent with the response. The ``status`` parameter is + used to set the HTTP status of the response. + """ + def __init__(self, content='', content_type='text/xml', headers={}, status=None): + super(Response, self).__init__() + self.content = content + self.content_type = content_type + self.headers = headers + self.status = status + + def getContent(self): + return self.content + + def getContentType(self): + return self.content_type + + def getHeaders(self): + return self.headers + + def getStatus(self): + return self.status diff -Nru eoxserver-0.4.0beta2/eoxserver/services/result.py eoxserver-0.3.2/eoxserver/services/result.py --- eoxserver-0.4.0beta2/eoxserver/services/result.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/result.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,284 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -import os -import os.path -from cStringIO import StringIO -from uuid import uuid4 - -from django.http import HttpResponse - -from eoxserver.core.util import multiparttools as mp - - -class ResultItem(object): - """ Base class (or interface) for result items of a result set. - - :param content_type: the content type of the result item. in HTTP this will - be translated to the ``Content-Type`` header - :param filename: the filename of the result item. - :param identifier: the identifier of the result item. translated to - ``Content-Id`` HTTP header - """ - - def __init__(self, content_type=None, filename=None, identifier=None): - self.content_type = content_type - self.filename = filename - self.identifier = identifier - - @property - def data(self): - """ Returns the "raw" data, usually as a string, buffer, memoryview, - etc. - """ - return "" - - @property - def data_file(self): - """ Returns the data as a Python file-like object. - """ - return StringIO("") - - def __len__(self): - """ Unified access to size of data. - """ - raise NotImplementedError - - size = property(lambda self: len(self)) - - def chunked(self, chunksize): - """ Returns a chunk of the data, which has at most ``chunksize`` bytes. - """ - yield "" - - def delete(self): - """ Cleanup any associated files, allocated memory, etc. - """ - pass - - -class ResultFile(ResultItem): - """ Class for results that wrap physical files on the disc. - """ - - def __init__(self, path, content_type=None, filename=None, identifier=None): - super(ResultFile, self).__init__(content_type, filename, identifier) - self.path = path - - @property - def data(self): - with open(self.path) as f: - return f.read() - - @property - def data_file(self): - return open(self.path) - - def __len__(self): - return os.path.getsize(self.path) - - def chunked(self, chunksize): - with open(self.path) as f: - while True: - data = f.read(chunksize) - if not data: - break - - yield data - - def delete(self): - os.remove(self.path) - - -class ResultBuffer(ResultItem): - """ Class for results that are actually a subset of a larger context. - Usually a buffer. - """ - - def __init__(self, buf, content_type=None, filename=None, identifier=None): - super(ResultBuffer, self).__init__(content_type, filename, identifier) - self.buf = buf - - @property - def data(self): - return self.buf - - @property - def data_file(self): - return StringIO(self.buf) - - def __len__(self): - return len(self.buf) - - def chunked(self, chunksize): - if chunksize < 0: - raise ValueError - - size = len(self.buf) - if chunksize >= size: - yield self.buf - return - - i = 0 - while i < size: - yield self.buf[i:i+chunksize] - i += chunksize - - -def get_content_type(result_set): - """ Returns the content type of a result set. If only one item is included - its content type is used, otherwise the constant "multipart/related". - """ - if len(result_set) == 1: - return result_set[0].content_type - else: - return "multipart/related" - - -def get_headers(result_item): - """ Yields content headers, if they are set in the result item. - """ - yield "Content-Type", result_item.content_type or "application/octet-stream" - if result_item.identifier: - yield "Content-Id", result_item.identifier - if result_item.filename: - yield ( - "Content-Disposition", 'attachment; filename="%s"' - % result_item.filename - ) - try: - yield "Content-Length", len(result_item) - except (AttributeError, NotImplementedError): - pass - - -def to_http_response(result_set, response_type=HttpResponse, boundary=None): - """ Returns a response for a given result set. The ``response_type`` is the - class to be used. It must be capable to work with iterators. This - function is also responsible to delete any temporary files and buffers - of the ``result_set``. - - :param result_set: an iterable of objects following the - :class:`ResultItem` interface - :param response_type: the response type class to use; defaults to - :class:`HttpResponse `. - For streaming responses use - :class:`StreamingHttpResponse - ` - :param boundary: the multipart boundary; if omitted a UUID hex string is - computed and used - :returns: a response object of the desired type - """ - - # if more than one item is contained in the result set, the content type is - # multipart - if len(result_set) > 1: - boundary = boundary or uuid4().hex - content_type = "multipart/related; boundary=%s" % boundary - headers = () - - # otherwise, the content type is the content type of the first included item - else: - boundary = None - content_type = result_set[0].content_type or "application/octet-stream" - headers = tuple(get_headers(result_set[0])) - - def response_iterator(items, boundary=None): - try: - if boundary: - boundary_str = "%s--%s%s" % (mp.CRLF, boundary, mp.CRLF) - boundary_str_end = "%s--%s--" % (mp.CRLF, boundary) - - for item in items: - if boundary: - yield boundary_str - yield mp.CRLF.join( - "%s: %s" % (key, value) - for key, value in get_headers(item) - ) + mp.CRLFCRLF - yield item.data - if boundary: - yield boundary_str_end - finally: - for item in items: - try: - item.delete() - except: - pass # bad exception swallowing... - - # workaround for bug in django, that does not consume iterator in tests. - if response_type == HttpResponse: - response = response_type( - list(response_iterator(result_set, boundary)), - content_type - ) - else: - response = response_type( - response_iterator(result_set, boundary), content_type - ) - - # set any headers - for key, value in headers: - response[key] = value - - return response - - -def parse_headers(headers): - """ Convenience function to read the "Content-Type", "Content-Disposition" - and "Content-Id" headers. - - :param headers: the raw header :class:`dict` - """ - content_type = headers.get("Content-Type", "application/octet-stream") - _, params = mp.parse_parametrized_option( - headers.get("Content-Disposition", "") - ) - filename = params.get("filename") - if filename: - if filename.startswith('"'): - filename = filename[1:] - if filename.endswith('"'): - filename = filename[:-1] - - identifier = headers.get("Content-Id") - return content_type, filename, identifier - - -def result_set_from_raw_data(data): - """ Create a result set from raw HTTP data. This can either be a single - or a multipart string. It returns a list containing objects of the - :class:`ResultBuffer` type that reference substrings of the given data. - - :param data: the raw byte data - :returns: a result set: a list containing :class:`ResultBuffer` - """ - return [ - ResultBuffer(d, *parse_headers(headers)) - for headers, d in mp.iterate(data) - if not headers.get("Content-Type").startswith("multipart") - ] diff -Nru eoxserver-0.4.0beta2/eoxserver/services/subset.py eoxserver-0.3.2/eoxserver/services/subset.py --- eoxserver-0.4.0beta2/eoxserver/services/subset.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/subset.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,535 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - - -import logging - -from django.contrib.gis.geos import Polygon, LineString - -from eoxserver.resources.coverages import crss -from eoxserver.services.exceptions import ( - InvalidAxisLabelException, InvalidSubsettingException, - InvalidSubsettingCrsException -) - - -__all__ = ["Subsets", "Trim", "Slice"] - -logger = logging.getLogger(__name__) - - -class Subsets(list): - """ Convenience class to handle a variety of spatial and/or temporal - subsets. - - :param iterable: an iterable of objects inheriting from :class:`Trim` - or :class:`Slice` - :param crs: the CRS definition - :param allowed_types: the allowed subset types. defaults to both - :class:`Trim` and :class:`Slice` - """ - - def __init__(self, iterable, crs=None, allowed_types=None): - """ Constructor. Allows to add set the initial subsets - """ - self.allowed_types = allowed_types if allowed_types is not None else ( - Trim, Slice - ) - # Do a manual insertion here to assure integrity - for subset in iterable: - self.append(subset) - - self._crs = crs - - # List API - - def extend(self, iterable): - """ See :meth:`list.extend` """ - for subset in iterable: - self._check_subset(subset) - super(Subsets, self).append(subset) - - def append(self, subset): - """ See :meth:`list.append` """ - self._check_subset(subset) - super(Subsets, self).append(subset) - - def insert(self, i, subset): - """ See :meth:`list.insert` """ - self._check_subset(subset) - super(Subsets, self).insert(i, subset) - - # Subset related stuff - - @property - def has_x(self): - """ Check if a subset along the X-axis is given. """ - return any(map(lambda s: s.is_x, self)) - - @property - def has_y(self): - """ Check if a subset along the Y-axis is given. """ - return any(map(lambda s: s.is_y, self)) - - @property - def has_t(self): - """ Check if a subset along the temporal axis is given. """ - return any(map(lambda s: s.is_temporal, self)) - - @property - def crs(self): - """ Return the subset CRS definiton. """ - return self._crs - - @crs.setter - def crs(self, value): - """ Set the subset CRS definiton. """ - self._crs = value - - @property - def srid(self): - """ Tries to find the correct integer SRID for the crs. - """ - crs = self.crs - if crs is not None: - srid = crss.parseEPSGCode(crs, - (crss.fromURL, crss.fromURN, crss.fromShortCode) - ) - if srid is None and not crss.is_image_crs(crs): - raise InvalidSubsettingCrsException( - "Could not parse EPSG code from URI '%s'" % crs - ) - return srid - return None - - def filter(self, queryset, containment="overlaps"): - """ Filter a :class:`Django QuerySet ` - of objects inheriting from :class:`EOObject - `. - - :param queryset: the ``QuerySet`` to filter - :param containment: either "overlaps" or "contains" - :returns: a ``QuerySet`` with additional filters applied - """ - if not len(self): - return queryset - - qs = queryset - - bbox = [None, None, None, None] - srid = self.srid - - if srid is None: - srid = 4326 - max_extent = crss.crs_bounds(srid) - tolerance = crss.crs_tolerance(srid) - - for subset in self: - if isinstance(subset, Slice): - is_slice = True - value = subset.value - elif isinstance(subset, Trim): - is_slice = False - low = subset.low - high = subset.high - - if subset.is_temporal: - if is_slice: - qs = qs.filter( - begin_time__lte=value, - end_time__gte=value - ) - - else: - if high is not None: - qs = qs.filter( - begin_time__lte=high - ) - if low is not None: - qs = qs.filter( - end_time__gte=low - ) - - # check if the temporal bounds must be strictly contained - if containment == "contains": - if high is not None: - qs = qs.filter( - end_time__lte=high - ) - if low is not None: - qs = qs.filter( - begin_time__gte=low - ) - - else: - if is_slice: - if subset.is_x: - line = LineString( - (value, max_extent[1]), - (value, max_extent[3]) - ) - else: - line = LineString( - (max_extent[0], value), - (max_extent[2], value) - ) - line.srid = srid - if srid != 4326: - line.transform(4326) - qs = qs.filter(footprint__intersects=line) - - else: - if subset.is_x: - bbox[0] = subset.low - bbox[2] = subset.high - else: - bbox[1] = subset.low - bbox[3] = subset.high - - if bbox != [None, None, None, None]: - bbox = map( - lambda v: v[0] if v[0] is not None else v[1], - zip(bbox, max_extent) - ) - - bbox[0] -= tolerance - bbox[1] -= tolerance - bbox[2] += tolerance - bbox[3] += tolerance - - logger.debug( - "Applying BBox %s with containment '%s'." % (bbox, containment) - ) - - poly = Polygon.from_bbox(bbox) - poly.srid = srid - - if srid != 4326: - poly.transform(4326) - if containment == "overlaps": - qs = qs.filter(footprint__intersects=poly) - elif containment == "contains": - qs = qs.filter(footprint__within=poly) - - return qs - - def matches(self, eo_object, containment="overlaps"): - """ Check if the given :class:`EOObject - ` matches the given - subsets. - - :param eo_object: the ``EOObject`` to match - :param containment: either "overlaps" or "contains" - :returns: a boolean value indicating if the object is contained in the - given subsets - """ - if not len(self): - return True - - bbox = [None, None, None, None] - srid = self.srid - if srid is None: - srid = 4326 - max_extent = crss.crs_bounds(srid) - tolerance = crss.crs_tolerance(srid) - - footprint = eo_object.footprint - begin_time = eo_object.begin_time - end_time = eo_object.end_time - - for subset in self: - if isinstance(subset, Slice): - is_slice = True - value = subset.value - elif isinstance(subset, Trim): - is_slice = False - low = subset.low - high = subset.high - - if subset.is_temporal: - if is_slice: - if begin_time > value or end_time < value: - return False - elif low is None and high is not None: - if begin_time > high: - return False - elif low is not None and high is None: - if end_time < low: - return False - else: - if begin_time > high or end_time < low: - return False - - else: - if is_slice: - if subset.is_x: - line = LineString( - (value, max_extent[1]), - (value, max_extent[3]) - ) - else: - line = LineString( - (max_extent[0], value), - (max_extent[2], value) - ) - line.srid = srid - if srid != 4326: - line.transform(4326) - - if not line.intersects(footprint): - return False - - else: - if subset.is_x: - bbox[0] = subset.low - bbox[2] = subset.high - else: - bbox[1] = subset.low - bbox[3] = subset.high - - if bbox != [None, None, None, None]: - bbox = map( - lambda v: v[0] if v[0] is not None else v[1], - zip(bbox, max_extent) - ) - - bbox[0] -= tolerance - bbox[1] -= tolerance - bbox[2] += tolerance - bbox[3] += tolerance - - logger.debug( - "Applying BBox %s with containment '%s'." % (bbox, containment) - ) - - poly = Polygon.from_bbox(bbox) - poly.srid = srid - - if srid != 4326: - poly.transform(4326) - if containment == "overlaps": - if not footprint.intersects(poly): - return False - elif containment == "contains": - if not footprint.within(poly): - return False - return True - - def _check_subset(self, subset): - if not isinstance(subset, Subset): - raise ValueError("Supplied argument is not a subset.") - - if not isinstance(subset, self.allowed_types): - raise InvalidSubsettingException( - "Supplied subset is not allowed." - ) - - if self.has_x and subset.is_x: - raise InvalidSubsettingException( - "Multiple subsets for X-axis given." - ) - - if self.has_y and subset.is_y: - raise InvalidSubsettingException( - "Multiple subsets for Y-axis given." - ) - - if self.has_t and subset.is_temporal: - raise InvalidSubsettingException( - "Multiple subsets for time-axis given." - ) - - @property - def xy_bbox(self): - """ Returns the minimum bounding box for all X and Y subsets. - - :returns: a list of four elements [minx, miny, maxx, maxy], which might - be ``None`` - """ - bbox = [None, None, None, None] - for subset in self: - if subset.is_x: - if isinstance(subset, Trim): - bbox[0] = subset.low - bbox[2] = subset.high - else: - bbox[0] = bbox[2] = subset.value - elif subset.is_y: - if isinstance(subset, Trim): - bbox[1] = subset.low - bbox[3] = subset.high - else: - bbox[1] = bbox[3] = subset.value - - return bbox - - def bounding_polygon(self, coverage): - """ Returns a minimum bounding :class:`django.contrib.gis.geos.Polygon` - for the given :class:`Coverage - ` - - :param coverage: the coverage to calculate the bounding polygon for - :returns: the calculated ``Polygon`` - """ - - srid = coverage.srid - extent = coverage.extent - size_x, size_y = coverage.size - footprint = coverage.footprint - - subset_srid = self.srid - - if subset_srid is None: - bbox = list(extent) - else: - bbox = list(footprint.extent) - - for subset in self: - if not isinstance(subset, Trim) or subset.is_temporal: - continue - - if subset_srid is None: - # transform coordinates from imageCRS to coverages CRS - if subset.is_x: - if subset.low is not None: - l = max(float(subset.low) / float(size_x), 0.0) - bbox[0] = extent[0] + l * (extent[2] - extent[0]) - - if subset.high is not None: - l = max(float(subset.high) / float(size_x), 0.0) - bbox[2] = extent[0] + l * (extent[2] - extent[0]) - - elif subset.is_y: - if subset.low is not None: - l = max(float(subset.low) / float(size_y), 0.0) - bbox[1] = extent[3] - l * (extent[3] - extent[1]) - - if subset.high is not None: - l = max(float(subset.high) / float(size_y), 0.0) - bbox[3] = extent[3] - l * (extent[3] - extent[1]) - - else: - if subset.is_x: - if subset.low is not None: - bbox[0] = max(subset.low, bbox[0]) - - if subset.high is not None: - bbox[2] = min(subset.high, bbox[2]) - - if subset.is_y: - if subset.low is not None: - bbox[1] = max(subset.low, bbox[1]) - - if subset.high is not None: - bbox[3] = min(subset.high, bbox[3]) - - if subset_srid is None: - poly = Polygon.from_bbox(bbox) - poly.srid = srid - - else: - poly = Polygon.from_bbox(bbox) - poly.srid = subset_srid - - return poly - - -class Subset(object): - """ Base class for all subsets. - """ - - def __init__(self, axis): - axis = axis.lower() - if axis not in all_axes: - raise InvalidAxisLabelException(axis) - self.axis = axis - - @property - def is_temporal(self): - return self.axis in temporal_axes - - @property - def is_x(self): - return self.axis in x_axes - - @property - def is_y(self): - return self.axis in y_axes - - -class Slice(Subset): - """ Slice subsets reduce the dimension of the subsetted object by one and - slice the given ``axis`` at the specified ``value``. - - :param axis: the axis name - :param value: the slice point - """ - def __init__(self, axis, value): - super(Slice, self).__init__(axis) - self.value = value - - def __repr__(self): - return "Slice: %s[%s]" % (self.axis, self.value) - - -class Trim(Subset): - """ Trim subsets reduce the domain of the specified ``axis`` - - :param axis: the axis name - :param low: the lower end of the ``Trim``; if omitted, the ``Trim`` has no - lower bound - :param high: the upper end of the ``Trim``; if omitted, the ``Trim`` has no - upper bound - """ - def __init__(self, axis, low=None, high=None): - super(Trim, self).__init__(axis) - - if low is not None and high is not None and low > high: - raise InvalidSubsettingException( - "Invalid bounds: lower bound greater than upper bound." - ) - - self.low = low - self.high = high - - def __repr__(self): - return "Trim: %s[%s:%s]" % ( - self.axis, self.low, self.high - ) - - -temporal_axes = ("t", "time", "phenomenontime") -x_axes = ("x", "lon", "long") -y_axes = ("y", "lat") -z_axes = ("z", "height") -all_axes = temporal_axes + x_axes + y_axes + z_axes - - -def is_temporal(axis): - """ Returns whether or not an axis is a temporal one. - """ - return (axis.lower() in temporal_axes) diff -Nru eoxserver-0.4.0beta2/eoxserver/services/testbase.py eoxserver-0.3.2/eoxserver/services/testbase.py --- eoxserver-0.4.0beta2/eoxserver/services/testbase.py 1970-01-01 00:00:00.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/testbase.py 2013-12-10 14:57:00.000000000 +0000 @@ -0,0 +1,1000 @@ +#------------------------------------------------------------------------------- +# $Id$ +# +# Project: EOxServer +# Authors: Stephan Krause +# Stephan Meissl +# Fabian Schindler +# Martin Paces +# +#------------------------------------------------------------------------------- +# Copyright (C) 2011 EOX IT Services GmbH +# +# 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 of this Software or works derived from this 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. +#------------------------------------------------------------------------------- + +import re +import os.path +import logging +from lxml import etree +import tempfile +import mimetypes +from cStringIO import StringIO + +from django.test import Client +from django.conf import settings + +from eoxserver.core.system import System +from eoxserver.core.util.xmltools import XMLDecoder +from eoxserver.core.util.multiparttools import ( + mpUnpack, getMimeType, getMultipartBoundary +) +from eoxserver.contrib import gdal, osr +from eoxserver.testing.core import ( + EOxServerTestCase, BASE_FIXTURES +) +from eoxserver.testing.xcomp import xmlCompareFiles + + +logger = logging.getLogger(__name__) + +# THIS IS INTENTIONALLY DOUBLED DUE TO A BUG IN MIMETYPES! +mimetypes.init() +mimetypes.init() + +# precompile regular expression +RE_MIME_TYPE_XML = re.compile("^text/xml|application/(?:[a-z]+\+)?xml$",re.IGNORECASE) + +#=============================================================================== +# Helper functions +#=============================================================================== + +def extent_from_ds(ds): + gt = ds.GetGeoTransform() + size_x = ds.RasterXSize + size_y = ds.RasterYSize + + return (gt[0], # minx + gt[3] + size_x * gt[5], # miny + gt[0] + size_y * gt[1], # maxx + gt[3]) # maxy + +def resolution_from_ds(ds): + gt = ds.GetGeoTransform() + return (gt[1], abs(gt[5])) + +def _getMime( s ) : + return s.partition(';')[0].strip().lower() +#=============================================================================== +# Common classes +#=============================================================================== + +class OWSTestCase(EOxServerTestCase): + + """ Main base class for testing the OWS interface + of EOxServer. + """ + + fixtures = BASE_FIXTURES + ["testing_coverages.json", "testing_asar.json"] + + def setUp(self): + super(OWSTestCase,self).setUp() + + logger.info("Starting Test Case: %s" % self.__class__.__name__) + + rq = self.getRequest() + + if ( len(rq) == 2 ): + request, req_type = rq + headers = {} + else: + request, req_type, headers = rq + + client = Client() + + if req_type == "kvp": + self.response = client.get('/ows?%s' % request, {}, **headers) + + elif req_type == "xml": + self.response = client.post('/ows', request, "text/xml", {}, **headers) + + else: + raise Exception("Invalid request type '%s'." % req_type) + + def isRequestConfigEnabled(self, config_key, default=False): + value = System.getConfig().getConfigValue("testing", config_key) + if value is None: + return default + elif value.lower() in ("yes", "y", "true", "on"): + return True + elif value.lower() in ("no", "n", "false", "off"): + return False + else: + return default + + def getRequest(self): + raise Exception("Not implemented.") + + def getFileExtension(self, file_type): + return "xml" + + def getResponseFileDir(self): + return os.path.join(settings.PROJECT_DIR,"responses") + + def getDataFileDir(self): + return os.path.join(settings.PROJECT_DIR,"data") + + def getResponseFileName(self, file_type): + return "%s.%s" % (self.__class__.__name__, self.getFileExtension(file_type)) + + def getResponseData(self): + return self.response.content + + def getExpectedFileDir(self): + return os.path.join(settings.PROJECT_DIR, "expected") + + def getExpectedFileName(self, file_type): + return "%s.%s" % (self.__class__.__name__, self.getFileExtension(file_type)) + + def getXMLData(self): + raise Exception("Not implemented.") + + def _testXMLComparison( self , suffix = "xml" , response = None ): + """ + Helper function for the basic XML tree comparison to be used by `testXMLComparison`. + """ + expected_path = os.path.join( self.getExpectedFileDir(), self.getExpectedFileName(suffix) ) + response_path = os.path.join( self.getResponseFileDir(), self.getResponseFileName(suffix) ) + + # store the XML response + if response is None : response = self.getXMLData() + + # check that the expected XML response exists + if not os.path.isfile( expected_path ) : + with file(response_path, 'w') as fid : fid.write(response) + self.skipTest( "Missing the expected XML response '%s'." % expected_path ) + + # perform the actual comparison + try: + xmlCompareFiles( expected_path , StringIO(response) ) + except Exception as e : + with file(response_path, 'w') as fid : fid.write(response) + self.fail( "Response returned in '%s' is not equal to expected response in '%s'. REASON: %s " % \ + ( response_path , expected_path , str(e) ) ) + + + def _testBinaryComparison(self, file_type, Data=None): + """ + Helper function for the `testBinaryComparisonRaster` function. + """ + expected_path = os.path.join(self.getExpectedFileDir(), self.getExpectedFileName(file_type)) + response_path = os.path.join(self.getResponseFileDir(), self.getResponseFileName(file_type)) + + try: + f = open(expected_path, 'r') + expected = f.read() + f.close() + except IOError: + expected = None + + actual_response = None + if Data is None: + if file_type in ("raster", "html"): + actual_response = self.getResponseData() + elif file_type == "xml": + actual_response = self.getXMLData() + else: + self.fail("Unknown file_type '%s'." % file_type) + else: + actual_response = Data + + if expected != actual_response: + if self.getFileExtension("raster") in ("hdf", "nc"): + self.skipTest("Skipping binary comparison for HDF or NetCDF file '%s'." % expected_path) + f = open(response_path, 'w') + f.write(actual_response) + f.close() + + if expected is None: + self.skipTest("Expected response in '%s' is not present" % expected_path) + else: + self.fail("Response returned in '%s' is not equal to expected response in '%s'." % ( + response_path, expected_path) + ) + + def testStatus(self): + logger.info("Checking HTTP Status ...") + #pylint: disable=E1103 + self.assertEqual(self.response.status_code, 200) + + +class RasterTestCase(OWSTestCase): + """ + Base class for test cases that expect a raster as response. + """ + + def getFileExtension(self, file_type): + return "tif" + + def testBinaryComparisonRaster(self): + if not self.isRequestConfigEnabled("binary_raster_comparison_enabled", True): + self.skipTest("Binary raster comparison is explicitly disabled.") + self._testBinaryComparison("raster") + + +class GDALDatasetTestCase(RasterTestCase): + """ + Extended RasterTestCases that open the result with GDAL and + perform several tests. + """ + + def tearDown(self): + super(GDALDatasetTestCase, self).tearDown() + try: + del self.res_ds + del self.exp_ds + os.remove(self.tmppath) + except AttributeError: + pass + + def _openDatasets(self): + _, self.tmppath = tempfile.mkstemp("." + self.getFileExtension("raster")) + f = open(self.tmppath, "w") + f.write(self.getResponseData()) + f.close() + gdal.AllRegister() + + exp_path = os.path.join(self.getExpectedFileDir(), self.getExpectedFileName("raster")) + + try: + self.res_ds = gdal.Open(self.tmppath, gdal.GA_ReadOnly) + except RuntimeError, e: + self.fail("Response could not be opened with GDAL. Error was %s" % e) + + try: + self.exp_ds = gdal.Open(exp_path, gdal.GA_ReadOnly) + except RuntimeError: + self.skipTest("Expected response in '%s' is not present" % exp_path) + +class RectifiedGridCoverageTestCase(GDALDatasetTestCase): + def testSize(self): + self._openDatasets() + self.assertEqual((self.res_ds.RasterXSize, self.res_ds.RasterYSize), + (self.exp_ds.RasterXSize, self.exp_ds.RasterYSize)) + + def testExtent(self): + self._openDatasets() + EPSILON = 1e-8 + + res_extent = extent_from_ds(self.res_ds) + exp_extent = extent_from_ds(self.exp_ds) + + self.assert_( + max([ + abs(res_extent[i] - exp_extent[i]) for i in range(0, 4) + ]) < EPSILON + ) + + def testResolution(self): + self._openDatasets() + res_resolution = resolution_from_ds(self.res_ds) + exp_resolution = resolution_from_ds(self.exp_ds) + self.assertAlmostEqual(res_resolution[0], exp_resolution[0], delta=exp_resolution[0]/10) + self.assertAlmostEqual(res_resolution[1], exp_resolution[1], delta=exp_resolution[1]/10) + + def testBandCount(self): + self._openDatasets() + self.assertEqual(self.res_ds.RasterCount, self.exp_ds.RasterCount) + +class ReferenceableGridCoverageTestCase(GDALDatasetTestCase): + def testSize(self): + self._openDatasets() + self.assertEqual((self.res_ds.RasterXSize, self.res_ds.RasterYSize), + (self.exp_ds.RasterXSize, self.exp_ds.RasterYSize)) + + def testBandCount(self): + self._openDatasets() + self.assertEqual(self.res_ds.RasterCount, self.exp_ds.RasterCount) + + def testGCPs(self): + self._openDatasets() + self.assertEqual(self.res_ds.GetGCPCount(), self.exp_ds.GetGCPCount()) + + def testGCPProjection(self): + self._openDatasets() + + res_proj = self.res_ds.GetGCPProjection() + if not res_proj: + self.fail("Response Dataset has no GCP Projection defined") + res_srs = osr.SpatialReference(res_proj) + + exp_proj = self.exp_ds.GetGCPProjection() + if not exp_proj: + self.fail("Expected Dataset has no GCP Projection defined") + exp_srs = osr.SpatialReference(exp_proj) + + self.assert_(res_srs.IsSame(exp_srs)) + +class XMLTestCase(OWSTestCase): + """ + Base class for test cases that expects XML output, which is parsed + and validated against a schema definition. + """ + + def getXMLData(self): + return self.response.content + + def testValidate(self, XMLData=None): + logger.info("Validating XML ...") + + if XMLData is None: + doc = etree.XML(self.getXMLData()) + else: + doc = etree.XML(XMLData) + schema_locations = doc.get("{http://www.w3.org/2001/XMLSchema-instance}schemaLocation") + locations = schema_locations.split() + + # get schema locations + schema_def = etree.Element("schema", attrib={ + "elementFormDefault": "qualified", + "version": "1.0.0", + }, nsmap={ + None: "http://www.w3.org/2001/XMLSchema" + } + ) + + for ns, location in zip(locations[::2], locations[1::2]): + if location == "../owsCoverages.xsd": + location = "http://schemas.opengis.net/wcs/1.1/wcsAll.xsd" + etree.SubElement(schema_def, "import", attrib={ + "namespace": ns, + "schemaLocation": location + } + ) + + # TODO: ugly workaround. But otherwise, the doc is not recognized as schema + schema = etree.XMLSchema(etree.XML(etree.tostring(schema_def))) + + try: + schema.assertValid(doc) + except etree.Error as e: + self.fail(str(e)) + + def testXMLComparison(self): + self._testXMLComparison() + +class SchematronTestMixIn(object): # requires to be mixed in with XMLTestCase + """ + Mixin class for XML test cases that uses XML schematrons for validation. + Use the `schematron_locations` + """ + schematron_locations = () + + def testSchematron(self): + errors = [] + doc = etree.XML(self.getXMLData()) + + schematron_def = etree.Element("schema", attrib={ + "queryBinding": "xslt2", + }, nsmap={ + None: "http://purl.oclc.org/dsdl/schematron" + } + ) + etree.SubElement(schematron_def, "pattern") + +# TODO: Check if this is even possible: +# for ns, location in zip(self.schematron_locations[::2], self.schematron_locations[1::2]): +# etree.SubElement(schematron_def, "import", attrib={ +# "namespace": ns, +# "schemaLocation": location +# } +# ) + + # TODO: ugly workaround. But otherwise, the doc is not recognized as schema + schematron = etree.Schematron(etree.XML(etree.tostring(schematron_def))) + + try: + schematron.assertValid(doc) + except etree.DocumentInvalid, e: + errors.append(str(e)) + except etree.SchematronValidateError: + self.skipTest("Schematron Testing is not enabled.") + + if len(errors): + self.fail(str(errors)) + + +class ExceptionTestCase(XMLTestCase): + """ + Exception test cases expect the request to fail and examine the + exception response. + """ + + def getExpectedHTTPStatus(self): + return 400 + + def getExpectedExceptionCode(self): + return "" + + def getExceptionCodeLocation(self): + return "/ows:Exception/@exceptionCode" + + def testStatus(self): + logger.info("Checking HTTP Status ...") + #pylint: disable=E1103 + self.assertEqual(self.response.status_code, self.getExpectedHTTPStatus()) + + def testExceptionCode(self): + logger.info("Checking OWS Exception Code ...") + decoder = XMLDecoder(self.getXMLData(), { + "exceptionCode": {"xml_location": self.getExceptionCodeLocation(), "xml_type": "string"} + }) + self.assertEqual(decoder.getValue("exceptionCode"), self.getExpectedExceptionCode()) + +class HTMLTestCase(OWSTestCase): + """ + HTML test cases expect to receive HTML text. + """ + + def getFileExtension(self, file_type): + return "html" + + def testBinaryComparisonHTML(self): + self._testBinaryComparison("html") + +class MultipartTestCase(XMLTestCase): + """ + Multipart tests combine XML and raster tests and split the response + into a xml and a raster part which are examined separately. + """ + + def setUp(self): + self.xmlData = None + self.imageData = None + self.isSetUp = False + super(MultipartTestCase, self).setUp() + + self._setUpMultiparts() + + + def _mangleXML( self , cbuffer ) : + """ remove variable parts of selected XML elements text and attributes """ + + #define XML names to be used + + N0 = "{http://www.opengis.net/gml/3.2}rangeSet/" \ + "{http://www.opengis.net/gml/3.2}File/" \ + "{http://www.opengis.net/gml/3.2}rangeParameters" + + N1 = "{http://www.opengis.net/gml/3.2}rangeSet/" \ + "{http://www.opengis.net/gml/3.2}File/" \ + "{http://www.opengis.net/gml/3.2}fileReference" + + N2 = "{http://www.opengis.net/wcs/1.1}Coverage/" \ + "{http://www.opengis.net/ows/1.1}Reference" + + HREF = "{http://www.w3.org/1999/xlink}href" + + # define handy closures + + def cropFileName( name ) : + base , _ , ext = name.rpartition(".") + base , _ , _ = base.rpartition("_") + return "%s.%s"%(base,ext) + + def changeHref( e ) : + if e is not None : e.set( HREF , cropFileName( e.get( HREF ) ) ) + + def changeText( e ) : + if e is not None: e.text = cropFileName( e.text ) + + # parse XML Note: etree.parse respects the encoding reported in XML delaration! + xml = etree.parse( StringIO( cbuffer ) ) + + # mangle XML content to get rid of variable content + + # WCS 2.0.x - rangeSet/File + changeHref( xml.find( N0 ) ) + changeText( xml.find( N1 ) ) + # WCS 1.1.x - Coverage/Reference + changeHref( xml.find( N2 ) ) + + # output xml - force UTF-8 encoding including the XML delaration + return etree.tostring( xml , encoding="UTF-8" , xml_declaration=True ) + + def _unpackMultipartContent( self , response ) : + + content = response.content + content_type = response['Content-Type'] + + # check the content type + if getMimeType( content_type ) \ + not in ( "multipart/mixed" , "multipart/related" ) : + raise Exception , "Received content is neither mixed nor related multipart! Content-Type: %s" % content_type + + # extract multipart boundary + boundary = getMultipartBoundary( content_type ) + + # unpack the multipart content + for header,offset,size in mpUnpack(content,boundary) : + match = RE_MIME_TYPE_XML.match( _getMime(header['content-type']) ) + if match is not None : + # store XML response + self.xmlData = content[offset:(offset+size)] + else : + # store coverage data + self.imageData = content[offset:(offset+size)] + + + def _setUpMultiparts(self): + if self.isSetUp: return + + self._unpackMultipartContent( self.response ) + + # mangle XML data + if self.xmlData : self.xmlData = self._mangleXML( self.xmlData ) + + self.isSetUp = True + + + def getFileExtension(self, part=None): + if part == "xml": + return "xml" + elif part == "raster": + return "tif" + elif part == "TransactionDescribeCoverage": + return "TransactionDescribeCoverage.xml" + elif part == "TransactionDeleteCoverage": + return "TransactionDeleteCoverage.xml" + elif part == "TransactionDescribeCoverageDeleted": + return "TransactionDescribeCoverageDeleted.xml" + else: + return "dat" + + def getXMLData(self): + self._setUpMultiparts() + return self.xmlData + + def getResponseData(self): + self._setUpMultiparts() + return self.imageData + +class RectifiedGridCoverageMultipartTestCase( + MultipartTestCase, + RectifiedGridCoverageTestCase +): + pass + +class ReferenceableGridCoverageMultipartTestCase( + MultipartTestCase, + ReferenceableGridCoverageTestCase +): + pass + +#=============================================================================== +# WCS-T +#=============================================================================== + +class WCSTransactionTestCase(XMLTestCase): + """ + Base class for WCS Transactional test cases. + """ + ADDmetaFile = None + ADDtiffFile = None + isAsync = False + ID = None + + def setUp(self): + super(WCSTransactionTestCase, self).setUp() + logger.debug("WCSTransactionTestCase for ID: %s" % self.ID) + + if self.isAsync: + from eoxserver.resources.processes.tracker import ( + dequeueTask, TaskStatus, startTask, stopTaskSuccessIfNotFinished + ) + + # get a pending task from the queue + taskId = dequeueTask(1)[0] + + # create instance of TaskStatus class + pStatus = TaskStatus( taskId ) + try: + # get task parameters and change status to STARTED + requestType , requestID , requestHandler , inputs = startTask( taskId ) + # load the handler + module , _ , funct = requestHandler.rpartition(".") + handler = getattr( __import__(module,fromlist=[funct]) , funct ) + # execute handler + handler( pStatus , inputs ) + # if no terminating status has been set do it right now + stopTaskSuccessIfNotFinished( taskId ) + except Exception as e : + pStatus.setFailure( unicode(e) ) + + # Add DescribeCoverage request/response + request = "service=WCS&version=2.0.0&request=DescribeCoverage&coverageid=%s" % str( self.ID ) + self.responseDescribeCoverage = self.client.get('/ows?%s' % request) + + # Add GetCoverage request/response + request = "service=WCS&version=2.0.0&request=GetCoverage&format=image/tiff&mediatype=multipart/mixed&coverageid=%s" % str( self.ID ) + self.responseGetCoverage = self.client.get('/ows?%s' % request) + + # Add delete coverage request/response + requestBegin = """ + + + """ + requestEnd = """ + + Delete + " + + + """ + request = requestBegin + self.ID + requestEnd + self.responseDeleteCoverage = self.client.post('/ows', request, "text/xml") + + # Add DescribeCoverage request/response after delete + request = "service=WCS&version=2.0.0&request=DescribeCoverage&coverageid=%s" % str( self.ID ) + self.responseDescribeCoverageDeleted = self.client.get('/ows?%s' % request) + + def getRequest(self): + requestBegin = """ + + + """ + requestMid1 = """ + """ + requestMid3 = """""" + requestMid5 = """Add + + """ + requestAsync = """http://NOTUSED""" + requestEnd = """""" + + params = requestBegin + self.ID + requestMid1 + self.getDataFullPath(self.ADDtiffFile) + requestMid2 + if self.ADDmetaFile is not None: + params += requestMid3 + self.getDataFullPath(self.ADDmetaFile) + requestMid4 + params += requestMid5 + if self.isAsync: + params += requestAsync + params += requestEnd + return (params, "xml") + + def getDataFullPath(self , path_to): + return os.path.abspath( os.path.join( self.getDataFileDir() , path_to) ) + + def testXMLComparison(self): + # the TimeStamp and RequestId elements are set during ingestion and + # thus have to be explicitly unified + tree = etree.fromstring(self.getXMLData()) + for node in tree.findall("{http://www.opengis.net/wcs/1.1/wcst}RequestId"): + node.text = "identifier" + for node in tree.findall("{http://www.opengis.net/wcs/1.1/wcst}TimeStamp"): + node.text = "2011-01-01T00:00:00Z" + self.response.content = etree.tostring(tree, encoding="ISO-8859-1") + super(WCSTransactionTestCase, self).testXMLComparison() + + def testResponseIdComparisonAdd(self): + """ + Tests that the in the XML request and response is the + same + """ + logger.debug("IDCompare testResponseIdComparison for ID: %s" % self.ID) + self._testResponseIdComparison( self.ID , self.getXMLData() ) + + def testStatusDescribeCoverage(self): + """ + Tests that the inserted coverage is available in a DescribeCoverage + request + """ + #pylint: disable=E1103 + self.assertEqual(self.responseDescribeCoverage.status_code, 200) + + def testValidateDescribeCoverage(self): + self.testValidate(self.responseDescribeCoverage.content) + + def testXMLComparisonDescribeCoverage(self): + #self._testBinaryComparison("TransactionDescribeCoverage", self.responseDescribeCoverage.content) + self._testXMLComparison( "TransactionDescribeCoverage" , self.responseDescribeCoverage.content ) + + def testStatusGetCoverage(self): + """ + Validate the inserted coverage via a GetCoverage request + """ + #pylint: disable=E1103 + self.assertEqual(self.responseGetCoverage.status_code, 200) + + def testStatusDeleteCoverage(self): + """ + Test to delete the previously inserted coaverage + """ + #pylint: disable=E1103 + self.assertEqual(self.responseDeleteCoverage.status_code, 200) + + def testValidateDeleteCoverage(self): + self.testValidate(self.responseDeleteCoverage.content) + + def testXMLComparisonDeleteCoverage(self): + tree = etree.fromstring(self.responseDeleteCoverage.content) + for node in tree.findall("{http://www.opengis.net/wcs/1.1/wcst}RequestId"): + node.text = "identifier" + #self._testBinaryComparison("TransactionDeleteCoverage", etree.tostring(tree, encoding="ISO-8859-1")) + self._testXMLComparison( "TransactionDeleteCoverage" , etree.tostring(tree, encoding="ISO-8859-1")) + + def testResponseIdComparisonDelete(self): + """ + Tests that the in the XML request and response is the + same + """ + logger.debug("IDCompare testResponseIdComparison for ID: %s" % self.ID) + self._testResponseIdComparison( self.ID , self.responseDeleteCoverage.content ) + + def testStatusDescribeCoverageDeleted(self): + """ + Tests that the deletec coverage is not longer available in a + DescribeCoverage request + """ + #pylint: disable=E1103 + self.assertEqual(self.responseDescribeCoverageDeleted.status_code, 404) + + def testValidateDescribeCoverageDeleted(self): + self.testValidate(self.responseDescribeCoverageDeleted.content) + + def testXMLComparisonDescribeCoverageDeleted(self): + #self._testBinaryComparison("TransactionDescribeCoverageDeleted", self.responseDescribeCoverageDeleted.content) + self._testXMLComparison( "TransactionDescribeCoverageDeleted" , self.responseDescribeCoverageDeleted.content ) + + def _testResponseIdComparison(self , id , rcontent ): + """ + Tests that the in the XML request and response is the + same + """ + logger.debug("_testResponseIdComparison for ID: %s" % id) + tree = etree.fromstring( rcontent ) + for node in tree.findall("{http://www.opengis.net/ows/1.1}Identifier"): + self.assertEqual( node.text, id ) + +class WCSTransactionRectifiedGridCoverageTestCase( + RectifiedGridCoverageMultipartTestCase, + WCSTransactionTestCase +): + """ + WCS-T test cases for RectifiedGridCoverages + """ + # Overwrite _setUpMultiparts() to return the GetCoverage response to be used + # in MultipartTestCase tests + def _setUpMultiparts(self): + if self.isSetUp: return + + self._unpackMultipartContent( self.responseGetCoverage ) + + self.isSetUp = True + + def getXMLData(self): + return self.response.content + +class WCSTransactionReferenceableGridCoverageTestCase( + ReferenceableGridCoverageMultipartTestCase, + WCSTransactionTestCase +): + """ + WCS-T test cases for ReferenceableGridCoverages + """ + # Overwrite _setUpMultiparts() to return the GetCoverage response to be used + # in MultipartTestCase tests + def _setUpMultiparts(self): + if self.isSetUp: return + + self._unpackMultipartContent( self.responseGetCoverage ) + + self.isSetUp = True + + def getXMLData(self): + return self.response.content + +#=============================================================================== +# WCS 2.0 +#=============================================================================== + +class WCS20DescribeEOCoverageSetSubsettingTestCase(XMLTestCase): + def getExpectedCoverageIds(self): + return [] + + def testCoverageIds(self): + logger.info("Checking Coverage Ids ...") + decoder = XMLDecoder(self.getXMLData(), { + "coverageids": {"xml_location": "/wcs:CoverageDescriptions/wcs:CoverageDescription/wcs:CoverageId", "xml_type": "string[]"} + }) + + result_coverage_ids = decoder.getValue("coverageids") + expected_coverage_ids = self.getExpectedCoverageIds() + self.assertItemsEqual(result_coverage_ids, expected_coverage_ids) + + # assert that every coverage ID is unique in the response + for coverage_id in result_coverage_ids: + self.assertTrue(result_coverage_ids.count(coverage_id) == 1, "CoverageID %s is not unique." % coverage_id) + +class WCS20DescribeEOCoverageSetPagingTestCase(XMLTestCase): + def getExpectedCoverageCount(self): + return 0 + + def testCoverageCount(self): + decoder = XMLDecoder(self.getXMLData(), { + "coverageids": {"xml_location": "/wcs:CoverageDescriptions/wcs:CoverageDescription/wcs:CoverageId", "xml_type": "string[]"} + }) + coverage_ids = decoder.getValue("coverageids") + self.assertEqual(len(coverage_ids), self.getExpectedCoverageCount()) + +class WCS20DescribeEOCoverageSetSectionsTestCase(XMLTestCase): + def getExpectedSections(self): + return [] + + def testSections(self): + decoder = XMLDecoder(self.getXMLData(), { + "sections": {"xml_location": "/*", "xml_type": "tagName[]"} + }) + sections = decoder.getValue("sections") + self.assertItemsEqual(sections, self.getExpectedSections()) + +class WCS20GetCoverageMultipartTestCase(MultipartTestCase): + def testXMLComparison(self): + # The timePosition tag depends on the actual time the request was + # answered. It has to be explicitly unified. + tree = etree.fromstring(self.getXMLData()) + for node in tree.findall("{http://www.opengis.net/gmlcov/1.0}metadata/" \ + "{http://www.opengis.net/gmlcov/1.0}Extension/" \ + "{http://www.opengis.net/wcseo/1.0}EOMetadata/" \ + "{http://www.opengis.net/wcseo/1.0}lineage/" \ + "{http://www.opengis.net/gml/3.2}timePosition"): + node.text = "2011-01-01T00:00:00Z" + self.xmlData = etree.tostring(tree, encoding="ISO-8859-1") + + super(WCS20GetCoverageMultipartTestCase, self).testXMLComparison() + +class WCS20GetCoverageRectifiedGridCoverageMultipartTestCase( + WCS20GetCoverageMultipartTestCase, + RectifiedGridCoverageTestCase +): + pass + +class WCS20GetCoverageReferenceableGridCoverageMultipartTestCase( + WCS20GetCoverageMultipartTestCase, + ReferenceableGridCoverageTestCase +): + pass + +class RasdamanTestCaseMixIn(object): + fixtures = BASE_FIXTURES + ["testing_rasdaman_coverages.json"] + + def setUp(self): + # TODO check if connection to DB server is possible + # TODO check if datasets are configured within the DB + + gdal.AllRegister() + if gdal.GetDriverByName("RASDAMAN") is None: + self.skipTest("Rasdaman driver is not enabled.") + + if not self.isRequestConfigEnabled("rasdaman_enabled"): + self.skipTest("Rasdaman tests are not enabled. Use the " + "configuration option 'rasdaman_enabled' to allow " + "rasdaman tests.") + + super(RasdamanTestCaseMixIn, self).setUp() + +#=============================================================================== +# WMS test classes +#=============================================================================== + +class WMS11GetMapTestCase(RasterTestCase): + layers = [] + styles = [] + crs = "epsg:4326" + bbox = (0, 0, 1, 1) + width = 100 + height = 100 + frmt = "image/jpeg" + time = None + dim_band = None + + swap_axes = True + + httpHeaders = None + + def getFileExtension(self, part=None): + return mimetypes.guess_extension(self.frmt, False)[1:] + + def getRequest(self): + params = "service=WMS&request=GetMap&version=1.1.1&" \ + "layers=%s&styles=%s&srs=%s&bbox=%s&" \ + "width=%d&height=%d&format=%s" % ( + ",".join(self.layers), ",".join(self.styles), self.crs, + ",".join(map(str, self.bbox)), + self.width, self.height, self.frmt + ) + + if self.time: + params += "&time=%s" % self.time + + if self.dim_band: + params += "&dim_band=%s" % self.dim_band + + if self.httpHeaders is None: + return (params, "kvp") + else: + return (params, "kvp", self.httpHeaders) + +class WMS13GetMapTestCase(RasterTestCase): + layers = [] + styles = [] + crs = "epsg:4326" + bbox = (0, 0, 1, 1) + width = 100 + height = 100 + frmt = "image/jpeg" + time = None + dim_band = None + + swap_axes = True + + httpHeaders = None + + def getFileExtension(self, part=None): + return mimetypes.guess_extension(self.frmt, False)[1:] + + def getRequest(self): + bbox = self.bbox if not self.swap_axes else ( + self.bbox[1], self.bbox[0], + self.bbox[3], self.bbox[2] + ) + + params = "service=WMS&request=GetMap&version=1.3.0&" \ + "layers=%s&styles=%s&crs=%s&bbox=%s&" \ + "width=%d&height=%d&format=%s" % ( + ",".join(self.layers), ",".join(self.styles), self.crs, + ",".join(map(str, bbox)), + self.width, self.height, self.frmt + ) + + if self.time: + params += "&time=%s" % self.time + + if self.dim_band: + params += "&dim_band=%s" % self.dim_band + + if self.httpHeaders is None: + return (params, "kvp") + else: + return (params, "kvp", self.httpHeaders) + +class WMS13ExceptionTestCase(ExceptionTestCase): + def getExceptionCodeLocation(self): + return "/{http://www.opengis.net/ogc}ServiceException/@code" diff -Nru eoxserver-0.4.0beta2/eoxserver/services/tests.py eoxserver-0.3.2/eoxserver/services/tests.py --- eoxserver-0.4.0beta2/eoxserver/services/tests.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/tests.py 2013-12-10 14:57:00.000000000 +0000 @@ -1,16 +1,20 @@ #------------------------------------------------------------------------------- +# $Id$ # # Project: EOxServer -# Authors: Fabian Schindler +# Authors: Stephan Krause +# Stephan Meissl +# Fabian Schindler +# Martin Paces # #------------------------------------------------------------------------------- -# Copyright (C) 2013 EOX IT Services GmbH +# Copyright (C) 2011 EOX IT Services GmbH # # 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 +# 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 @@ -25,80 +29,1701 @@ # THE SOFTWARE. #------------------------------------------------------------------------------- -from textwrap import dedent +from urllib import quote -from django.test import TestCase +import eoxserver.services.testbase as eoxstest +from eoxserver.testing.core import BASE_FIXTURES -from eoxserver.core.util import multiparttools as mp -from eoxserver.services.result import result_set_from_raw_data +class EmptyRequestExceptionTestCase(eoxstest.ExceptionTestCase): + """This test shall check empty requests. A valid ows:ExceptionReport shall be returned""" + def getRequest(self): + params = "" + return (params, "kvp") -class MultipartTest(TestCase): - """ Test class for multipart parsing/splitting - """ - - example_multipart = dedent("""\ - MIME-Version: 1.0\r - Content-Type: multipart/mixed; boundary=frontier\r - \r - This is a message with multiple parts in MIME format.\r - --frontier - Content-Type: text/plain\r - \r - This is the body of the message.\r - --frontier - Content-Type: application/octet-stream\r - Content-Transfer-Encoding: base64\r - \r - PGh0bWw+CiAgPGhlYWQ+CiAgPC9oZWFkPgogIDxib2R5PgogICAgPHA+VGhpcyBpcyB0aGUgYm9keSBvZiB0aGUgbWVzc2FnZS48L3A+CiAgPC9ib2R5Pgo8L2h0bWw+Cg==\r - --frontier-- - """) - - def test_multipart_iteration(self): - parsed = map( - lambda i: (i[0], str(i[1])), mp.iterate(self.example_multipart) - ) - - self.assertEqual([ - ({"MIME-Version": "1.0", "Content-Type": "multipart/mixed; boundary=frontier"}, ""), - ({"Content-Type": "text/plain"}, "This is the body of the message."), - ({"Content-Type": "application/octet-stream", "Content-Transfer-Encoding": "base64"}, "PGh0bWw+CiAgPGhlYWQ+CiAgPC9oZWFkPgogIDxib2R5PgogICAgPHA+VGhpcyBpcyB0aGUgYm9keSBvZiB0aGUgbWVzc2FnZS48L3A+CiAgPC9ib2R5Pgo8L2h0bWw+Cg==") - ], parsed - ) - - -class ResultSetTestCase(TestCase): - - example_multipart = dedent("""\ - MIME-Version: 1.0\r - Content-Type: multipart/mixed; boundary=frontier\r - \r - This is a message with multiple parts in MIME format.\r - --frontier - Content-Type: text/plain\r - Content-Disposition: attachmet; filename="message.msg"\r - Content-Id: message-part\r - \r - This is the body of the message.\r - --frontier - Content-Type: application/octet-stream\r - Content-Transfer-Encoding: base64\r - \r - PGh0bWw+CiAgPGhlYWQ+CiAgPC9oZWFkPgogIDxib2R5PgogICAgPHA+VGhpcyBpcyB0aGUgYm9keSBvZiB0aGUgbWVzc2FnZS48L3A+CiAgPC9ib2R5Pgo8L2h0bWw+Cg==\r - --frontier-- - """) - - - def test_result_set_from_raw(self): - result_set = result_set_from_raw_data(self.example_multipart) - self.assertTrue(len(result_set) == 2) - - first = result_set[0] - second = result_set[1] - - self.assertEqual(str(first.data), "This is the body of the message.") - self.assertEqual(first.content_type, "text/plain") - self.assertEqual(first.filename, "message.msg") - self.assertEqual(first.identifier, "message-part") - self.assertEqual(str(second.data), "PGh0bWw+CiAgPGhlYWQ+CiAgPC9oZWFkPgogIDxib2R5PgogICAgPHA+VGhpcyBpcyB0aGUgYm9keSBvZiB0aGUgbWVzc2FnZS48L3A+CiAgPC9ib2R5Pgo8L2h0bWw+Cg==") + def getExpectedExceptionCode(self): + return "MissingParameterValue" +#=============================================================================== +# WCS 1.0 +#=============================================================================== + +class WCS10GetCapabilitiesValidTestCase(eoxstest.XMLTestCase): + def getRequest(self): + params = "service=WCS&version=1.0.0&request=GetCapabilities" + return (params, "kvp") + +class WCS10GetCapabilitiesEmptyTestCase(eoxstest.XMLTestCase): + """This test shall retrieve a valid but empty WCS 1.0 GetCapabilities response (see #41)""" + fixtures = BASE_FIXTURES + + def getRequest(self): + params = "service=WCS&version=1.0.0&request=GetCapabilities" + return (params, "kvp") + +class WCS10DescribeCoverageDatasetTestCase(eoxstest.XMLTestCase): + def getRequest(self): + params = "service=WCS&version=1.0.0&request=DescribeCoverage&coverage=mosaic_MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_RGB_reduced" + return (params, "kvp") + +class WCS10DescribeCoverageMosaicTestCase(eoxstest.XMLTestCase): + def getRequest(self): + params = "service=WCS&version=1.0.0&request=DescribeCoverage&coverage=mosaic_MER_FRS_1P_RGB_reduced" + return (params, "kvp") + +class WCS10GetCoverageDatasetTestCase(eoxstest.RectifiedGridCoverageTestCase): + def getRequest(self): + params = "service=WCS&version=1.0.0&request=GetCoverage&coverage=mosaic_MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_RGB_reduced&crs=epsg:4326&bbox=-4,32,28,46.5&width=640&height=290&format=GeoTIFF" + return (params, "kvp") + +class WCS10GetCoverageMosaicTestCase(eoxstest.RectifiedGridCoverageTestCase): + def getRequest(self): + params = "service=WCS&version=1.0.0&request=GetCoverage&coverage=mosaic_MER_FRS_1P_RGB_reduced&crs=epsg:4326&bbox=-4,32,28,46.5&width=640&height=290&format=image/tiff" + return (params, "kvp") + +#=============================================================================== +# WCS 1.1 +#=============================================================================== + +class WCS11GetCapabilitiesValidTestCase(eoxstest.XMLTestCase): + def getRequest(self): + params = "service=WCS&version=1.1.2&request=GetCapabilities" + return (params, "kvp") + +class WCS11GetCapabilitiesEmptyTestCase(eoxstest.XMLTestCase): + """This test shall retrieve a valid but empty WCS 1.1 GetCapabilities response (see #41)""" + fixtures = BASE_FIXTURES + + def getRequest(self): + params = "service=WCS&version=1.1.2&request=GetCapabilities" + return (params, "kvp") + +class WCS11DescribeCoverageDatasetTestCase(eoxstest.XMLTestCase): + def getRequest(self): + params = "service=WCS&version=1.1.2&request=DescribeCoverage&identifier=mosaic_MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_RGB_reduced" + return (params, "kvp") + +class WCS11DescribeCoverageMosaicTestCase(eoxstest.XMLTestCase): + def getRequest(self): + params = "service=WCS&version=1.1.2&request=DescribeCoverage&identifier=mosaic_MER_FRS_1P_RGB_reduced" + return (params, "kvp") + +class WCS11GetCoverageDatasetTestCase(eoxstest.RectifiedGridCoverageMultipartTestCase): + def getRequest(self): + params = "service=WCS&version=1.1.2&request=GetCoverage&identifier=mosaic_MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_RGB_reduced&crs=epsg:4326&bbox=12,32,28,46.5&format=image/tiff" + return (params, "kvp") + +class WCS11GetCoverageMosaicTestCase(eoxstest.RectifiedGridCoverageMultipartTestCase): + def getRequest(self): + params = "service=WCS&version=1.1.2&request=GetCoverage&identifier=mosaic_MER_FRS_1P_RGB_reduced&crs=epsg:4326&bbox=-4,32,28,46.5&format=image/tiff" + return (params, "kvp") + +class WCS11GetCoverageDatasetSubsetTestCase(eoxstest.RectifiedGridCoverageMultipartTestCase): + def getRequest(self): + params = "service=WCS&version=1.1.2&request=GetCoverage&identifier=mosaic_MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_RGB_reduced&boundingbox=0,0,550,440,urn:ogc:def:crs:OGC::imageCRS&format=image/tiff&GridCS=urn:ogc:def:crs:OGC::imageCRS&GridBaseCRS=urn:ogc:def:crs:OGC::imageCRS&GridType=urn:ogc:def:method:WCS:1.1:2dGridIn2dCrs&GridOrigin=0,0&GridOffsets=2,2" + return (params, "kvp") + +class WCS11GetCoverageDatasetSubsetEPSG4326TestCase(eoxstest.RectifiedGridCoverageMultipartTestCase): + def getRequest(self): + params = "service=WCS&version=1.1.2&request=GetCoverage&identifier=mosaic_MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_RGB_reduced&boundingbox=32,12,46.5,28,urn:ogc:def:crs:EPSG::4326&format=image/tiff&GridCS=urn:ogc:def:crs:OGC::imageCRS&GridBaseCRS=urn:ogc:def:crs:OGC::imageCRS&GridType=urn:ogc:def:method:WCS:1.1:2dGridIn2dCrs&GridOrigin=46.5,12&GridOffsets=0.06,0.06" + return (params, "kvp") + +class WCS11GetCoverageMosaicSubsetTestCase(eoxstest.RectifiedGridCoverageMultipartTestCase): + def getRequest(self): + params = "service=WCS&version=1.1.2&request=GetCoverage&identifier=mosaic_MER_FRS_1P_RGB_reduced&boundingbox=300,200,700,350,urn:ogc:def:crs:OGC::imageCRS&format=image/tiff&GridCS=urn:ogc:def:crs:OGC::imageCRS&GridBaseCRS=urn:ogc:def:crs:OGC::imageCRS&GridType=urn:ogc:def:method:WCS:1.1:2dGridIn2dCrs&GridOrigin=0,0&GridOffsets=2,2" + return (params, "kvp") + +class WCS11GetCoverageMosaicSubsetEPSG4326TestCase(eoxstest.RectifiedGridCoverageMultipartTestCase): + def getRequest(self): + params = "service=WCS&version=1.1.2&request=GetCoverage&identifier=mosaic_MER_FRS_1P_RGB_reduced&boundingbox=35,10,42,20,urn:ogc:def:crs:EPSG::4326&format=image/tiff&GridCS=urn:ogc:def:crs:OGC::imageCRS&GridBaseCRS=urn:ogc:def:crs:OGC::imageCRS&GridType=urn:ogc:def:method:WCS:1.1:2dGridIn2dCrs&GridOrigin=40,10&GridOffsets=-0.06,0.06" + return (params, "kvp") + +#=============================================================================== +# WCS 1.1: Exceptions +#=============================================================================== + +class WCS11DescribeCoverageNoSuchCoverageFaultTestCase(eoxstest.ExceptionTestCase): + def getRequest(self): + params = "service=wcs&version=1.1.2&request=DescribeCoverage&identifier=INVALID" + return (params, "kvp") + + def getExpectedExceptionCode(self): + return "NoSuchCoverage" + + +class WCS11GetCoverageNoSuchCoverageFaultTestCase(eoxstest.ExceptionTestCase): + def getRequest(self): + params = "service=wcs&version=1.1.2&request=GetCoverage&identifier=INVALID" + return (params, "kvp") + + def getExpectedExceptionCode(self): + return "NoSuchCoverage" + +#NOTE: Support for Referenceable Datasets in WCS < 2.0.0 not avaiable. +# Any attempt to access Ref.DS via WCS 1.x should be treated as +# non-existing coverage. +class WCS11DescribeCoverageReferenceableFaultTestCase(eoxstest.ExceptionTestCase): + def getRequest(self): + params = "service=wcs&version=1.1.2&request=DescribeCoverage&identifier=ASA_WSM_1PNDPA20050331_075939_000000552036_00035_16121_0775" + return (params, "kvp") + + def getExpectedExceptionCode(self): + return "NoSuchCoverage" + +class WCS11GetCoverageReferenceableFaultTestCase(eoxstest.ExceptionTestCase): + def getRequest(self): + params = "service=wcs&version=1.1.2&request=GetCoverage&identifier=ASA_WSM_1PNDPA20050331_075939_000000552036_00035_16121_0775" + return (params, "kvp") + + def getExpectedExceptionCode(self): + return "NoSuchCoverage" + +class WCS11GetCoverageBBoxFaultTestCase(eoxstest.ExceptionTestCase): + def getRequest(self): + params = "service=wcs&version=1.1.2&request=GetCoverage&identifier=mosaic_MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_RGB_reduced&boundingbox=80,80,90,90,urn:ogc:def:crs:EPSG::4326&format=image/tiff&GridCS=urn:ogc:def:crs:OGC::imageCRS&GridBaseCRS=urn:ogc:def:crs:OGC::imageCRS&GridType=urn:ogc:def:method:WCS:1.1:2dGridIn2dCrs&GridOrigin=46.5,12&GridOffsets=0.06,0.06" + return (params, "kvp") + + def getExpectedExceptionCode(self): + return "bbox" + +class WCS11GetCoverageFormatUnsupportedFaultTestCase(eoxstest.ExceptionTestCase): + def getRequest(self): + params = "service=wcs&version=1.1.2&request=GetCoverage&identifier=MER_FRS_1PNPDE20060822_092058_000001972050_00308_23408_0077_uint16_reduced_compressed&format=image/jpeg" + return (params, "kvp") + + def getExpectedExceptionCode(self): + return "InvalidParameterValue" + +class WCS11GetCoverageFormatUnknownFaultTestCase(eoxstest.ExceptionTestCase): + def getRequest(self): + params = "service=wcs&version=1.1.2&request=GetCoverage&identifier=MER_FRS_1PNPDE20060822_092058_000001972050_00308_23408_0077_uint16_reduced_compressed&format=unknown" + return (params, "kvp") + + def getExpectedExceptionCode(self): + return "InvalidParameterValue" + +#=============================================================================== +# WCS 1.1 WCS Transaction tests +#=============================================================================== + +class WCS11TransactionRectifiedDatasetTestCase(eoxstest.WCSTransactionRectifiedGridCoverageTestCase): + """ This test case shall test the synchronous inserting of a new + RectifiedGridCoverage by means of the WCS 1.1 Transaction operation + ("Add" action). + """ + fixtures = BASE_FIXTURES + ID = "RECTIFIED_MERIS_ID" + ADDtiffFile = "meris/mosaic_MER_FRS_1P_RGB_reduced/mosaic_ENVISAT-MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_RGB_reduced.tif" + ADDmetaFile = "meris/mosaic_MER_FRS_1P_RGB_reduced/mosaic_ENVISAT-MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_RGB_reduced.xml" + +class WCS11TransactionAsyncRectifiedDatasetTestCase(eoxstest.WCSTransactionRectifiedGridCoverageTestCase): + """ This test case shall test the asynchronous inserting of a new + RectifiedGridCoverage by means of the WCS 1.1 Transaction operation + ("Add" action). + """ + fixtures = BASE_FIXTURES + ID = "RECTIFIED_MERIS_ID" + ADDtiffFile = "meris/mosaic_MER_FRS_1P_RGB_reduced/mosaic_ENVISAT-MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_RGB_reduced.tif" + ADDmetaFile = "meris/mosaic_MER_FRS_1P_RGB_reduced/mosaic_ENVISAT-MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_RGB_reduced.xml" + isAsync = True + +class WCS11TransactionReferenceableDatasetTestCase(eoxstest.WCSTransactionReferenceableGridCoverageTestCase): + """ This test case shall test the synchronous inserting of a new + ReferenceableGridCoverage by means of the WCS 1.1 Transaction operation + ("Add" action). + """ + fixtures = BASE_FIXTURES + ID = "REFERENCEABLE_ASAR_ID" + ADDtiffFile = "asar/ASA_WSM_1PNDPA20050331_075939_000000552036_00035_16121_0775.tiff" + +#=============================================================================== +# WCS 2.0 Get Capabilities +#=============================================================================== + +class WCS20GetCapabilitiesValidTestCase(eoxstest.XMLTestCase, eoxstest.SchematronTestMixIn): + """This test shall retrieve a valid WCS 2.0 EO-AP (EO-WCS) GetCapabilities response""" + + schematron_locations = ["http://schemas.opengis.net/wcs/crs/", + "http://schemas.opengis.net/wcs/crs/1.0/wcsCrs.sch"] + + def getRequest(self): + params = "service=WCS&version=2.0.1&request=GetCapabilities" + return (params, "kvp") + +class WCS20GetCapabilitiesEmptyTestCase(eoxstest.XMLTestCase): + """This test shall retrieve a valid but empty WCS 2.0 EO-AP (EO-WCS) GetCapabilities response (see #41)""" + fixtures = BASE_FIXTURES + + def getRequest(self): + params = "service=WCS&version=2.0.1&request=GetCapabilities" + return (params, "kvp") + +class WCSVersionNegotiationOldStyleTestCase(eoxstest.XMLTestCase): + """This test shall check old style version negotiation. A valid WCS 2.0 EO-AP (EO-WCS) GetCapabilities response shall be returned""" + def getRequest(self): + params = "service=wcs&version=3.0.0&request=GetCapabilities" + return (params, "kvp") + +class WCSVersionNegotiationNewStyleTestCase(eoxstest.XMLTestCase): + """This test shall check new style version negotiation. A valid WCS 2.0 EO-AP (EO-WCS) GetCapabilities response shall be returned""" + def getRequest(self): + params = "service=wcs&acceptversions=2.0.1,1.1.0&request=GetCapabilities" + return (params, "kvp") + +class WCSVersionNegotiationFaultTestCase(eoxstest.ExceptionTestCase): + """This test shall check new style version negotiation. A valid ows:ExceptionReport shall be returned""" + def getRequest(self): + params = "service=wcs&acceptversions=3.0.0&request=GetCapabilities" + return (params, "kvp") + + def getExpectedExceptionCode(self): + return "VersionNegotiationFailed" + +#=============================================================================== +# WCS 2.0 DescribeCoverage +#=============================================================================== + +class WCS20DescribeCoverageDatasetTestCase(eoxstest.XMLTestCase): + """This test shall retrieve a valid WCS 2.0 EO-AP (EO-WCS) DescribeCoverage response for a wcseo:RectifiedDataset.""" + def getRequest(self): + params = "service=WCS&version=2.0.0&request=DescribeCoverage&CoverageId=MER_FRS_1PNPDE20060822_092058_000001972050_00308_23408_0077_uint16_reduced_compressed" + return (params, "kvp") + +class WCS20DescribeCoverageMosaicTestCase(eoxstest.XMLTestCase): + """This test shall retrieve a valid WCS 2.0 EO-AP (EO-WCS) DescribeCoverage response for a wcseo:RectifiedStitchedMosaic.""" + def getRequest(self): + params = "service=WCS&version=2.0.0&request=DescribeCoverage&CoverageId=mosaic_MER_FRS_1P_RGB_reduced" + return (params, "kvp") + +class WCS20DescribeCoverageDatasetSeriesFaultTestCase(eoxstest.ExceptionTestCase): + """This test shall try to retrieve a CoverageDescription for a non-coverage. It shall yield a valid ows:ExceptionReport""" + def getRequest(self): + params = "service=WCS&version=2.0.0&request=DescribeCoverage&CoverageId=MER_FRS_1P_reduced" + return (params, "kvp") + + def getExpectedHTTPStatus(self): + return 404 + + def getExpectedExceptionCode(self): + return "NoSuchCoverage" + +class WCS20DescribeCoverageFaultTestCase(eoxstest.ExceptionTestCase): + """This test shall try to retrieve a CoverageDescription for a coverage that does not exist. It shall yield a valid ows:ExceptionReport""" + def getRequest(self): + params = "service=WCS&version=2.0.0&request=DescribeCoverage&CoverageId=some_coverage" + return (params, "kvp") + + def getExpectedHTTPStatus(self): + return 404 + + def getExpectedExceptionCode(self): + return "NoSuchCoverage" + +class WCS20DescribeCoverageMissingParameterFaultTestCase(eoxstest.ExceptionTestCase): + """This test shall yield a valid ows:ExceptionReport for a missing parameter""" + def getRequest(self): + params = "service=WCS&version=2.0.0&request=DescribeCoverage" + return (params, "kvp") + + def getExpectedExceptionCode(self): + return "MissingParameterValue" + +#=============================================================================== +# WCS 2.0 DescribeEOCoverageSet +#=============================================================================== + +class WCS20DescribeEOCoverageSetDatasetTestCase(eoxstest.XMLTestCase): + """This test shall retrieve a valid WCS 2.0 EO-AP (EO-WCS) DescribeEOCoverageSet response for a wcseo:RectifiedDataset""" + def getRequest(self): + params = "service=WCS&version=2.0.0&request=DescribeEOCoverageSet&eoId=MER_FRS_1PNPDE20060822_092058_000001972050_00308_23408_0077_uint16_reduced_compressed" + return (params, "kvp") + +class WCS20DescribeEOCoverageSetMosaicTestCase(eoxstest.XMLTestCase): + """This test shall retrieve a valid WCS 2.0 EO-AP (EO-WCS) DescribeEOCoverageSet response for a wcseo:RectifiedStitchedMosaic""" + def getRequest(self): + params = "service=WCS&version=2.0.0&request=DescribeEOCoverageSet&eoId=mosaic_MER_FRS_1P_RGB_reduced" + return (params, "kvp") + +class WCS20DescribeEOCoverageSetDatasetSeriesTestCase(eoxstest.XMLTestCase): + """This test shall retrieve a valid WCS 2.0 EO-AP (EO-WCS) DescribeEOCoverageSet response for a wcseo:RectifiedDatasetSeries.""" + def getRequest(self): + params = "service=WCS&version=2.0.0&request=DescribeEOCoverageSet&eoId=MER_FRS_1P_reduced" + return (params, "kvp") + +class WCS20DescribeEOCoverageSetFaultTestCase(eoxstest.ExceptionTestCase): + """This test shall try to retrieve a CoverageDescription set for an wcseo-Object that does not exist. It shall yield a valid ows:ExceptionReport.""" + def getRequest(self): + params = "service=WCS&version=2.0.0&request=DescribeEOCoverageSet&eoId=some_eo_object" + return (params, "kvp") + + def getExpectedHTTPStatus(self): + return 404 + + def getExpectedExceptionCode(self): + return "NoSuchCoverage" + +class WCS20DescribeEOCoverageSetMissingParameterFaultTestCase(eoxstest.ExceptionTestCase): + def getRequest(self): + params = "service=WCS&version=2.0.0&request=DescribeEOCoverageSet" + return (params, "kvp") + + def getExpectedExceptionCode(self): + return "MissingParameterValue" + +class WCS20DescribeEOCoverageSetTwoSpatialSubsetsTestCase(eoxstest.WCS20DescribeEOCoverageSetSubsettingTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.0&request=DescribeEOCoverageSet&EOID=MER_FRS_1P_reduced&subset=lat,http://www.opengis.net/def/crs/EPSG/0/4326(32,47)&subset=long,http://www.opengis.net/def/crs/EPSG/0/4326(11,33)" + return (params, "kvp") + + def getExpectedCoverageIds(self): + return [ + "MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_uint16_reduced_compressed", + "MER_FRS_1PNPDE20060822_092058_000001972050_00308_23408_0077_uint16_reduced_compressed", + "MER_FRS_1PNPDE20060830_100949_000001972050_00423_23523_0079_uint16_reduced_compressed", + "mosaic_MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_RGB_reduced", + "mosaic_MER_FRS_1P_RGB_reduced" + ] + +class WCS20DescribeEOCoverageSetTwoSpatialSubsetsOverlapsTestCase(eoxstest.WCS20DescribeEOCoverageSetSubsettingTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.0&request=DescribeEOCoverageSet&EOID=MER_FRS_1P_reduced&subset=lat,http://www.opengis.net/def/crs/EPSG/0/4326(32,47)&subset=long,http://www.opengis.net/def/crs/EPSG/0/4326(11,33)&containment=overlaps" + return (params, "kvp") + + def getExpectedCoverageIds(self): + return [ + "MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_uint16_reduced_compressed", + "MER_FRS_1PNPDE20060822_092058_000001972050_00308_23408_0077_uint16_reduced_compressed", + "MER_FRS_1PNPDE20060830_100949_000001972050_00423_23523_0079_uint16_reduced_compressed", + "mosaic_MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_RGB_reduced", + "mosaic_MER_FRS_1P_RGB_reduced" + ] + +class WCS20DescribeEOCoverageSetTwoSpatialSubsetsContainsTestCase(eoxstest.WCS20DescribeEOCoverageSetSubsettingTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.0&request=DescribeEOCoverageSet&EOID=MER_FRS_1P_reduced&subset=lat,http://www.opengis.net/def/crs/EPSG/0/4326(32,47)&subset=long,http://www.opengis.net/def/crs/EPSG/0/4326(11,33)&containment=contains" + return (params, "kvp") + + def getExpectedCoverageIds(self): + return [ + "MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_uint16_reduced_compressed", + "mosaic_MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_RGB_reduced" + ] + +class WCS20DescribeEOCoverageSetTemporalSubsetTestCase(eoxstest.WCS20DescribeEOCoverageSetSubsettingTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.0&request=DescribeEOCoverageSet&EOID=MER_FRS_1P_reduced&subset=phenomenonTime(\"2006-08-01\",\"2006-08-22T09:22:00Z\")" + return (params, "kvp") + + def getExpectedCoverageIds(self): + return [ + "MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_uint16_reduced_compressed", + "MER_FRS_1PNPDE20060822_092058_000001972050_00308_23408_0077_uint16_reduced_compressed", + "mosaic_MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_RGB_reduced", + "mosaic_MER_FRS_1P_RGB_reduced" + ] + +class WCS20DescribeEOCoverageSetTemporalSubsetOverlapsTestCase(eoxstest.WCS20DescribeEOCoverageSetSubsettingTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.0&request=DescribeEOCoverageSet&EOID=MER_FRS_1P_reduced&subset=phenomenonTime(\"2006-08-01\",\"2006-08-22T09:22:00Z\")&containment=overlaps" + return (params, "kvp") + + def getExpectedCoverageIds(self): + return [ + "MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_uint16_reduced_compressed", + "MER_FRS_1PNPDE20060822_092058_000001972050_00308_23408_0077_uint16_reduced_compressed", + "mosaic_MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_RGB_reduced", + "mosaic_MER_FRS_1P_RGB_reduced" + ] + +class WCS20DescribeEOCoverageSetTemporalSubsetContainsTestCase(eoxstest.WCS20DescribeEOCoverageSetSubsettingTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.0&request=DescribeEOCoverageSet&EOID=MER_FRS_1P_reduced&subset=phenomenonTime(\"2006-08-01\",\"2006-08-22T09:22:00Z\")&containment=contains" + return (params, "kvp") + + def getExpectedCoverageIds(self): + return [ + "MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_uint16_reduced_compressed", + "mosaic_MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_RGB_reduced" + ] + +class WCS20DescribeEOCoverageSetSpatioTemporalSubsetTestCase(eoxstest.WCS20DescribeEOCoverageSetSubsettingTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.0&request=DescribeEOCoverageSet&EOID=MER_FRS_1P_reduced&subset=phenomenonTime(\"2006-08-01\",\"2006-08-22T09:22:00Z\")&subset=lat,http://www.opengis.net/def/crs/EPSG/0/4326(32,47)&subset=long,http://www.opengis.net/def/crs/EPSG/0/4326(11,33)" + return (params, "kvp") + + def getExpectedCoverageIds(self): + return [ + "MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_uint16_reduced_compressed", + "MER_FRS_1PNPDE20060822_092058_000001972050_00308_23408_0077_uint16_reduced_compressed", + "mosaic_MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_RGB_reduced", + "mosaic_MER_FRS_1P_RGB_reduced" + ] + +class WCS20DescribeEOCoverageSetSpatioTemporalSubsetOverlapsTestCase(eoxstest.WCS20DescribeEOCoverageSetSubsettingTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.0&request=DescribeEOCoverageSet&EOID=MER_FRS_1P_reduced&subset=phenomenonTime(\"2006-08-01\",\"2006-08-22T09:22:00Z\")&subset=lat,http://www.opengis.net/def/crs/EPSG/0/4326(32,47)&subset=long,http://www.opengis.net/def/crs/EPSG/0/4326(11,33)&containment=overlaps" + return (params, "kvp") + + def getExpectedCoverageIds(self): + return [ + "MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_uint16_reduced_compressed", + "MER_FRS_1PNPDE20060822_092058_000001972050_00308_23408_0077_uint16_reduced_compressed", + "mosaic_MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_RGB_reduced", + "mosaic_MER_FRS_1P_RGB_reduced" + ] + +class WCS20DescribeEOCoverageSetSpatioTemporalSubsetContainsTestCase(eoxstest.WCS20DescribeEOCoverageSetSubsettingTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.0&request=DescribeEOCoverageSet&EOID=MER_FRS_1P_reduced&subset=phenomenonTime(\"2006-08-01\",\"2006-08-22T09:22:00Z\")&subset=lat,http://www.opengis.net/def/crs/EPSG/0/4326(32,47)&subset=long,http://www.opengis.net/def/crs/EPSG/0/4326(11,33)&containment=contains" + return (params, "kvp") + + def getExpectedCoverageIds(self): + return [ + "MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_uint16_reduced_compressed", + "mosaic_MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_RGB_reduced" + ] + +class WCS20DescribeEOCoverageSetIncorrectTemporalSubsetFaultTestCase(eoxstest.ExceptionTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.0&request=DescribeEOCoverageSet&EOID=MER_FRS_1P_reduced&subset=phenomenonTime(2006-08-01,2006-08-22)" + return (params, "kvp") + + def getExpectedHTTPStatus(self): + return 404 + + def getExpectedExceptionCode(self): + return "InvalidSubsetting" + +class WCS20DescribeEOCoverageSetInvalidTemporalSubsetFaultTestCase(eoxstest.ExceptionTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.0&request=DescribeEOCoverageSet&EOID=MER_FRS_1P_reduced&subset=phenomenonTime(\"2006-08-01\",\"2006-31-31\")" + return (params, "kvp") + + def getExpectedHTTPStatus(self): + return 404 + + def getExpectedExceptionCode(self): + return "InvalidSubsetting" + +class WCS20DescribeEOCoverageSetIncorrectSpatialSubsetFaultTestCase(eoxstest.ExceptionTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.0&request=DescribeEOCoverageSet&EOID=MER_FRS_1P_reduced&subset=lat,http://www.opengis.net/def/crs/EPSG/0/4326(some_lat,some_other_lat)" + return (params, "kvp") + + def getExpectedHTTPStatus(self): + return 404 + + def getExpectedExceptionCode(self): + return "InvalidSubsetting" + +class WCS20DescribeEOCoverageSetInvalidSpatialSubsetFaultTestCase(eoxstest.ExceptionTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.0&request=DescribeEOCoverageSet&EOID=MER_FRS_1P_reduced&subset=lat,http://www.opengis.net/def/crs/EPSG/0/4326(47,32)" + return (params, "kvp") + + def getExpectedHTTPStatus(self): + return 404 + + def getExpectedExceptionCode(self): + return "InvalidSubsetting" + +# EOxServer allows and understands certain additional axis labels like "lat", or "long". +class WCS20DescribeEOCoverageSetInvalidAxisLabelFaultTestCase(eoxstest.ExceptionTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.0&request=DescribeEOCoverageSet&EOID=MER_FRS_1P_reduced&subset=x_axis,http://www.opengis.net/def/crs/EPSG/0/4326(32,47)" + return (params, "kvp") + + def getExpectedHTTPStatus(self): + return 404 + + def getExpectedExceptionCode(self): + return "InvalidAxisLabel" + +#=============================================================================== +# WCS 2.0: Paging testcases +#=============================================================================== + +class WCS20DescribeEOCoverageSetDatasetPagingCountTestCase(eoxstest.WCS20DescribeEOCoverageSetPagingTestCase): + def getExpectedCoverageCount(self): + return 2 + + def getRequest(self): + params = "service=WCS&version=2.0.0&request=DescribeEOCoverageSet&eoId=MER_FRS_1P_reduced&count=2" + return (params, "kvp") + +#=============================================================================== +# WCS 2.0: Section test cases +#=============================================================================== + +class WCS20DescribeEOCoverageSetSectionsAllTestCase(eoxstest.WCS20DescribeEOCoverageSetSectionsTestCase): + def getExpectedSections(self): + return [ + "wcs:CoverageDescriptions", + "wcseo:DatasetSeriesDescriptions" + ] + + def getRequest(self): + params = "service=WCS&version=2.0.0&request=DescribeEOCoverageSet&eoId=MER_FRS_1P_reduced§ions=All" + return (params, "kvp") + +class WCS20DescribeEOCoverageSetSectionsAll2TestCase(eoxstest.WCS20DescribeEOCoverageSetSectionsTestCase): + def getExpectedSections(self): + return [ + "wcs:CoverageDescriptions", + "wcseo:DatasetSeriesDescriptions" + ] + + def getRequest(self): + params = "service=WCS&version=2.0.0&request=DescribeEOCoverageSet&eoId=MER_FRS_1P_reduced§ions=CoverageDescriptions,DatasetSeriesDescriptions" + return (params, "kvp") + +class WCS20DescribeEOCoverageSetSectionsAll3TestCase(eoxstest.WCS20DescribeEOCoverageSetSectionsTestCase): + def getExpectedSections(self): + return [ + "wcs:CoverageDescriptions", + "wcseo:DatasetSeriesDescriptions" + ] + + def getRequest(self): + params = "service=WCS&version=2.0.0&request=DescribeEOCoverageSet&eoId=MER_FRS_1P_reduced§ions=All,DatasetSeriesDescriptions" + return (params, "kvp") + +class WCS20DescribeEOCoverageSetSectionsAll4TestCase(eoxstest.WCS20DescribeEOCoverageSetSectionsTestCase): + def getExpectedSections(self): + return [ + "wcs:CoverageDescriptions", + "wcseo:DatasetSeriesDescriptions" + ] + + def getRequest(self): + params = "service=WCS&version=2.0.0&request=DescribeEOCoverageSet&eoId=MER_FRS_1P_reduced§ions=CoverageDescriptions,All" + return (params, "kvp") + +class WCS20DescribeEOCoverageSetSectionsCoverageDescriptionsTestCase(eoxstest.WCS20DescribeEOCoverageSetSectionsTestCase): + def getExpectedSections(self): + return [ + "wcs:CoverageDescriptions" + ] + + def getRequest(self): + params = "service=WCS&version=2.0.0&request=DescribeEOCoverageSet&eoId=MER_FRS_1P_reduced§ions=CoverageDescriptions" + return (params, "kvp") + +class WCS20DescribeEOCoverageSetSectionsDatasetSeriesDescriptionsTestCase(eoxstest.WCS20DescribeEOCoverageSetSectionsTestCase): + def getExpectedSections(self): + return [ + "wcseo:DatasetSeriesDescriptions" + ] + + def getRequest(self): + params = "service=WCS&version=2.0.0&request=DescribeEOCoverageSet&eoId=MER_FRS_1P_reduced§ions=DatasetSeriesDescriptions" + return (params, "kvp") + +class WCS20DescribeEOCoverageSetSectionsFaultTestCase(eoxstest.ExceptionTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.0&request=DescribeEOCoverageSet&EOID=MER_FRS_1P_reduced§ions=WrongSection" + return (params, "kvp") + + def getExpectedHTTPStatus(self): + return 400 + + def getExpectedExceptionCode(self): + return "InvalidParameterValue" + + +class WCS20DescribeEOCoverageSetDatasetUniqueTestCase(eoxstest.WCS20DescribeEOCoverageSetSubsettingTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.0&request=DescribeEOCoverageSet&EOID=MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_uint16_reduced_compressed,MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_uint16_reduced_compressed" + return (params, "kvp") + + def getExpectedCoverageIds(self): + return [ + "MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_uint16_reduced_compressed" + ] + +class WCS20DescribeEOCoverageSetDatasetOutOfSubsetTestCase(eoxstest.WCS20DescribeEOCoverageSetSubsettingTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.0&request=DescribeEOCoverageSet&EOID=MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_uint16_reduced_compressed&ubset=lat,http://www.opengis.net/def/crs/EPSG/0/4326(0,1)&subset=long,http://www.opengis.net/def/crs/EPSG/0/4326(0,1)" + return (params, "kvp") + + def getExpectedCoverageIds(self): + return [] + +class WCS20DescribeEOCoverageSetDatasetSeriesStitchedMosaicTestCase(eoxstest.WCS20DescribeEOCoverageSetSubsettingTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.0&request=DescribeEOCoverageSet&EOID=MER_FRS_1P_reduced,mosaic_MER_FRS_1P_RGB_reduced" + return (params, "kvp") + + def getExpectedCoverageIds(self): + return [ + "MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_uint16_reduced_compressed", + "MER_FRS_1PNPDE20060822_092058_000001972050_00308_23408_0077_uint16_reduced_compressed", + "MER_FRS_1PNPDE20060830_100949_000001972050_00423_23523_0079_uint16_reduced_compressed", + "mosaic_MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_RGB_reduced", + "mosaic_MER_FRS_1PNPDE20060822_092058_000001972050_00308_23408_0077_RGB_reduced", + "mosaic_MER_FRS_1PNPDE20060830_100949_000001972050_00423_23523_0079_RGB_reduced", + "mosaic_MER_FRS_1P_RGB_reduced" + ] + +#=============================================================================== +# WCS 2.0: Exceptions +#=============================================================================== + +# after WCS 2.0.1 implementation does not lead to an error anymore +#class WCS20GetCoverageFormatMissingFaultTestCase(eoxstest.ExceptionTestCase): +# def getRequest(self): +# params = "service=wcs&version=2.0.0&request=GetCoverage&CoverageId=mosaic_MER_FRS_1P_RGB_reduced" +# return (params, "kvp") +# +# def getExpectedExceptionCode(self): +# return "MissingParameterValue" + +class WCS20GetCoverageNoSuchCoverageFaultTestCase(eoxstest.ExceptionTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.0&request=GetCoverage&CoverageId=INVALID" + return (params, "kvp") + + def getExpectedHTTPStatus(self): + return 404 + + def getExpectedExceptionCode(self): + return "NoSuchCoverage" + +class WCS20GetCoverageFormatUnsupportedFaultTestCase(eoxstest.ExceptionTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.0&request=GetCoverage&CoverageId=MER_FRS_1PNPDE20060822_092058_000001972050_00308_23408_0077_uint16_reduced_compressed&format=image/jpeg" + return (params, "kvp") + + def getExpectedExceptionCode(self): + return "InvalidParameterValue" + +class WCS20GetCoverageFormatUnknownFaultTestCase(eoxstest.ExceptionTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.0&request=GetCoverage&CoverageId=MER_FRS_1PNPDE20060822_092058_000001972050_00308_23408_0077_uint16_reduced_compressed&format=unknown" + return (params, "kvp") + + def getExpectedExceptionCode(self): + return "InvalidParameterValue" + +#=============================================================================== +# WCS 2.0: Simple requests +#=============================================================================== + +class WCS20GetCoverageMosaicTestCase(eoxstest.RectifiedGridCoverageTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.0&request=GetCoverage&CoverageId=mosaic_MER_FRS_1P_RGB_reduced&format=image/tiff" + return (params, "kvp") + +class WCS20GetCoverageDatasetTestCase(eoxstest.RectifiedGridCoverageTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.0&request=GetCoverage&CoverageId=MER_FRS_1PNPDE20060822_092058_000001972050_00308_23408_0077_uint16_reduced_compressed&format=image/tiff" + return (params, "kvp") + +#============================================================================== +# WCS 2.0: Formats +#============================================================================== + +# WCS 2.0.1 introduced the native format, i.e., default format in case of missing format specification +class WCS20GetCoverageNativeTestCase(eoxstest.RectifiedGridCoverageTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.1&request=GetCoverage&CoverageId=mosaic_MER_FRS_1P_RGB_reduced" + return (params, "kvp") + + def getFileExtension(self, part=None): + return "tif" + +class WCS20GetCoverageJPEG2000TestCase(eoxstest.RectifiedGridCoverageTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.0&request=GetCoverage&CoverageId=mosaic_MER_FRS_1P_RGB_reduced&format=image/jp2" + return (params, "kvp") + + def getFileExtension(self, part=None): + return "jp2" + +class WCS20GetCoverageNetCDFTestCase(eoxstest.RectifiedGridCoverageTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.0&request=GetCoverage&CoverageId=mosaic_MER_FRS_1P_RGB_reduced&format=application/x-netcdf" + return (params, "kvp") + + def getFileExtension(self, part=None): + return "nc" + +class WCS20GetCoverageHDFTestCase(eoxstest.RectifiedGridCoverageTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.0&request=GetCoverage&CoverageId=mosaic_MER_FRS_1P_RGB_reduced&format=application/x-hdf" + return (params, "kvp") + + def getFileExtension(self, part=None): + return "hdf" + +class WCS20GetCoverageCompressionLZWTestCase(eoxstest.RectifiedGridCoverageTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.0&request=GetCoverage&CoverageId=mosaic_MER_FRS_1P_RGB_reduced&format=%s" % quote("image/tiff;compress=LZW") + return (params, "kvp") + +class WCS20GetCoverageCompressionJPEGTestCase(eoxstest.RectifiedGridCoverageTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.0&request=GetCoverage&CoverageId=mosaic_MER_FRS_1P_RGB_reduced&format=%s" % quote("image/tiff;compress=JPEG;jpeg_quality=50") + return (params, "kvp") + +class WCS20GetCoverageTiledTestCase(eoxstest.RectifiedGridCoverageTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.0&request=GetCoverage&CoverageId=mosaic_MER_FRS_1P_RGB_reduced&format=%s" % quote ("image/tiff;tiled=YES") + return (params, "kvp") + +# TODO: Enable test once subdatasets are supported (see #123): +#class WCS20GetCoverageNetCDFInputTestCase(eoxstest.RectifiedGridCoverageTestCase): +# def getRequest(self): +# params = "service=wcs&version=2.0.0&request=GetCoverage&CoverageId=MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_uint16_reduced_compressed_netCDF&format=image/tiff" +# return (params, "kvp") + +class WCS20GetCoverageJPEG2000InputTestCase(eoxstest.RectifiedGridCoverageTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.0&request=GetCoverage&CoverageId=mosaic_MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_RGB_reduced_JPEG2000&format=image/tiff" + return (params, "kvp") + +#=============================================================================== +# WCS 2.0: Multipart requests +#=============================================================================== + +class WCS20GetCoverageMultipartMosaicTestCase(eoxstest.WCS20GetCoverageRectifiedGridCoverageMultipartTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.1&request=GetCoverage&CoverageId=mosaic_MER_FRS_1P_RGB_reduced&format=image/tiff&mediatype=multipart/related" + return (params, "kvp") + +class WCS20GetCoverageMultipartDatasetTestCase(eoxstest.WCS20GetCoverageRectifiedGridCoverageMultipartTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.1&request=GetCoverage&CoverageId=MER_FRS_1PNPDE20060822_092058_000001972050_00308_23408_0077_uint16_reduced_compressed&format=image/tiff&mediatype=multipart/related" + return (params, "kvp") + +# TODO: wrong multipart parameters only result in non-multipart images. Uncomment, when implemented +#class WCS20GetCoverageWrongMultipartParameterFaultTestCase(eoxstest.ExceptionTestCase): +# def getRequest(self): +# params = "service=wcs&version=2.0.1&request=GetCoverage&CoverageId=mosaic_MER_FRS_1P_RGB_reduced&format=image/tiff&mediatype=multipart/something" +# return (params, "kvp") +# +# def getExpectedExceptionCode(self): +# return "InvalidParameterValue" + +#=============================================================================== +# WCS 2.0: Subset requests +#=============================================================================== + +class WCS20GetCoverageSubsetDatasetTestCase(eoxstest.RectifiedGridCoverageTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.1&request=GetCoverage&CoverageId=MER_FRS_1PNPDE20060822_092058_000001972050_00308_23408_0077_uint16_reduced_compressed&format=image/tiff&subset=x(100,200)&subset=y(200,300)" + return (params, "kvp") + +class WCS20GetCoverageMultipartSubsetMosaicTestCase(eoxstest.WCS20GetCoverageRectifiedGridCoverageMultipartTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.1&request=GetCoverage&CoverageId=mosaic_MER_FRS_1P_RGB_reduced&format=image/tiff&mediatype=multipart/related&subset=x(100,1000)&subset=y(0,99)" + return (params, "kvp") + +class WCS20GetCoverageMultipartSubsetDatasetTestCase(eoxstest.WCS20GetCoverageRectifiedGridCoverageMultipartTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.1&request=GetCoverage&CoverageId=MER_FRS_1PNPDE20060822_092058_000001972050_00308_23408_0077_uint16_reduced_compressed&format=image/tiff&mediatype=multipart/related&subset=x(100,200)&subset=y(200,300)" + return (params, "kvp") + +class WCS20GetCoverageSubsetEPSG4326DatasetTestCase(eoxstest.RectifiedGridCoverageTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.1&request=GetCoverage&CoverageId=MER_FRS_1PNPDE20060822_092058_000001972050_00308_23408_0077_uint16_reduced_compressed&format=image/tiff&subset=lat,http://www.opengis.net/def/crs/EPSG/0/4326(38,40)&subset=long,http://www.opengis.net/def/crs/EPSG/0/4326(20,22)" + return (params, "kvp") + +class WCS20GetCoverageSubsetEPSG4326MosaicTestCase(eoxstest.RectifiedGridCoverageTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.1&request=GetCoverage&CoverageId=mosaic_MER_FRS_1P_RGB_reduced&format=image/tiff&subset=lat,http://www.opengis.net/def/crs/EPSG/0/4326(38,40)&subset=long,http://www.opengis.net/def/crs/EPSG/0/4326(0,30)" + return (params, "kvp") + +class WCS20GetCoverageSubsetInvalidEPSGFaultTestCase(eoxstest.ExceptionTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.1&request=GetCoverage&CoverageId=mosaic_MER_FRS_1P_RGB_reduced&format=image/tiff&subset=x,http://www.opengis.net/def/crs/EPSG/0/99999(38,40)&subset=y,http://www.opengis.net/def/crs/EPSG/0/99999(20,22)" + return (params, "kvp") + + def getExpectedExceptionCode(self): + return "InvalidParameterValue" + +#=============================================================================== +# WCS 2.0: OutputCRS +#=============================================================================== + +class WCS20GetCoverageOutputCRSDatasetTestCase(eoxstest.WCS20GetCoverageRectifiedGridCoverageMultipartTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.1&request=GetCoverage&CoverageId=MER_FRS_1PNPDE20060822_092058_000001972050_00308_23408_0077_uint16_reduced_compressed&format=image/tiff&mediatype=multipart/related&outputcrs=http://www.opengis.net/def/crs/EPSG/0/3035" + return (params, "kvp") + +class WCS20GetCoverageOutputCRSotherUoMDatasetTestCase(eoxstest.WCS20GetCoverageRectifiedGridCoverageMultipartTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.1&request=GetCoverage&CoverageId=MER_FRS_1PNPDE20060822_092058_000001972050_00308_23408_0077_uint16_reduced_compressed&format=image/tiff&mediatype=multipart/related&outputcrs=http://www.opengis.net/def/crs/EPSG/0/3857" + return (params, "kvp") + +#=============================================================================== +# WCS 2.0: Size +#=============================================================================== + +class WCS20GetCoverageSizeDatasetTestCase(eoxstest.RectifiedGridCoverageTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.0&request=GetCoverage&CoverageId=MER_FRS_1PNPDE20060822_092058_000001972050_00308_23408_0077_uint16_reduced_compressed&format=image/tiff&size=x(200)&size=y(200)" + return (params, "kvp") + +class WCS20GetCoverageSizeMosaicTestCase(eoxstest.RectifiedGridCoverageTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.0&request=GetCoverage&CoverageId=mosaic_MER_FRS_1P_RGB_reduced&format=image/tiff&size=x(200)&size=y(400)" + return (params, "kvp") + +class WCS20GetCoverageSubsetSizeDatasetTestCase(eoxstest.RectifiedGridCoverageTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.0&request=GetCoverage&CoverageId=MER_FRS_1PNPDE20060822_092058_000001972050_00308_23408_0077_uint16_reduced_compressed&format=image/tiff&subset=x(100,200)&subset=y(200,300)&size=x(20)&size=y(20)" + return (params, "kvp") + +class WCS20GetCoverageSubsetEPSG4326SizeDatasetTestCase(eoxstest.WCS20GetCoverageRectifiedGridCoverageMultipartTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.1&request=GetCoverage&CoverageId=MER_FRS_1PNPDE20060822_092058_000001972050_00308_23408_0077_uint16_reduced_compressed&format=image/tiff&mediatype=multipart/related&subset=lat,http://www.opengis.net/def/crs/EPSG/0/4326(38,40)&subset=long,http://www.opengis.net/def/crs/EPSG/0/4326(20,22)&size=lat(20)&size=long(20)" + return (params, "kvp") + +class WCS20GetCoverageSubsetEPSG4326SizeExceedsExtentDatasetTestCase(eoxstest.WCS20GetCoverageRectifiedGridCoverageMultipartTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.1&request=GetCoverage&CoverageId=MER_FRS_1PNPDE20060822_092058_000001972050_00308_23408_0077_uint16_reduced_compressed&format=image/tiff&subset=lat,http://www.opengis.net/def/crs/EPSG/0/4326(10,50)&subset=long,http://www.opengis.net/def/crs/EPSG/0/4326(0,50)&size=lat(100)&size=long(100)&mediatype=multipart/related" + return (params, "kvp") + +class WCS20GetCoverageInvalidSizeFaultTestCase(eoxstest.ExceptionTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.0&request=GetCoverage&CoverageId=mosaic_MER_FRS_1P_RGB_reduced&format=image/tiff&size=x(1.11)" + return (params, "kvp") + + def getExpectedExceptionCode(self): + return "InvalidParameterValue" + +#=============================================================================== +# WCS 2.0: Resolution +#=============================================================================== + +class WCS20GetCoverageResolutionDatasetTestCase(eoxstest.RectifiedGridCoverageTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.0&request=GetCoverage&CoverageId=MER_FRS_1PNPDE20060822_092058_000001972050_00308_23408_0077_uint16_reduced_compressed&format=image/tiff&resolution=x(0.1)&resolution=y(0.1)" + return (params, "kvp") + +class WCS20GetCoverageResolutionMosaicTestCase(eoxstest.RectifiedGridCoverageTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.0&request=GetCoverage&CoverageId=mosaic_MER_FRS_1P_RGB_reduced&format=image/tiff&resolution=x(0.1)&resolution=y(0.1)" + return (params, "kvp") + +class WCS20GetCoverageSubsetResolutionDatasetTestCase(eoxstest.RectifiedGridCoverageTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.0&request=GetCoverage&CoverageId=MER_FRS_1PNPDE20060822_092058_000001972050_00308_23408_0077_uint16_reduced_compressed&format=image/tiff&subset=x(100,200)&subset=y(200,300)&resolution=x(0.1)&resolution=y(0.1)" + return (params, "kvp") + +class WCS20GetCoverageSubsetEPSG4326ResolutionLatLonDatasetTestCase(eoxstest.RectifiedGridCoverageTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.0&request=GetCoverage&CoverageId=MER_FRS_1PNPDE20060822_092058_000001972050_00308_23408_0077_uint16_reduced_compressed&format=image/tiff&subset=lat,http://www.opengis.net/def/crs/EPSG/0/4326(38,40)&subset=long,http://www.opengis.net/def/crs/EPSG/0/4326(20,22)&resolution=lat(0.01)&resolution=long(0.01)" + return (params, "kvp") + +class WCS20GetCoverageSubsetEPSG4326ResolutionInvalidAxisDatasetFaultTestCase(eoxstest.ExceptionTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.0&request=GetCoverage&CoverageId=MER_FRS_1PNPDE20060822_092058_000001972050_00308_23408_0077_uint16_reduced_compressed&format=image/tiff&subset=lat,http://www.opengis.net/def/crs/EPSG/0/4326(38,40)&subset=long,http://www.opengis.net/def/crs/EPSG/0/4326(20,22)&resolution=x(0.01)&resolution=y(0.01)" + return (params, "kvp") + + def getExpectedExceptionCode(self): + return "InvalidParameterValue" + +#=============================================================================== +# WCS 2.0: Rangesubset +#=============================================================================== + +class WCS20GetCoverageRangeSubsetIndicesDatasetTestCase(eoxstest.RectifiedGridCoverageTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.0&request=GetCoverage&CoverageId=MER_FRS_1PNPDE20060822_092058_000001972050_00308_23408_0077_uint16_reduced_compressed&format=image/tiff&rangesubset=1,2,3" + return (params, "kvp") + +class WCS20GetCoverageRangeSubsetNamesDatasetTestCase(eoxstest.RectifiedGridCoverageTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.0&request=GetCoverage&CoverageId=MER_FRS_1PNPDE20060822_092058_000001972050_00308_23408_0077_uint16_reduced_compressed&format=image/tiff&rangesubset=MERIS_radiance_04_uint16,MERIS_radiance_05_uint16,MERIS_radiance_06_uint16" + return (params, "kvp") + +class WCS20GetCoverageMultipartRangeSubsetNamesDatasetTestCase(eoxstest.WCS20GetCoverageRectifiedGridCoverageMultipartTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.1&request=GetCoverage&CoverageId=MER_FRS_1PNPDE20060822_092058_000001972050_00308_23408_0077_uint16_reduced_compressed&format=image/tiff&mediatype=multipart/related&rangesubset=MERIS_radiance_04_uint16,MERIS_radiance_05_uint16,MERIS_radiance_06_uint16" + return (params, "kvp") + +class WCS20GetCoverageSubsetSizeResolutionOutputCRSRangeSubsetIndicesDatasetTestCase(eoxstest.WCS20GetCoverageRectifiedGridCoverageMultipartTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.1&request=GetCoverage&CoverageId=MER_FRS_1PNPDE20060822_092058_000001972050_00308_23408_0077_uint16_reduced_compressed&format=image/tiff&subset=x(100,200)&subset=y(200,300)&size=y(100)&resolution=x(0.1)&outputcrs=http://www.opengis.net/def/crs/EPSG/0/3035&rangesubset=1,2,3&mediatype=multipart/related" + return (params, "kvp") + +#=============================================================================== +# WCS 2.0: Polygon Mask +#=============================================================================== + +# TODO: Enable these tests once the feature is implemented in MapServer + +#class WCS20GetCoveragePolygonMaskTestCase(eoxstest.RectifiedGridCoverageTestCase): +# def getRequest(self): +# params = "service=wcs&version=2.0.0&request=GetCoverage&CoverageId=MER_FRS_1PNPDE20060822_092058_000001972050_00308_23408_0077_uint16_reduced_compressed&format=image/tiff&mask=polygon(14.124422306243844,42.806626286621963,21.208516509273601,43.730638573973678,21.208516509273601,43.730638573973678,21.892970055460054,37.8443380767702,15.04843459359555,36.646544370943914,12.379065763468395,39.555471942236323,14.124422306243844,42.806626286621963)" +# return (params, "kvp") + + +#class WCS20GetCoveragePolygonMaskProjectedTestCase(eoxstest.RectifiedGridCoverageTestCase): +# def getRequest(self): # TODO: swap axes +# params = "service=wcs&version=2.0.0&request=GetCoverage&CoverageId=MER_FRS_1PNPDE20060822_092058_000001972050_00308_23408_0077_uint16_reduced_compressed&format=image/tiff&mask=polygon,http://www.opengis.net/def/crs/EPSG/0/4326(42.806626286621963,14.124422306243844,43.730638573973678,21.208516509273601,43.730638573973678,21.208516509273601,37.8443380767702,21.892970055460054,36.646544370943914,15.04843459359555,39.555471942236323,12.379065763468395,42.806626286621963,14.124422306243844)" +# return (params, "kvp") + +#class WCS20PostGetCoveragePolygonMaskTestCase(eoxstest.RectifiedGridCoverageTestCase): +# def getRequest(self): +# params = """ +# MER_FRS_1PNPDE20060822_092058_000001972050_00308_23408_0077_uint16_reduced_compressed +# image/tiff +# +# 14.124422306243844 42.806626286621963 21.208516509273601 43.730638573973678 21.208516509273601 43.730638573973678 21.892970055460054 37.8443380767702 15.04843459359555 36.646544370943914 12.379065763468395 39.555471942236323 14.124422306243844 42.806626286621963 +# +# """ +# return (params, "xml") + +#=============================================================================== +# WCS 2.0 Rasdaman test cases +#=============================================================================== + +class WCS20GetCoverageRasdamanMultipartDatasetTestCase(eoxstest.RasdamanTestCaseMixIn, eoxstest.WCS20GetCoverageRectifiedGridCoverageMultipartTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.1&request=GetCoverage&CoverageId=mosaic_MER_FRS_1PNPDE20060830_100949_000001972050_00423_23523_0079_RGB_reduced_rasdaman&format=image/tiff&mediatype=multipart/related" + return (params, "kvp") + +class WCS20GetCoverageRasdamanMultipartDatasetSubsetTestCase(eoxstest.RasdamanTestCaseMixIn, eoxstest.WCS20GetCoverageRectifiedGridCoverageMultipartTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.1&request=GetCoverage&CoverageId=mosaic_MER_FRS_1PNPDE20060830_100949_000001972050_00423_23523_0079_RGB_reduced_rasdaman&format=image/tiff&mediatype=multipart/related&subset=x(100,200)&subset=y(200,300)" + return (params, "kvp") + +class WCS20GetCoverageRasdamanMultipartDatasetSizeTestCase(eoxstest.RasdamanTestCaseMixIn, eoxstest.WCS20GetCoverageRectifiedGridCoverageMultipartTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.1&request=GetCoverage&CoverageId=mosaic_MER_FRS_1PNPDE20060830_100949_000001972050_00423_23523_0079_RGB_reduced_rasdaman&format=image/tiff&mediatype=multipart/related&size=x(100)&size=y(100)" + return (params, "kvp") + +class WCS20GetCoverageRasdamanMultipartDatasetResolutionTestCase(eoxstest.RasdamanTestCaseMixIn, eoxstest.WCS20GetCoverageRectifiedGridCoverageMultipartTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.1&request=GetCoverage&CoverageId=mosaic_MER_FRS_1PNPDE20060830_100949_000001972050_00423_23523_0079_RGB_reduced_rasdaman&format=image/tiff&mediatype=multipart/related&resolution=x(0.1)&resolution=y(0.1)" + return (params, "kvp") + +class WCS20GetCoverageRasdamanMultipartDatasetOutputCRSTestCase(eoxstest.RasdamanTestCaseMixIn, eoxstest.WCS20GetCoverageRectifiedGridCoverageMultipartTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.1&request=GetCoverage&CoverageId=mosaic_MER_FRS_1PNPDE20060830_100949_000001972050_00423_23523_0079_RGB_reduced_rasdaman&format=image/tiff&mediatype=multipart/related&outputcrs=http://www.opengis.net/def/crs/EPSG/0/3035" + return (params, "kvp") + +class WCS20GetCoverageRasdamanMultipartDatasetSubsetSizeTestCase(eoxstest.RasdamanTestCaseMixIn, eoxstest.WCS20GetCoverageRectifiedGridCoverageMultipartTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.1&request=GetCoverage&CoverageId=mosaic_MER_FRS_1PNPDE20060830_100949_000001972050_00423_23523_0079_RGB_reduced_rasdaman&format=image/tiff&mediatype=multipart/related&subset=x(100,200)&subset=y(200,300)&size=x(20)&size=y(20)" + return (params, "kvp") + +class WCS20GetCoverageRasdamanMultipartDatasetSubsetResolutionTestCase(eoxstest.RasdamanTestCaseMixIn, eoxstest.WCS20GetCoverageRectifiedGridCoverageMultipartTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.1&request=GetCoverage&CoverageId=mosaic_MER_FRS_1PNPDE20060830_100949_000001972050_00423_23523_0079_RGB_reduced_rasdaman&format=image/tiff&mediatype=multipart/related&subset=x(100,200)&subset=y(200,300)&resolution=x(0.1)&resolution=y(0.1)" + return (params, "kvp") + +class WCS20GetCoverageRasdamanMultipartDatasetRangeSubsetTestCase(eoxstest.RasdamanTestCaseMixIn, eoxstest.WCS20GetCoverageRectifiedGridCoverageMultipartTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.1&request=GetCoverage&CoverageId=mosaic_MER_FRS_1PNPDE20060830_100949_000001972050_00423_23523_0079_RGB_reduced_rasdaman&format=image/tiff&mediatype=multipart/related&rangesubset=1" + return (params, "kvp") + +class WCS20GetCoverageRasdamanSubsetSizeResolutionOutputCRSRangeSubsetIndicesDatasetTestCase(eoxstest.RasdamanTestCaseMixIn, eoxstest.WCS20GetCoverageRectifiedGridCoverageMultipartTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.1&request=GetCoverage&CoverageId=mosaic_MER_FRS_1PNPDE20060830_100949_000001972050_00423_23523_0079_RGB_reduced_rasdaman&format=image/tiff&subset=x(100,200)&subset=y(200,300)&size=y(100)&resolution=x(0.1)&outputcrs=http://www.opengis.net/def/crs/EPSG/0/3035&rangesubset=1,2,3&mediatype=multipart/related" + return (params, "kvp") + + +#=============================================================================== +# WCS 2.0: GetCov with EPSG:3035 input images +#=============================================================================== + +class WCS20DescribeCoverageReprojectedDatasetTestCase(eoxstest.XMLTestCase): + fixtures = eoxstest.OWSTestCase.fixtures + ["testing_reprojected_coverages.json"] + def getRequest(self): + params = "service=WCS&version=2.0.0&request=DescribeCoverage&CoverageId=MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_uint16_reduced_compressed_reprojected" + return (params, "kvp") + +class WCS20GetCoverageReprojectedDatasetTestCase(eoxstest.RectifiedGridCoverageTestCase): + fixtures = eoxstest.OWSTestCase.fixtures + ["testing_reprojected_coverages.json"] + def getRequest(self): + params = "service=wcs&version=2.0.0&request=GetCoverage&CoverageId=MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_uint16_reduced_compressed_reprojected&format=image/tiff" + return (params, "kvp") + +class WCS20GetCoverageReprojectedSubsetDatasetTestCase(eoxstest.RectifiedGridCoverageTestCase): + fixtures = eoxstest.OWSTestCase.fixtures + ["testing_reprojected_coverages.json"] + def getRequest(self): + params = "service=wcs&version=2.0.0&request=GetCoverage&CoverageId=MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_uint16_reduced_compressed_reprojected&format=image/tiff&subset=x(100,200)&subset=y(200,300)" + return (params, "kvp") + +class WCS20GetCoverageReprojectedSubsetEPSG4326DatasetTestCase(eoxstest.WCS20GetCoverageRectifiedGridCoverageMultipartTestCase): + fixtures = eoxstest.OWSTestCase.fixtures + ["testing_reprojected_coverages.json"] + def getRequest(self): + params = "service=wcs&version=2.0.1&request=GetCoverage&CoverageId=MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_uint16_reduced_compressed_reprojected&format=image/tiff&mediatype=multipart/related&subset=lat,http://www.opengis.net/def/crs/EPSG/0/4326(38,40)&subset=long,http://www.opengis.net/def/crs/EPSG/0/4326(20,22)" + return (params, "kvp") + +class WCS20GetCoverageReprojectedMultipartDatasetTestCase(eoxstest.WCS20GetCoverageRectifiedGridCoverageMultipartTestCase): + fixtures = eoxstest.OWSTestCase.fixtures + ["testing_reprojected_coverages.json"] + def getRequest(self): + params = "service=wcs&version=2.0.1&request=GetCoverage&CoverageId=MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_uint16_reduced_compressed_reprojected&format=image/tiff&mediatype=multipart/related" + return (params, "kvp") + +class WCS20GetCoverageReprojectedEPSG3857DatasetTestCase(eoxstest.WCS20GetCoverageRectifiedGridCoverageMultipartTestCase): + fixtures = eoxstest.OWSTestCase.fixtures + ["testing_reprojected_coverages.json"] + def getRequest(self): + params = "service=wcs&version=2.0.1&request=GetCoverage&CoverageId=MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_uint16_reduced_compressed_reprojected&format=image/tiff&mediatype=multipart/related&outputcrs=http://www.opengis.net/def/crs/EPSG/0/3857" + return (params, "kvp") + +#=============================================================================== +# WCS 2.0 Referenceable Grid Coverages +#=============================================================================== + +class WCS20DescribeCoverageReferenceableDatasetTestCase(eoxstest.XMLTestCase): + """This test shall retrieve a valid WCS 2.0 EO-AP (EO-WCS) DescribeCoverage response for a wcseo:ReferenceableDataset.""" + def getRequest(self): + params = "service=WCS&version=2.0.0&request=DescribeCoverage&CoverageId=ASA_WSM_1PNDPA20050331_075939_000000552036_00035_16121_0775" + return (params, "kvp") + +class WCS20GetCoverageReferenceableDatasetTestCase(eoxstest.WCS20GetCoverageReferenceableGridCoverageMultipartTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.1&request=GetCoverage&CoverageId=ASA_WSM_1PNDPA20050331_075939_000000552036_00035_16121_0775&format=image/tiff&mediatype=multipart/related" + return (params, "kvp") + +class WCS20GetCoverageReferenceableDatasetImageCRSSubsetTestCase(eoxstest.WCS20GetCoverageReferenceableGridCoverageMultipartTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.1&request=GetCoverage&CoverageId=ASA_WSM_1PNDPA20050331_075939_000000552036_00035_16121_0775&format=image/tiff&mediatype=multipart/related&subset=x(0,99)&subset=y(0,99)" + return (params, "kvp") + +class WCS20GetCoverageReferenceableDatasetGeogCRSSubsetTestCase(eoxstest.WCS20GetCoverageReferenceableGridCoverageMultipartTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.0&request=GetCoverage&CoverageId=ASA_WSM_1PNDPA20050331_075939_000000552036_00035_16121_0775&format=image/tiff&mediatype=multipart/mixed&subset=x,http://www.opengis.net/def/crs/EPSG/0/4326(18.0,20.0)&subset=y,http://www.opengis.net/def/crs/EPSG/0/4326(-34.5,-33.5)" + return (params, "kvp") + +class WCS20GetCoverageReferenceableDatasetGeogCRSSubsetExceedsExtentTestCase(eoxstest.WCS20GetCoverageReferenceableGridCoverageMultipartTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.0&request=GetCoverage&CoverageId=ASA_WSM_1PNDPA20050331_075939_000000552036_00035_16121_0775&format=image/tiff&mediatype=multipart/mixed&subset=x,http://www.opengis.net/def/crs/EPSG/0/4326(18,23)&subset=y,http://www.opengis.net/def/crs/EPSG/0/4326(-35,-33)" + return (params, "kvp") + +class WCS20GetCoverageReferenceableDatasetGeogCRSSubsetOutsideExtentTestCase(eoxstest.ExceptionTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.0&request=GetCoverage&CoverageId=ASA_WSM_1PNDPA20050331_075939_000000552036_00035_16121_0775&format=image/tiff&mediatype=multipart/mixed&subset=x,http://www.opengis.net/def/crs/EPSG/0/4326(14.5,16.5)&subset=y,http://www.opengis.net/def/crs/EPSG/0/4326(-34.5,-33.5)" + return (params, "kvp") + + def getExpectedHTTPStatus(self): + return 400 + + def getExpectedExceptionCode(self): + return "InvalidParameterValue" + +#=============================================================================== +# WCS 2.0.1 Corrigendum test cases +#=============================================================================== + + +class WCS20CorrigendumGetCapabilitiesEmptyTestCase(eoxstest.XMLTestCase): + """This test shall retrieve a valid but empty WCS 2.0.1 EO-AP (EO-WCS) GetCapabilities response (see #162)""" + fixtures = BASE_FIXTURES + + def getRequest(self): + params = "service=WCS&version=2.0.1&request=GetCapabilities" + return (params, "kvp") + + +class WCS20CorrigendumDescribeCoverageDatasetTestCase(eoxstest.XMLTestCase): + """This test shall retrieve a valid WCS 2.0.1 EO-AP (EO-WCS) DescribeCoverage response for a wcseo:RectifiedDataset (see #162).""" + def getRequest(self): + params = "service=WCS&version=2.0.1&request=DescribeCoverage&CoverageId=MER_FRS_1PNPDE20060822_092058_000001972050_00308_23408_0077_uint16_reduced_compressed" + return (params, "kvp") + + +class WCS20CorrigendumDescribeEOCoverageSetMosaicTestCase(eoxstest.XMLTestCase): + """This test shall retrieve a valid WCS 2.0.1 EO-AP (EO-WCS) DescribeEOCoverageSet response for a wcseo:RectifiedStitchedMosaic (see #162)""" + def getRequest(self): + params = "service=WCS&version=2.0.1&request=DescribeEOCoverageSet&eoId=mosaic_MER_FRS_1P_RGB_reduced" + return (params, "kvp") + + +class WCS20CorrigendumGetCoverageDatasetTestCase(eoxstest.RectifiedGridCoverageTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.1&request=GetCoverage&CoverageId=MER_FRS_1PNPDE20060822_092058_000001972050_00308_23408_0077_uint16_reduced_compressed" + return (params, "kvp") + + +#=============================================================================== +# WCS 2.0 - POST +#=============================================================================== + +class WCS20PostGetCapabilitiesValidTestCase(eoxstest.XMLTestCase): + """This test shall retrieve a valid WCS 2.0 EO-AP (EO-WCS) GetCapabilities response + via POST. + """ + def getRequest(self): + params = """ + 2.0.0 + + """ + return (params, "xml") + +class WCS20PostDescribeCoverageDatasetTestCase(eoxstest.XMLTestCase): + """This test shall retrieve a valid WCS 2.0 EO-AP (EO-WCS) DescribeCoverage response + for a wcseo:RectifiedDataset via POST. + """ + def getRequest(self): + params = """ + MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_uint16_reduced_compressed + """ + return (params, "xml") + +class WCS20PostDescribeEOCoverageSetDatasetSeriesTestCase(eoxstest.XMLTestCase): + """This test shall retrieve a valid WCS 2.0 EO-AP (EO-WCS) DescribeEOCoverageSet response + for a wcseo:RectifiedDatasetSeries via POST. + """ + def getRequest(self): + params = """ + MER_FRS_1P_reduced + OVERLAPS + + All + + + Long + 16 + 18 + + + Lat + 46 + 48 + + """ + return (params, "xml") + +class WCS20PostGetCoverageMultipartDatasetTestCase(eoxstest.WCS20GetCoverageRectifiedGridCoverageMultipartTestCase): + def getRequest(self): + params = """ + mosaic_MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_RGB_reduced + image/tiff + multipart/related + """ + return (params, "xml") + +class WCS20PostGetCoverageSubsetMultipartDatasetTestCase(eoxstest.WCS20GetCoverageRectifiedGridCoverageMultipartTestCase): + def getRequest(self): + params = """ + mosaic_MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_RGB_reduced + + x + 0 + 99 + + + y + 0 + 99 + + image/tiff + multipart/related + """ + return (params, "xml") + +# TODO: Enable once CRS handling with POST requests is implemented in MapServer +# TODO: Adjust to final namespace maybe "http://www.opengis.net/wcs_service-extension_crs/1.0" +#class WCS20PostGetCoverageSubsetEPSG4326MultipartDatasetTestCase(eoxstest.WCS20GetCoverageRectifiedGridCoverageMultipartTestCase): +# def getRequest(self): +# params = """ +# +# +# http://www.opengis.net/def/crs/EPSG/0/4326 +# +# +# mosaic_MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_RGB_reduced +# +# Long +# 20 +# 22 +# +# +# Lat +# 36 +# 38 +# +# image/tiff +# multipart/related +# """ +# return (params, "xml") + +class WCS20PostGetCoverageReferenceableMultipartDatasetTestCase(eoxstest.WCS20GetCoverageReferenceableGridCoverageMultipartTestCase): + def getRequest(self): + params = """ + ASA_WSM_1PNDPA20050331_075939_000000552036_00035_16121_0775 + + x + 0 + 100 + + + y + 0 + 100 + + image/tiff + multipart/related + """ + return (params, "xml") + +#=============================================================================== +# WCS 1.1 - POST +#=============================================================================== + +class WCS11PostGetCapabilitiesValidTestCase(eoxstest.XMLTestCase): + """This test shall retrieve a valid WCS 1.1 GetCapabilities response via POST. + """ + def getRequest(self): + params = """""" + return (params, "xml") + +class WCS11PostDescribeCoverageDatasetTestCase(eoxstest.XMLTestCase): + """This test shall retrieve a valid WCS 1.1 DescribeCoverage response for a + wcseo:RectifiedDataset via POST. + """ + def getRequest(self): + params = """ + mosaic_MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_RGB_reduced + """ + return (params, "xml") + +class WCS11PostDescribeCoverageMosaicTestCase(eoxstest.XMLTestCase): + """This test shall retrieve a valid WCS 1.1 DescribeCoverage response for a + wcseo:RectifiedStitchedMosaic via POST. + """ + def getRequest(self): + params = """ + mosaic_MER_FRS_1P_RGB_reduced + """ + return (params, "xml") + +class WCS11PostGetCoverageDatasetTestCase(eoxstest.RectifiedGridCoverageMultipartTestCase): + def getRequest(self): + params = """ + mosaic_MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_RGB_reduced + + + 32 12 + 46.5 28 + + + + """ + return (params, "xml") + +class WCS11PostGetCoverageMosaicTestCase(eoxstest.RectifiedGridCoverageMultipartTestCase): + def getRequest(self): + params = """ + mosaic_MER_FRS_1P_RGB_reduced + + + 32 -4 + 46.5 28 + + + + """ + return (params, "xml") + +# TODO: Not working because of a bug in MapServer +#class WCS11PostGetCoverageDatasetSubsetTestCase(eoxstest.RectifiedGridCoverageMultipartTestCase): +# def getRequest(self): +# params = """ +# mosaic_MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_RGB_reduced +# +# +# 0 0 +# 550 440 +# +# +# +# +# urn:ogc:def:crs:EPSG::4326 +# urn:ogc:def:method:WCS:1.1:2dGridIn2dCrs +# 0 0 +# 2 2 +# urn:ogc:def:crs:OGC::imageCRS +# +# +# """ +# return (params, "xml") + +#class WCS11PostGetCoverageDatasetSubsetEPSG4326TestCase(eoxstest.RectifiedGridCoverageMultipartTestCase): +# def getRequest(self): +# params = """""" +##boundingbox=32,12,46.5,28,urn:ogc:def:crs:EPSG::4326&GridCS=urn:ogc:def:crs:OGC::imageCRS&GridType=urn:ogc:def:method:WCS:1.1:2dGridIn2dCrs&GridOrigin=46.5,12&GridOffsets=0.06,0.06" +# return (params, "xml") + +#class WCS11PostGetCoverageMosaicSubsetTestCase(eoxstest.RectifiedGridCoverageMultipartTestCase): +# def getRequest(self): +# params = """""" +##boundingbox=300,200,700,350,urn:ogc:def:crs:OGC::imageCRS&GridCS=urn:ogc:def:crs:OGC::imageCRS&GridType=urn:ogc:def:method:WCS:1.1:2dGridIn2dCrs&GridOrigin=0,0&GridOffsets=2,2" +# return (params, "xml") + +#class WCS11PostGetCoverageMosaicSubsetEPSG4326TestCase(eoxstest.RectifiedGridCoverageMultipartTestCase): +# def getRequest(self): +# params = """""" +##boundingbox=35,10,42,20,urn:ogc:def:crs:EPSG::4326&GridCS=urn:ogc:def:crs:OGC::imageCRS&GridType=urn:ogc:def:method:WCS:1.1:2dGridIn2dCrs&GridOrigin=40,10&GridOffsets=-0.06,0.06" +# return (params, "xml") + + +#=============================================================================== +# WMS +#=============================================================================== + +class WMS11GetCapabilitiesValidTestCase(eoxstest.OWSTestCase): + """This test shall retrieve a valid WMS 1.1 GetCapabilities response""" + def getRequest(self): + params = "service=WMS&version=1.1.1&request=GetCapabilities" + return (params, "kvp") + +class WMS13GetCapabilitiesValidTestCase(eoxstest.XMLTestCase): + """This test shall retrieve a valid WMS 1.3 GetCapabilities response""" + def getRequest(self): + params = "service=WMS&version=1.3.0&request=GetCapabilities" + return (params, "kvp") + +class WMS13GetCapabilitiesEmptyTestCase(eoxstest.XMLTestCase): + """This test shall retrieve a valid but empty WMS 1.3 GetCapabilities response (see #41)""" + fixtures = BASE_FIXTURES + + def getRequest(self): + params = "service=WMS&version=1.3.0&request=GetCapabilities" + return (params, "kvp") + +class WMS13GetMapDatasetTestCase(eoxstest.WMS13GetMapTestCase): + """ Test a GetMap request with a simple dataset. """ + layers = ("mosaic_MER_FRS_1PNPDE20060822_092058_000001972050_00308_23408_0077_RGB_reduced",) + bbox = (8.487755775451660, 32.195316643454134, 25.407486727461219, 46.249103546142578) + +class WMS13GetMapNoServiceParameterTestCase(eoxstest.RasterTestCase): + """This test shall retrieve a map while omitting the service parameter. """ + def getRequest(self): + params = "version=1.3.0&request=GetMap&" \ + "layers=mosaic_MER_FRS_1P_RGB_reduced&styles=&crs=epsg:4326&" \ + "bbox=35,10,45,20&width=100&height=100&format=image/tiff" + return (params, "kvp") + +class WMS11GetMapMultipleDatasetsTestCase(eoxstest.WMS11GetMapTestCase): + """ Test a GetMap request with two datasets. """ + layers = ("mosaic_MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_RGB_reduced", + "mosaic_MER_FRS_1PNPDE20060830_100949_000001972050_00423_23523_0079_RGB_reduced", + ) + width = 200 + bbox = (-3.75, 32.19025, 28.29481, 46.268645) + +class WMS13GetMapMultipleDatasetsTestCase(eoxstest.WMS13GetMapTestCase): + """ Test a GetMap request with two datasets. """ + layers = ("mosaic_MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_RGB_reduced", + "mosaic_MER_FRS_1PNPDE20060830_100949_000001972050_00423_23523_0079_RGB_reduced", + ) + width = 200 + bbox = (-3.75, 32.19025, 28.29481, 46.268645) + +class WMS13GetMapDatasetMultispectralTestCase(eoxstest.WMS13GetMapTestCase): + """ Test a GetMap request with a dataset containing 15 bands. """ + layers = ("MER_FRS_1PNPDE20060822_092058_000001972050_00308_23408_0077_uint16_reduced_compressed",) + bbox = (8.487755775451660, 32.195316643454134, 25.407486727461219, 46.249103546142578) + +class WMS13GetMapMosaicTestCase(eoxstest.WMS13GetMapTestCase): + """ Test a GetMap request with a stitched mosaic. """ + layers = ("mosaic_MER_FRS_1P_RGB_reduced",) + bbox = (-3.75, 32.158895, 28.326165, 46.3) + width = 200 + +class WMS13GetMapPNGDatasetTestCase(eoxstest.WMS13GetMapTestCase): + """ Test a GetMap request with a dataset series. """ + layers = ("mosaic_MER_FRS_1PNPDE20060822_092058_000001972050_00308_23408_0077_RGB_reduced",) + bbox = (8.5, 32.2, 25.4, 46.3) + frmt = "image/png" + +class WMS13GetMapGIFDatasetTestCase(eoxstest.WMS13GetMapTestCase): + """ Test a GetMap request with a dataset series. """ + layers = ("mosaic_MER_FRS_1PNPDE20060822_092058_000001972050_00308_23408_0077_RGB_reduced",) + bbox = (8.5, 32.2, 25.4, 46.3) + frmt = "image/gif" + +class WMS13GetMapTIFFDatasetTestCase(eoxstest.WMS13GetMapTestCase): + """ Test a GetMap request with a dataset series. """ + layers = ("mosaic_MER_FRS_1PNPDE20060822_092058_000001972050_00308_23408_0077_RGB_reduced",) + bbox = (8.5, 32.2, 25.4, 46.3) + frmt = "image/tiff" + +class WMS13GetMapLayerNotDefinedFaultTestCase(eoxstest.WMS13ExceptionTestCase): + def getRequest(self): + params = "service=WMS&version=1.3.0&request=GetMap&layers=INVALIDLAYER&bbox=0,0,1,1&crs=EPSG:4326&width=10&height=10&exceptions=XML&format=image/png" + return (params, "kvp") + + def getExpectedExceptionCode(self): + return "LayerNotDefined" + +class WMS13GetMapFormatUnknownFaultTestCase(eoxstest.WMS13ExceptionTestCase): + def getRequest(self): + params = "service=WMS&version=1.3.0&request=GetMap&layers=MER_FRS_1P_reduced&bbox=-32,-4,46,28&crs=EPSG:4326&width=100&height=100&format=image/INVALID&exceptions=application/vnd.ogc.se_xml" + return (params, "kvp") + + def getExpectedExceptionCode(self): + return "InvalidFormat" + +class WMS13GetMapInvalidBoundingBoxTestCase(eoxstest.WMS13ExceptionTestCase): + def getRequest(self): + params = "service=WMS&version=1.3.0&request=GetMap&layers=MER_FRS_1P_reduced&bbox=1,2,3&crs=EPSG:4326&width=100&height=100&format=image/jpeg&exceptions=application/vnd.ogc.se_xml" + return (params, "kvp") + + def getExpectedExceptionCode(self): + return "InvalidParameterValue" + +class WMS13GetMapInvalidCRSTestCase(eoxstest.WMS13ExceptionTestCase): + def getRequest(self): + params = "service=WMS&version=1.3.0&request=GetMap&layers=MER_FRS_1P_reduced&bbox=0,0,1,1&crs=INVALIDCRS&width=100&height=100&format=image/jpeg&exceptions=application/vnd.ogc.se_xml" + return (params, "kvp") + + def getExpectedExceptionCode(self): + return "InvalidCRS" + +class WMS13GetMapReferenceableGridTestCase(eoxstest.WMS13GetMapTestCase): + layers = ("ASA_WSM_1PNDPA20050331_075939_000000552036_00035_16121_0775", ) + bbox = (17.0, -36.0, 22.0, -32.0) + width = 500 + height = 400 + +class WMS13GetMapReferenceableGridReprojectionTestCase(eoxstest.WMS13GetMapTestCase): + layers = ("ASA_WSM_1PNDPA20050331_075939_000000552036_00035_16121_0775", ) + crs = "epsg:32734" + bbox = (122043.08622624225, 6008645.867004246, 594457.4634022854, 6459127.468615601) + width = 472 + height = 451 + swap_axes = False + +class WMS13GetMapDatasetSeriesTestCase(eoxstest.WMS13GetMapTestCase): + """ Test a GetMap request with a dataset series. """ + layers = ("MER_FRS_1P_RGB_reduced",) + width = 200 + bbox = (-3.75, 32.158895, 28.326165, 46.3) + +class WMS13GetMapDatasetSeriesTimePointTestCase(eoxstest.WMS13GetMapTestCase): + layers = ("MER_FRS_1P_RGB_reduced",) + width = 200 + bbox = (-3.75, 32.158895, 28.326165, 46.3) + time = "2006-08-30T10:09:49Z" + +class WMS13GetMapDatasetSeriesTimeIntervalTestCase(eoxstest.WMS13GetMapTestCase): + layers = ("MER_FRS_1P_RGB_reduced",) + width = 200 + bbox = (-3.75, 32.158895, 28.326165, 46.3) + time = "2006-08-01T00:00:00Z/2006-08-22T23:59:59Z" + +class WMS13GetMapDatasetSeriesOutlinesTestCase(eoxstest.WMS13GetMapTestCase): + layers = ("MER_FRS_1P_reduced_outlines",) + width = 200 + bbox = (-3.75, 32.158895, 28.326165, 46.3) + +class WMS13GetMapRectifiedStitchedMosaicOutlinesTestCase(eoxstest.WMS13GetMapTestCase): + layers = ("mosaic_MER_FRS_1P_RGB_reduced_outlines",) + width = 200 + bbox = (-3.75, 32.158895, 28.326165, 46.3) + +class WMS13GetMapRectifiedStitchedMosaicOutlinesWhiteTestCase(eoxstest.WMS13GetMapTestCase): + layers = ("mosaic_MER_FRS_1P_RGB_reduced_outlines",) + width = 200 + bbox = (-3.75, 32.158895, 28.326165, 46.3) + styles = ("white",) + +class WMS13GetMapDatasetSeriesOutlinesTimeIntervalTestCase(eoxstest.WMS13GetMapTestCase): + layers = ("MER_FRS_1P_RGB_reduced_outlines",) + width = 200 + bbox = (-3.75, 32.158895, 28.326165, 46.3) + time = "2006-08-16T09:09:29Z/2006-08-16T09:15:46Z" + +class WMS13GetMapDatasetOneBandTestCase(eoxstest.WMS13GetMapTestCase): + """ Test a GetMap request with a dataset containing 15 bands. """ + layers = ("MER_FRS_1PNPDE20060822_092058_000001972050_00308_23408_0077_uint16_reduced_compressed_bands",) + bbox = (8.487755775451660, 32.195316643454134, 25.407486727461219, 46.249103546142578) + dim_band = "MERIS_radiance_01_uint16" + +class WMS13GetMapDatasetThreeBandsTestCase(eoxstest.WMS13GetMapTestCase): + """ Test a GetMap request with a dataset containing 15 bands. """ + layers = ("MER_FRS_1PNPDE20060822_092058_000001972050_00308_23408_0077_uint16_reduced_compressed_bands",) + bbox = (8.487755775451660, 32.195316643454134, 25.407486727461219, 46.249103546142578) + dim_band = "MERIS_radiance_02_uint16,MERIS_radiance_08_uint16,MERIS_radiance_12_uint16" + +class WMS13GetMapReprojectedDatasetTestCase(eoxstest.WMS13GetMapTestCase): + """ Test a GetMap request with a reprojected dataset. """ + fixtures = eoxstest.OWSTestCase.fixtures + ["testing_reprojected_coverages.json"] + + layers = ("MER_FRS_1PNPDE20060816_090929_000001972050_00222_23322_0058_uint16_reduced_compressed_reprojected",) + bbox = (8.487755775451660, 32.195316643454134, 25.407486727461219, 46.249103546142578) + + +class WMS13GetMapCrossesDatelineDatasetTestCase(eoxstest.WMS13GetMapTestCase): + """ Test a GetMap request with a reprojected dataset. """ + fixtures = BASE_FIXTURES + ["testing_crosses_dateline.json"] + + layers = ("crosses_dateline",) + bbox = (-180, -90, 180, 90) + width = 200 + + +class WMS13GetFeatureInfoTestCase(eoxstest.HTMLTestCase): + """ Test a GetFeatureInfo on an outline layer. """ + + def getRequest(self): + params = "SERVICE=WMS&VERSION=1.3.0&REQUEST=GetFeatureInfo&LAYERS=MER_FRS_1P_RGB_reduced_outlines&QUERY_LAYERS=MER_FRS_1P_RGB_reduced_outlines&STYLES=&BBOX=32.158895,-3.75,46.3,28.326165&FEATURE_COUNT=10&HEIGHT=100&WIDTH=200&FORMAT=image%2Fpng&INFO_FORMAT=text/html&CRS=EPSG:4326&I=100&J=50"; + return (params, "kvp") + +class WMS13GetFeatureInfoTimeIntervalTestCase(eoxstest.HTMLTestCase): + """ Test a GetFeatureInfo on an outline layer with a given time slice. """ + + def getRequest(self): + params = "SERVICE=WMS&VERSION=1.3.0&REQUEST=GetFeatureInfo&LAYERS=MER_FRS_1P_RGB_reduced_outlines&QUERY_LAYERS=MER_FRS_1P_RGB_reduced_outlines&STYLES=&BBOX=24.433594,-8.986816,60.205078,58.908691&FEATURE_COUNT=10&HEIGHT=814&WIDTH=1545&FORMAT=image%2Fpng&INFO_FORMAT=text/html&CRS=EPSG:4326&I=598&J=504&TIME=2006-08-16T09:09:29Z/2006-08-16T09:12:46Z"; + return (params, "kvp") + + +class WMS13GetFeatureInfoEmptyTestCase(eoxstest.HTMLTestCase): + """ Test a GetFeatureInfo request not hitting any datasets because of spatial/temporal bounds. """ + + def getRequest(self): + params = "LAYERS=MER_FRS_1P_RGB_reduced_outlines&QUERY_LAYERS=MER_FRS_1P_RGB_reduced_outlines&STYLES=&SERVICE=WMS&VERSION=1.3.0&REQUEST=GetFeatureInfo&EXCEPTIONS=INIMAGE&BBOX=20.742187%2C-19.401855%2C56.513672%2C48.493652&FEATURE_COUNT=10&HEIGHT=814&WIDTH=1545&FORMAT=image%2Fpng&INFO_FORMAT=text%2Fhtml&CRS=EPSG%3A4326&I=1038&J=505" + return (params, "kvp") + +#=============================================================================== +# Authorisation Components +#=============================================================================== + +httpHeadersAuthnInvalid = { + 'AUTH_TYPE': 'shibboleth', + 'uid': 'nclue', + 'cn': 'Clue Norman', + 'description': 'Unauthorized User', + 'Shib-AuthnContext-Class': 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport', + 'Shib-Authentication-Method': 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport', + 'Shib-Authentication-Instant': '2012-01-01T01:01:01.000Z', + 'HTTP_HOST': 'sp.c3ttv042.d03.arc.local', + 'Shib-Session-Index': '509d42a63423976dc7b0a91d6ac10ee3d15f21b8133c6b8b82216997875945e4', + 'displayName': 'Norman Clue', + 'Shib-Application-ID': 'default', + 'Shib-Identity-Provider': 'https://idp.c3ttv042.d03.arc.local/idp/shibboleth', + 'sn': 'Clue', + 'Shib-Session-ID': '_7e0d42381af797d3f69b06d7473455ff', + 'givenName': 'Norman', + 'DUMMY_MODE': 'DUMMY_MODE' +} + +httpHeadersAuthnValid = { + 'AUTH_TYPE': 'shibboleth', + 'uid': 'jdoe', + 'cn': 'Doe John', + 'description': 'Authorized User', + 'Shib-AuthnContext-Class': 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport', + 'Shib-Authentication-Method': 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport', + 'Shib-Authentication-Instant': '2012-01-01T01:01:01.000Z', + 'HTTP_HOST': 'sp.c3ttv042.d03.arc.local', + 'Shib-Session-Index': '509d42a63423976dc7b0a91d6ac10ee3d15f21b8133c6b8b82216997875945e4', + 'Shib-Application-ID': 'default', + 'Shib-Identity-Provider': 'https://idp.c3ttv042.d03.arc.local/idp/shibboleth', + 'sn': 'Doe', + 'Shib-Session-ID': '_7e0d42381af797d3f69b06d7473455ff', + 'givenName': 'John', + 'DUMMY_MODE': 'DUMMY_MODE' +} + +class SecWCS10GetCapabilitiesValidTestCase(eoxstest.XMLTestCase): + def getRequest(self): + params = "service=WCS&version=1.0.0&request=GetCapabilities" + return (params, "kvp", httpHeadersAuthnValid) + +class SecWCS20GetCapabilitiesValidAuthorizedTestCase(eoxstest.XMLTestCase): + """This test shall retrieve a valid WCS 2.0 EO-AP (EO-WCS) GetCapabilities response""" + def getRequest(self): + params = "service=WCS&version=2.0.0&request=GetCapabilities" + return (params, "kvp", httpHeadersAuthnValid) + +class SecWCS20PostGetCapabilitiesValidTestCase(eoxstest.XMLTestCase): + """This test shall retrieve a valid WCS 2.0 EO-AP (EO-WCS) GetCapabilities response + via POST. + """ + def getRequest(self): + params = """ + 2.0.0 + + """ + return (params, "xml", httpHeadersAuthnValid) + +class SecWCS20GetCapabilitiesValidNotAuthorizedTestCase(eoxstest.ExceptionTestCase): + """This test shall retrieve a valid WCS 2.0 EO-AP (EO-WCS) GetCapabilities response without privileges""" + def getRequest(self): + params = "service=WCS&version=2.0.0&request=GetCapabilities" + return (params, "kvp", httpHeadersAuthnInvalid) + + def getExpectedExceptionCode(self): + return "AccessForbidden" + + def getExpectedHTTPStatus(self): + return 403 + +class SecWCS20GetCoverageMosaicTestCase(eoxstest.RectifiedGridCoverageTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.0&request=GetCoverage&CoverageId=mosaic_MER_FRS_1P_RGB_reduced&format=image/tiff" + return (params, "kvp", httpHeadersAuthnValid) + +class SecWCS20GetCoverageCompressionJPEGTestCase(eoxstest.RectifiedGridCoverageTestCase): + def getRequest(self): + params = "service=wcs&version=2.0.0&request=GetCoverage&CoverageId=mosaic_MER_FRS_1P_RGB_reduced&format=%s" % quote("image/tiff;compress=JPEG;jpeg_quality=50") + return (params, "kvp", httpHeadersAuthnValid) + +class SecWMS13GetCapabilitiesValidTestCase(eoxstest.XMLTestCase): + """This test shall retrieve a valid WMS 1.3 GetCapabilities response""" + def getRequest(self): + params = "service=WMS&version=1.3.0&request=GetCapabilities" + return (params, "kvp", httpHeadersAuthnValid) + +class SecWMS13GetMapPNGDatasetTestCase(eoxstest.WMS13GetMapTestCase): + """ Test a GetMap request with a dataset series. """ + layers = ("mosaic_MER_FRS_1PNPDE20060822_092058_000001972050_00308_23408_0077_RGB_reduced",) + bbox = (8.5, 32.2, 25.4, 46.3) + frmt = "image/png" + + styles = [] + crs = "epsg:4326" + width = 100 + height = 100 + + time = None + dim_band = None + + swap_axes = True + + httpHeaders = httpHeadersAuthnValid diff -Nru eoxserver-0.4.0beta2/eoxserver/services/urls.py eoxserver-0.3.2/eoxserver/services/urls.py --- eoxserver-0.4.0beta2/eoxserver/services/urls.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/urls.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,47 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2014 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -from django.conf.urls import url -from django.core.urlresolvers import reverse - -from eoxserver.services import views - -urlpatterns = [ - url(r'^$', views.ows, name='ows'), -] - - -def get_http_service_url(request=None): - """ Returns the URL the OWS view is available under. If a - :class:`django.http.HttpRequest` is passed, an absolute URL is - constructed with the request information. - """ - - http_service_url = reverse("ows") - if request: - http_service_url = request.build_absolute_uri(http_service_url) - return http_service_url diff -Nru eoxserver-0.4.0beta2/eoxserver/services/views.py eoxserver-0.3.2/eoxserver/services/views.py --- eoxserver-0.4.0beta2/eoxserver/services/views.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/services/views.py 2013-12-10 14:57:00.000000000 +0000 @@ -1,8 +1,8 @@ #------------------------------------------------------------------------------- +# $Id$ # # Project: EOxServer -# Authors: Fabian Schindler -# Stephan Krause +# Authors: Stephan Krause # Stephan Meissl # #------------------------------------------------------------------------------- @@ -11,8 +11,8 @@ # 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 +# 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 @@ -28,71 +28,79 @@ #------------------------------------------------------------------------------- """This model contains Django views for the EOxServer software. Its main -function is :func:`ows` which handles all incoming OWS requests""" +function is ows() which handles all incoming OWS requests""" import logging -import traceback from django.http import HttpResponse -try: - from django.http import StreamingHttpResponse -except: - class StreamingHttpResponse(object): - pass +from django.conf import settings -from eoxserver.core import env -from eoxserver.services.ows.component import ServiceComponent +from eoxserver.core.system import System +from eoxserver.services.owscommon import OWSCommonHandler +from eoxserver.services.requests import OWSRequest +from eoxserver.services.auth.base import getPDP logger = logging.getLogger(__name__) - def ows(request): - """ Main entry point for OWS requests against EOxServer. It uses the - :class:`ServiceComponent - ` to dynamically - determine the handler component for this request. - - If an exception occurs during the handling of the request, an exception - handler component is determined and dispatched. - - Any response of the service handler and exception handler is transformed - to a django :class:`HttpResponse ` to adhere the - required interface. + """ + This function handles all incoming OWS requests. + + It prepares the system by a call to + :meth:`eoxserver.core.system.System.init` and generates + an :class:`~.OWSRequest` object containing the request parameters and + passes the handling on to an instance of :class:`~.OWSCommonHandler`. + + If security handling is enabled, the Policy Decision Point (PDP) is + called first in order to determine if the request is authorized. Otherwise + the response of the PDP is sent back to the client. See also + :mod:`eoxserver.services.auth.base`. """ - component = ServiceComponent(env) + if request.method == 'GET': + ows_req = OWSRequest( + http_req=request, + params=request.GET, + param_type="kvp" + ) + elif request.method == 'POST': + ows_req = OWSRequest( + http_req=request, + params=request.raw_post_data, + param_type="xml" + ) + else: + raise Exception("Unsupported request method '%s'" % request.method) - try: - handler = component.query_service_handler(request) - result = handler.handle(request) - default_status = 200 - except Exception, e: - logger.debug(traceback.format_exc()) - handler = component.query_exception_handler(request) - result = handler.handle_exception(request, e) - default_status = 400 - - # try to return a django compatible response - if isinstance(result, (HttpResponse, StreamingHttpResponse)): - return result - - elif isinstance(result, basestring): - return HttpResponse(result) - - # convert result to a django response - try: - content, content_type, status = result - return HttpResponse( - content=content, content_type=content_type, status=status + System.init() + + pdp = getPDP() + + if pdp: + auth_resp = pdp.authorize(ows_req) + + if not pdp or auth_resp.authorized: + + handler = OWSCommonHandler() + + ows_resp = handler.handle(ows_req) + + response = HttpResponse( + content=ows_resp.getContent(), + content_type=ows_resp.getContentType(), + status=ows_resp.getStatus() ) - except ValueError: - pass + for header_name, header_value in ows_resp.headers.items(): + response[header_name] = header_value - try: - content, content_type = result - return HttpResponse( - content=content, content_type=content_type, status=default_status + else: + response = HttpResponse( + content=auth_resp.getContent(), + content_type=auth_resp.getContentType(), + status=auth_resp.getStatus() ) - except ValueError: - pass + for header_name, header_value in auth_resp.headers.items(): + response[header_name] = header_value + + return response diff -Nru eoxserver-0.4.0beta2/eoxserver/testing/core.py eoxserver-0.3.2/eoxserver/testing/core.py --- eoxserver-0.4.0beta2/eoxserver/testing/core.py 1970-01-01 00:00:00.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/testing/core.py 2013-12-10 14:57:00.000000000 +0000 @@ -0,0 +1,232 @@ +#------------------------------------------------------------------------------- +# $Id$ +# +# Project: EOxServer +# Authors: Stephan Krause +# Stephan Meissl +# Fabian Schindler +# +#------------------------------------------------------------------------------- +# Copyright (C) 2011 EOX IT Services GmbH +# +# 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 of this Software or works derived from this 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. +#------------------------------------------------------------------------------- + +import sys +import logging +import re +from StringIO import StringIO + +from lxml import etree + +from django.test import TestCase, TransactionTestCase +from django.test.simple import DjangoTestSuiteRunner, get_tests +from django.db.models.loading import get_app +from django.core.management import execute_from_command_line + +from eoxserver.core.system import System + + +logger = logging.getLogger(__name__) + +BASE_FIXTURES = ["initial_rangetypes.json", "testing_base.json", "testing_asar_base.json"] + +class TestSchemaFactory(object): + schemas = {} + + @classmethod + def getSchema(cls, schema_location): + logger.info("Opening schema: %s" % schema_location) + f = open(schema_location) + schema = etree.XMLSchema(etree.parse(f)) + f.close() + + return schema + + +class EOxServerTestCase(TestCase): + """Test are carried out in a transaction which is rolled back after each + test.""" + fixtures = BASE_FIXTURES + + def setUp(self): + System.init() + +class CommandTestCase(EOxServerTestCase): + """ Base class for testing CLI tools. + """ + + name = "" + args = () + kwargs = {} + expect_failure = False + + def setUp(self): + super(CommandTestCase, self).setUp() + + # construct command line parameters + + args = ["manage.py", self.name] + if isinstance(args, (list, tuple)): + args.extend(self.args) + elif isinstance(args, basestring): + #pylint: disable=E1101 + args.extend(self.args.split(" ")) + + for key, value in self.kwargs.items(): + args.append("-%s" % key if len(key) == 1 else "--%s" % key) + if isinstance(value, (list, tuple)): + args.extend(value) + else: + args.append(value) + + # redirect stderr to buffer + sys.stderr = StringIO() + + # execute command + self.execute_command(args) + + # reset stderr + sys.stderr = sys.__stderr__ + + + def execute_command(self, args): + """ This function actually executes the given command. It raises a + failure if the command prematurely quits. + """ + + try: + execute_from_command_line(args) + except SystemExit: + if not self.expect_failure: + self.fail("Command '%s' failed and exited. Message was: '%s'" % ( + " ".join( args ) , + "".join(sys.stderr.getvalue().rsplit("\n", 1)) ) ) + + +class CommandFaultTestCase(CommandTestCase): + """ Base class for CLI tool tests that expect failures (CommandErrors) to + be raised. + """ + + expected_error = None + + def execute_command(self, args): + """ Specialized implementation of the command execution. A failure is + raised if no error occurs. + """ + + failure = False + try: + execute_from_command_line(args) + except SystemExit: + failure = True + + if not failure: + self.fail("Command did not fail as expected.") + + if self.expected_error: + self.assertEqual(sys.stderr.getvalue(), self.expected_error) + + def testFault(self): + pass + +def _expand_regex_classes(module, regex): + ret = [] + for item in dir(module): + cls = getattr(module, item) + try: + if ((issubclass(cls, TestCase) or + issubclass(cls, TransactionTestCase)) + and re.match(regex, item)): + ret.append(item) + except TypeError: + pass + if not ret: + raise ValueError("Expression %s did not match any test." % regex) + return ret + +def _expand_regex_method(module, klass, regex): + ret = [] + for item in dir(getattr(module, klass)): + if re.match(regex, item): + ret.append(item) + if not ret: + raise ValueError("Expression %s did not match any test." % regex) + return ret + +class EOxServerTestRunner(DjangoTestSuiteRunner): + """ + Custom test runner. It extends the standard + :class:`django.test.simple.DjangoTestSuiteRunner` + with automatic test case search for a given regular expression. + + Activate by including ``TEST_RUNNER = + 'eoxserver.testing.core.EOxServerTestRunner'`` in `settings.py`. + + For example `services.WCS20` would get expanded to all test cases of the + `service` app starting with `WCS20`. + + Note that we're using regex and thus `services.WCS20\*` would get expanded + to all test cases of the `services` app starting with `WCS2` and followed + by any number of `0`\ s. + + Add test cases to exclude after a "|" character + e.g. services.WCS20GetCoverage|WCS20GetCoverageReprojectedEPSG3857DatasetTestCase,WCS20GetCoverageOutputCRSotherUoMDatasetTestCase + """ + def build_suite(self, test_labels, extra_tests=None, **kwargs): + new_labels = [] + for test_label in test_labels: + parts = test_label.split('|') + test_labels_exclude = None + if len(parts) == 2: + test_label = parts[0] + test_labels_exclude = parts[1].split(',') + + parts = test_label.split('.') + + if len(parts) > 3 or len(parts) < 1: + new_labels.append(test_label) + continue + + app_module = get_app(parts[0]) + test_module = get_tests(app_module) + + classes = None + + if len(parts) == 1: + classes = _expand_regex_classes(test_module, '') + new_labels.extend([".".join((parts[0], klass)) for klass in classes]) + elif len(parts) == 2: + classes = _expand_regex_classes(test_module, parts[1]) + new_labels.extend([".".join((parts[0], klass)) for klass in classes]) + else: + classes = _expand_regex_classes(test_module, parts[1]) + for klass in classes: + methods = _expand_regex_method(test_module, klass, parts[2]) + new_labels.extend([".".join((parts[0], klass, method)) for method in methods]) + + if test_labels_exclude is not None: + for test_label_exclude in test_labels_exclude: + try: + new_labels.remove(".".join((parts[0], test_label_exclude))) + except ValueError: + raise ValueError("Test '%s' to exclude not found." % test_label_exclude) + + return super(EOxServerTestRunner, self).build_suite(new_labels, extra_tests, **kwargs) diff -Nru eoxserver-0.4.0beta2/eoxserver/testing/xcomp.py eoxserver-0.3.2/eoxserver/testing/xcomp.py --- eoxserver-0.4.0beta2/eoxserver/testing/xcomp.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/testing/xcomp.py 2013-12-10 14:57:00.000000000 +0000 @@ -1,4 +1,5 @@ #------------------------------------------------------------------------------- +# $Id$ # # simple tool comparing XML documents - powered by Python miniDOM # @@ -11,8 +12,8 @@ # 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 +# 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 diff -Nru eoxserver-0.4.0beta2/eoxserver/views.py eoxserver-0.3.2/eoxserver/views.py --- eoxserver-0.4.0beta2/eoxserver/views.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/views.py 2013-12-10 14:57:00.000000000 +0000 @@ -1,4 +1,5 @@ #------------------------------------------------------------------------------- +# $Id$ # # Project: EOxServer # Authors: Stephan Krause @@ -11,8 +12,8 @@ # 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 +# 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 diff -Nru eoxserver-0.4.0beta2/eoxserver/webclient/admin.py eoxserver-0.3.2/eoxserver/webclient/admin.py --- eoxserver-0.4.0beta2/eoxserver/webclient/admin.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/webclient/admin.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,45 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2014 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -from django.contrib.gis import admin - -from eoxserver.webclient import models -from eoxserver.resources.coverages.admin import ( - RectifiedDatasetAdmin, ReferenceableDatasetAdmin, - RectifiedStitchedMosaicAdmin, DatasetSeriesAdmin -) - - -class ExtraInline(admin.StackedInline): - model = models.Extra - - -for admin in ( - RectifiedDatasetAdmin, ReferenceableDatasetAdmin, - RectifiedStitchedMosaicAdmin, DatasetSeriesAdmin -): - admin.inlines = admin.inlines + (ExtraInline,) diff -Nru eoxserver-0.4.0beta2/eoxserver/webclient/models.py eoxserver-0.3.2/eoxserver/webclient/models.py --- eoxserver-0.4.0beta2/eoxserver/webclient/models.py 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/webclient/models.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,39 +0,0 @@ -#------------------------------------------------------------------------------- -# -# Project: EOxServer -# Authors: Fabian Schindler -# -#------------------------------------------------------------------------------- -# Copyright (C) 2014 EOX IT Services GmbH -# -# 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 of this Software or works derived from this 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. -#------------------------------------------------------------------------------- - -from django.db import models - -from eoxserver.resources.coverages import models as coverages - - -class Extra(models.Model): - eo_object = models.OneToOneField(coverages.EOObject, related_name="webclient_extra") - - display_name = models.CharField(max_length=64, blank=True, null=True) - info = models.TextField(blank=True, null=True) - color = models.CharField(max_length=64, blank=True, null=True) - default_visible = models.BooleanField(default=False) Binary files /tmp/Jwl1cbsnLO/eoxserver-0.4.0beta2/eoxserver/webclient/static/ajax-loader.gif and /tmp/OxIigXjl4D/eoxserver-0.3.2/eoxserver/webclient/static/ajax-loader.gif differ diff -Nru eoxserver-0.4.0beta2/eoxserver/webclient/static/backbone/backbone.js eoxserver-0.3.2/eoxserver/webclient/static/backbone/backbone.js --- eoxserver-0.4.0beta2/eoxserver/webclient/static/backbone/backbone.js 1970-01-01 00:00:00.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/webclient/static/backbone/backbone.js 2013-12-10 14:57:00.000000000 +0000 @@ -0,0 +1,1431 @@ +// Backbone.js 0.9.2 + +// (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc. +// Backbone may be freely distributed under the MIT license. +// For all details and documentation: +// http://backbonejs.org + +(function(){ + + // Initial Setup + // ------------- + + // Save a reference to the global object (`window` in the browser, `global` + // on the server). + var root = this; + + // Save the previous value of the `Backbone` variable, so that it can be + // restored later on, if `noConflict` is used. + var previousBackbone = root.Backbone; + + // Create a local reference to slice/splice. + var slice = Array.prototype.slice; + var splice = Array.prototype.splice; + + // The top-level namespace. All public Backbone classes and modules will + // be attached to this. Exported for both CommonJS and the browser. + var Backbone; + if (typeof exports !== 'undefined') { + Backbone = exports; + } else { + Backbone = root.Backbone = {}; + } + + // Current version of the library. Keep in sync with `package.json`. + Backbone.VERSION = '0.9.2'; + + // Require Underscore, if we're on the server, and it's not already present. + var _ = root._; + if (!_ && (typeof require !== 'undefined')) _ = require('underscore'); + + // For Backbone's purposes, jQuery, Zepto, or Ender owns the `$` variable. + var $ = root.jQuery || root.Zepto || root.ender; + + // Set the JavaScript library that will be used for DOM manipulation and + // Ajax calls (a.k.a. the `$` variable). By default Backbone will use: jQuery, + // Zepto, or Ender; but the `setDomLibrary()` method lets you inject an + // alternate JavaScript library (or a mock library for testing your views + // outside of a browser). + Backbone.setDomLibrary = function(lib) { + $ = lib; + }; + + // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable + // to its previous owner. Returns a reference to this Backbone object. + Backbone.noConflict = function() { + root.Backbone = previousBackbone; + return this; + }; + + // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option + // will fake `"PUT"` and `"DELETE"` requests via the `_method` parameter and + // set a `X-Http-Method-Override` header. + Backbone.emulateHTTP = false; + + // Turn on `emulateJSON` to support legacy servers that can't deal with direct + // `application/json` requests ... will encode the body as + // `application/x-www-form-urlencoded` instead and will send the model in a + // form param named `model`. + Backbone.emulateJSON = false; + + // Backbone.Events + // ----------------- + + // Regular expression used to split event strings + var eventSplitter = /\s+/; + + // A module that can be mixed in to *any object* in order to provide it with + // custom events. You may bind with `on` or remove with `off` callback functions + // to an event; trigger`-ing an event fires all callbacks in succession. + // + // var object = {}; + // _.extend(object, Backbone.Events); + // object.on('expand', function(){ alert('expanded'); }); + // object.trigger('expand'); + // + var Events = Backbone.Events = { + + // Bind one or more space separated events, `events`, to a `callback` + // function. Passing `"all"` will bind the callback to all events fired. + on: function(events, callback, context) { + + var calls, event, node, tail, list; + if (!callback) return this; + events = events.split(eventSplitter); + calls = this._callbacks || (this._callbacks = {}); + + // Create an immutable callback list, allowing traversal during + // modification. The tail is an empty object that will always be used + // as the next node. + while (event = events.shift()) { + list = calls[event]; + node = list ? list.tail : {}; + node.next = tail = {}; + node.context = context; + node.callback = callback; + calls[event] = {tail: tail, next: list ? list.next : node}; + } + + return this; + }, + + // Remove one or many callbacks. If `context` is null, removes all callbacks + // with that function. If `callback` is null, removes all callbacks for the + // event. If `events` is null, removes all bound callbacks for all events. + off: function(events, callback, context) { + var event, calls, node, tail, cb, ctx; + + // No events, or removing *all* events. + if (!(calls = this._callbacks)) return; + if (!(events || callback || context)) { + delete this._callbacks; + return this; + } + + // Loop through the listed events and contexts, splicing them out of the + // linked list of callbacks if appropriate. + events = events ? events.split(eventSplitter) : _.keys(calls); + while (event = events.shift()) { + node = calls[event]; + delete calls[event]; + if (!node || !(callback || context)) continue; + // Create a new list, omitting the indicated callbacks. + tail = node.tail; + while ((node = node.next) !== tail) { + cb = node.callback; + ctx = node.context; + if ((callback && cb !== callback) || (context && ctx !== context)) { + this.on(event, cb, ctx); + } + } + } + + return this; + }, + + // Trigger one or many events, firing all bound callbacks. Callbacks are + // passed the same arguments as `trigger` is, apart from the event name + // (unless you're listening on `"all"`, which will cause your callback to + // receive the true name of the event as the first argument). + trigger: function(events) { + var event, node, calls, tail, args, all, rest; + if (!(calls = this._callbacks)) return this; + all = calls.all; + events = events.split(eventSplitter); + rest = slice.call(arguments, 1); + + // For each event, walk through the linked list of callbacks twice, + // first to trigger the event, then to trigger any `"all"` callbacks. + while (event = events.shift()) { + if (node = calls[event]) { + tail = node.tail; + while ((node = node.next) !== tail) { + node.callback.apply(node.context || this, rest); + } + } + if (node = all) { + tail = node.tail; + args = [event].concat(rest); + while ((node = node.next) !== tail) { + node.callback.apply(node.context || this, args); + } + } + } + + return this; + } + + }; + + // Aliases for backwards compatibility. + Events.bind = Events.on; + Events.unbind = Events.off; + + // Backbone.Model + // -------------- + + // Create a new model, with defined attributes. A client id (`cid`) + // is automatically generated and assigned for you. + var Model = Backbone.Model = function(attributes, options) { + var defaults; + attributes || (attributes = {}); + if (options && options.parse) attributes = this.parse(attributes); + if (defaults = getValue(this, 'defaults')) { + attributes = _.extend({}, defaults, attributes); + } + if (options && options.collection) this.collection = options.collection; + this.attributes = {}; + this._escapedAttributes = {}; + this.cid = _.uniqueId('c'); + this.changed = {}; + this._silent = {}; + this._pending = {}; + this.set(attributes, {silent: true}); + // Reset change tracking. + this.changed = {}; + this._silent = {}; + this._pending = {}; + this._previousAttributes = _.clone(this.attributes); + this.initialize.apply(this, arguments); + }; + + // Attach all inheritable methods to the Model prototype. + _.extend(Model.prototype, Events, { + + // A hash of attributes whose current and previous value differ. + changed: null, + + // A hash of attributes that have silently changed since the last time + // `change` was called. Will become pending attributes on the next call. + _silent: null, + + // A hash of attributes that have changed since the last `'change'` event + // began. + _pending: null, + + // The default name for the JSON `id` attribute is `"id"`. MongoDB and + // CouchDB users may want to set this to `"_id"`. + idAttribute: 'id', + + // Initialize is an empty function by default. Override it with your own + // initialization logic. + initialize: function(){}, + + // Return a copy of the model's `attributes` object. + toJSON: function(options) { + return _.clone(this.attributes); + }, + + // Get the value of an attribute. + get: function(attr) { + return this.attributes[attr]; + }, + + // Get the HTML-escaped value of an attribute. + escape: function(attr) { + var html; + if (html = this._escapedAttributes[attr]) return html; + var val = this.get(attr); + return this._escapedAttributes[attr] = _.escape(val == null ? '' : '' + val); + }, + + // Returns `true` if the attribute contains a value that is not null + // or undefined. + has: function(attr) { + return this.get(attr) != null; + }, + + // Set a hash of model attributes on the object, firing `"change"` unless + // you choose to silence it. + set: function(key, value, options) { + var attrs, attr, val; + + // Handle both `"key", value` and `{key: value}` -style arguments. + if (_.isObject(key) || key == null) { + attrs = key; + options = value; + } else { + attrs = {}; + attrs[key] = value; + } + + // Extract attributes and options. + options || (options = {}); + if (!attrs) return this; + if (attrs instanceof Model) attrs = attrs.attributes; + if (options.unset) for (attr in attrs) attrs[attr] = void 0; + + // Run validation. + if (!this._validate(attrs, options)) return false; + + // Check for changes of `id`. + if (this.idAttribute in attrs) this.id = attrs[this.idAttribute]; + + var changes = options.changes = {}; + var now = this.attributes; + var escaped = this._escapedAttributes; + var prev = this._previousAttributes || {}; + + // For each `set` attribute... + for (attr in attrs) { + val = attrs[attr]; + + // If the new and current value differ, record the change. + if (!_.isEqual(now[attr], val) || (options.unset && _.has(now, attr))) { + delete escaped[attr]; + (options.silent ? this._silent : changes)[attr] = true; + } + + // Update or delete the current value. + options.unset ? delete now[attr] : now[attr] = val; + + // If the new and previous value differ, record the change. If not, + // then remove changes for this attribute. + if (!_.isEqual(prev[attr], val) || (_.has(now, attr) != _.has(prev, attr))) { + this.changed[attr] = val; + if (!options.silent) this._pending[attr] = true; + } else { + delete this.changed[attr]; + delete this._pending[attr]; + } + } + + // Fire the `"change"` events. + if (!options.silent) this.change(options); + return this; + }, + + // Remove an attribute from the model, firing `"change"` unless you choose + // to silence it. `unset` is a noop if the attribute doesn't exist. + unset: function(attr, options) { + (options || (options = {})).unset = true; + return this.set(attr, null, options); + }, + + // Clear all attributes on the model, firing `"change"` unless you choose + // to silence it. + clear: function(options) { + (options || (options = {})).unset = true; + return this.set(_.clone(this.attributes), options); + }, + + // Fetch the model from the server. If the server's representation of the + // model differs from its current attributes, they will be overriden, + // triggering a `"change"` event. + fetch: function(options) { + options = options ? _.clone(options) : {}; + var model = this; + var success = options.success; + options.success = function(resp, status, xhr) { + if (!model.set(model.parse(resp, xhr), options)) return false; + if (success) success(model, resp); + }; + options.error = Backbone.wrapError(options.error, model, options); + return (this.sync || Backbone.sync).call(this, 'read', this, options); + }, + + // Set a hash of model attributes, and sync the model to the server. + // If the server returns an attributes hash that differs, the model's + // state will be `set` again. + save: function(key, value, options) { + var attrs, current; + + // Handle both `("key", value)` and `({key: value})` -style calls. + if (_.isObject(key) || key == null) { + attrs = key; + options = value; + } else { + attrs = {}; + attrs[key] = value; + } + options = options ? _.clone(options) : {}; + + // If we're "wait"-ing to set changed attributes, validate early. + if (options.wait) { + if (!this._validate(attrs, options)) return false; + current = _.clone(this.attributes); + } + + // Regular saves `set` attributes before persisting to the server. + var silentOptions = _.extend({}, options, {silent: true}); + if (attrs && !this.set(attrs, options.wait ? silentOptions : options)) { + return false; + } + + // After a successful server-side save, the client is (optionally) + // updated with the server-side state. + var model = this; + var success = options.success; + options.success = function(resp, status, xhr) { + var serverAttrs = model.parse(resp, xhr); + if (options.wait) { + delete options.wait; + serverAttrs = _.extend(attrs || {}, serverAttrs); + } + if (!model.set(serverAttrs, options)) return false; + if (success) { + success(model, resp); + } else { + model.trigger('sync', model, resp, options); + } + }; + + // Finish configuring and sending the Ajax request. + options.error = Backbone.wrapError(options.error, model, options); + var method = this.isNew() ? 'create' : 'update'; + var xhr = (this.sync || Backbone.sync).call(this, method, this, options); + if (options.wait) this.set(current, silentOptions); + return xhr; + }, + + // Destroy this model on the server if it was already persisted. + // Optimistically removes the model from its collection, if it has one. + // If `wait: true` is passed, waits for the server to respond before removal. + destroy: function(options) { + options = options ? _.clone(options) : {}; + var model = this; + var success = options.success; + + var triggerDestroy = function() { + model.trigger('destroy', model, model.collection, options); + }; + + if (this.isNew()) { + triggerDestroy(); + return false; + } + + options.success = function(resp) { + if (options.wait) triggerDestroy(); + if (success) { + success(model, resp); + } else { + model.trigger('sync', model, resp, options); + } + }; + + options.error = Backbone.wrapError(options.error, model, options); + var xhr = (this.sync || Backbone.sync).call(this, 'delete', this, options); + if (!options.wait) triggerDestroy(); + return xhr; + }, + + // Default URL for the model's representation on the server -- if you're + // using Backbone's restful methods, override this to change the endpoint + // that will be called. + url: function() { + var base = getValue(this, 'urlRoot') || getValue(this.collection, 'url') || urlError(); + if (this.isNew()) return base; + return base + (base.charAt(base.length - 1) == '/' ? '' : '/') + encodeURIComponent(this.id); + }, + + // **parse** converts a response into the hash of attributes to be `set` on + // the model. The default implementation is just to pass the response along. + parse: function(resp, xhr) { + return resp; + }, + + // Create a new model with identical attributes to this one. + clone: function() { + return new this.constructor(this.attributes); + }, + + // A model is new if it has never been saved to the server, and lacks an id. + isNew: function() { + return this.id == null; + }, + + // Call this method to manually fire a `"change"` event for this model and + // a `"change:attribute"` event for each changed attribute. + // Calling this will cause all objects observing the model to update. + change: function(options) { + options || (options = {}); + var changing = this._changing; + this._changing = true; + + // Silent changes become pending changes. + for (var attr in this._silent) this._pending[attr] = true; + + // Silent changes are triggered. + var changes = _.extend({}, options.changes, this._silent); + this._silent = {}; + for (var attr in changes) { + this.trigger('change:' + attr, this, this.get(attr), options); + } + if (changing) return this; + + // Continue firing `"change"` events while there are pending changes. + while (!_.isEmpty(this._pending)) { + this._pending = {}; + this.trigger('change', this, options); + // Pending and silent changes still remain. + for (var attr in this.changed) { + if (this._pending[attr] || this._silent[attr]) continue; + delete this.changed[attr]; + } + this._previousAttributes = _.clone(this.attributes); + } + + this._changing = false; + return this; + }, + + // Determine if the model has changed since the last `"change"` event. + // If you specify an attribute name, determine if that attribute has changed. + hasChanged: function(attr) { + if (!arguments.length) return !_.isEmpty(this.changed); + return _.has(this.changed, attr); + }, + + // Return an object containing all the attributes that have changed, or + // false if there are no changed attributes. Useful for determining what + // parts of a view need to be updated and/or what attributes need to be + // persisted to the server. Unset attributes will be set to undefined. + // You can also pass an attributes object to diff against the model, + // determining if there *would be* a change. + changedAttributes: function(diff) { + if (!diff) return this.hasChanged() ? _.clone(this.changed) : false; + var val, changed = false, old = this._previousAttributes; + for (var attr in diff) { + if (_.isEqual(old[attr], (val = diff[attr]))) continue; + (changed || (changed = {}))[attr] = val; + } + return changed; + }, + + // Get the previous value of an attribute, recorded at the time the last + // `"change"` event was fired. + previous: function(attr) { + if (!arguments.length || !this._previousAttributes) return null; + return this._previousAttributes[attr]; + }, + + // Get all of the attributes of the model at the time of the previous + // `"change"` event. + previousAttributes: function() { + return _.clone(this._previousAttributes); + }, + + // Check if the model is currently in a valid state. It's only possible to + // get into an *invalid* state if you're using silent changes. + isValid: function() { + return !this.validate(this.attributes); + }, + + // Run validation against the next complete set of model attributes, + // returning `true` if all is well. If a specific `error` callback has + // been passed, call that instead of firing the general `"error"` event. + _validate: function(attrs, options) { + if (options.silent || !this.validate) return true; + attrs = _.extend({}, this.attributes, attrs); + var error = this.validate(attrs, options); + if (!error) return true; + if (options && options.error) { + options.error(this, error, options); + } else { + this.trigger('error', this, error, options); + } + return false; + } + + }); + + // Backbone.Collection + // ------------------- + + // Provides a standard collection class for our sets of models, ordered + // or unordered. If a `comparator` is specified, the Collection will maintain + // its models in sort order, as they're added and removed. + var Collection = Backbone.Collection = function(models, options) { + options || (options = {}); + if (options.model) this.model = options.model; + if (options.comparator) this.comparator = options.comparator; + this._reset(); + this.initialize.apply(this, arguments); + if (models) this.reset(models, {silent: true, parse: options.parse}); + }; + + // Define the Collection's inheritable methods. + _.extend(Collection.prototype, Events, { + + // The default model for a collection is just a **Backbone.Model**. + // This should be overridden in most cases. + model: Model, + + // Initialize is an empty function by default. Override it with your own + // initialization logic. + initialize: function(){}, + + // The JSON representation of a Collection is an array of the + // models' attributes. + toJSON: function(options) { + return this.map(function(model){ return model.toJSON(options); }); + }, + + // Add a model, or list of models to the set. Pass **silent** to avoid + // firing the `add` event for every new model. + add: function(models, options) { + var i, index, length, model, cid, id, cids = {}, ids = {}, dups = []; + options || (options = {}); + models = _.isArray(models) ? models.slice() : [models]; + + // Begin by turning bare objects into model references, and preventing + // invalid models or duplicate models from being added. + for (i = 0, length = models.length; i < length; i++) { + if (!(model = models[i] = this._prepareModel(models[i], options))) { + throw new Error("Can't add an invalid model to a collection"); + } + cid = model.cid; + id = model.id; + if (cids[cid] || this._byCid[cid] || ((id != null) && (ids[id] || this._byId[id]))) { + dups.push(i); + continue; + } + cids[cid] = ids[id] = model; + } + + // Remove duplicates. + i = dups.length; + while (i--) { + models.splice(dups[i], 1); + } + + // Listen to added models' events, and index models for lookup by + // `id` and by `cid`. + for (i = 0, length = models.length; i < length; i++) { + (model = models[i]).on('all', this._onModelEvent, this); + this._byCid[model.cid] = model; + if (model.id != null) this._byId[model.id] = model; + } + + // Insert models into the collection, re-sorting if needed, and triggering + // `add` events unless silenced. + this.length += length; + index = options.at != null ? options.at : this.models.length; + splice.apply(this.models, [index, 0].concat(models)); + if (this.comparator) this.sort({silent: true}); + if (options.silent) return this; + for (i = 0, length = this.models.length; i < length; i++) { + if (!cids[(model = this.models[i]).cid]) continue; + options.index = i; + model.trigger('add', model, this, options); + } + return this; + }, + + // Remove a model, or a list of models from the set. Pass silent to avoid + // firing the `remove` event for every model removed. + remove: function(models, options) { + var i, l, index, model; + options || (options = {}); + models = _.isArray(models) ? models.slice() : [models]; + for (i = 0, l = models.length; i < l; i++) { + model = this.getByCid(models[i]) || this.get(models[i]); + if (!model) continue; + delete this._byId[model.id]; + delete this._byCid[model.cid]; + index = this.indexOf(model); + this.models.splice(index, 1); + this.length--; + if (!options.silent) { + options.index = index; + model.trigger('remove', model, this, options); + } + this._removeReference(model); + } + return this; + }, + + // Add a model to the end of the collection. + push: function(model, options) { + model = this._prepareModel(model, options); + this.add(model, options); + return model; + }, + + // Remove a model from the end of the collection. + pop: function(options) { + var model = this.at(this.length - 1); + this.remove(model, options); + return model; + }, + + // Add a model to the beginning of the collection. + unshift: function(model, options) { + model = this._prepareModel(model, options); + this.add(model, _.extend({at: 0}, options)); + return model; + }, + + // Remove a model from the beginning of the collection. + shift: function(options) { + var model = this.at(0); + this.remove(model, options); + return model; + }, + + // Get a model from the set by id. + get: function(id) { + if (id == null) return void 0; + return this._byId[id.id != null ? id.id : id]; + }, + + // Get a model from the set by client id. + getByCid: function(cid) { + return cid && this._byCid[cid.cid || cid]; + }, + + // Get the model at the given index. + at: function(index) { + return this.models[index]; + }, + + // Return models with matching attributes. Useful for simple cases of `filter`. + where: function(attrs) { + if (_.isEmpty(attrs)) return []; + return this.filter(function(model) { + for (var key in attrs) { + if (attrs[key] !== model.get(key)) return false; + } + return true; + }); + }, + + // Force the collection to re-sort itself. You don't need to call this under + // normal circumstances, as the set will maintain sort order as each item + // is added. + sort: function(options) { + options || (options = {}); + if (!this.comparator) throw new Error('Cannot sort a set without a comparator'); + var boundComparator = _.bind(this.comparator, this); + if (this.comparator.length == 1) { + this.models = this.sortBy(boundComparator); + } else { + this.models.sort(boundComparator); + } + if (!options.silent) this.trigger('reset', this, options); + return this; + }, + + // Pluck an attribute from each model in the collection. + pluck: function(attr) { + return _.map(this.models, function(model){ return model.get(attr); }); + }, + + // When you have more items than you want to add or remove individually, + // you can reset the entire set with a new list of models, without firing + // any `add` or `remove` events. Fires `reset` when finished. + reset: function(models, options) { + models || (models = []); + options || (options = {}); + for (var i = 0, l = this.models.length; i < l; i++) { + this._removeReference(this.models[i]); + } + this._reset(); + this.add(models, _.extend({silent: true}, options)); + if (!options.silent) this.trigger('reset', this, options); + return this; + }, + + // Fetch the default set of models for this collection, resetting the + // collection when they arrive. If `add: true` is passed, appends the + // models to the collection instead of resetting. + fetch: function(options) { + options = options ? _.clone(options) : {}; + if (options.parse === undefined) options.parse = true; + var collection = this; + var success = options.success; + options.success = function(resp, status, xhr) { + collection[options.add ? 'add' : 'reset'](collection.parse(resp, xhr), options); + if (success) success(collection, resp); + }; + options.error = Backbone.wrapError(options.error, collection, options); + return (this.sync || Backbone.sync).call(this, 'read', this, options); + }, + + // Create a new instance of a model in this collection. Add the model to the + // collection immediately, unless `wait: true` is passed, in which case we + // wait for the server to agree. + create: function(model, options) { + var coll = this; + options = options ? _.clone(options) : {}; + model = this._prepareModel(model, options); + if (!model) return false; + if (!options.wait) coll.add(model, options); + var success = options.success; + options.success = function(nextModel, resp, xhr) { + if (options.wait) coll.add(nextModel, options); + if (success) { + success(nextModel, resp); + } else { + nextModel.trigger('sync', model, resp, options); + } + }; + model.save(null, options); + return model; + }, + + // **parse** converts a response into a list of models to be added to the + // collection. The default implementation is just to pass it through. + parse: function(resp, xhr) { + return resp; + }, + + // Proxy to _'s chain. Can't be proxied the same way the rest of the + // underscore methods are proxied because it relies on the underscore + // constructor. + chain: function () { + return _(this.models).chain(); + }, + + // Reset all internal state. Called when the collection is reset. + _reset: function(options) { + this.length = 0; + this.models = []; + this._byId = {}; + this._byCid = {}; + }, + + // Prepare a model or hash of attributes to be added to this collection. + _prepareModel: function(model, options) { + options || (options = {}); + if (!(model instanceof Model)) { + var attrs = model; + options.collection = this; + model = new this.model(attrs, options); + if (!model._validate(model.attributes, options)) model = false; + } else if (!model.collection) { + model.collection = this; + } + return model; + }, + + // Internal method to remove a model's ties to a collection. + _removeReference: function(model) { + if (this == model.collection) { + delete model.collection; + } + model.off('all', this._onModelEvent, this); + }, + + // Internal method called every time a model in the set fires an event. + // Sets need to update their indexes when models change ids. All other + // events simply proxy through. "add" and "remove" events that originate + // in other collections are ignored. + _onModelEvent: function(event, model, collection, options) { + if ((event == 'add' || event == 'remove') && collection != this) return; + if (event == 'destroy') { + this.remove(model, options); + } + if (model && event === 'change:' + model.idAttribute) { + delete this._byId[model.previous(model.idAttribute)]; + this._byId[model.id] = model; + } + this.trigger.apply(this, arguments); + } + + }); + + // Underscore methods that we want to implement on the Collection. + var methods = ['forEach', 'each', 'map', 'reduce', 'reduceRight', 'find', + 'detect', 'filter', 'select', 'reject', 'every', 'all', 'some', 'any', + 'include', 'contains', 'invoke', 'max', 'min', 'sortBy', 'sortedIndex', + 'toArray', 'size', 'first', 'initial', 'rest', 'last', 'without', 'indexOf', + 'shuffle', 'lastIndexOf', 'isEmpty', 'groupBy']; + + // Mix in each Underscore method as a proxy to `Collection#models`. + _.each(methods, function(method) { + Collection.prototype[method] = function() { + return _[method].apply(_, [this.models].concat(_.toArray(arguments))); + }; + }); + + // Backbone.Router + // ------------------- + + // Routers map faux-URLs to actions, and fire events when routes are + // matched. Creating a new one sets its `routes` hash, if not set statically. + var Router = Backbone.Router = function(options) { + options || (options = {}); + if (options.routes) this.routes = options.routes; + this._bindRoutes(); + this.initialize.apply(this, arguments); + }; + + // Cached regular expressions for matching named param parts and splatted + // parts of route strings. + var namedParam = /:\w+/g; + var splatParam = /\*\w+/g; + var escapeRegExp = /[-[\]{}()+?.,\\^$|#\s]/g; + + // Set up all inheritable **Backbone.Router** properties and methods. + _.extend(Router.prototype, Events, { + + // Initialize is an empty function by default. Override it with your own + // initialization logic. + initialize: function(){}, + + // Manually bind a single named route to a callback. For example: + // + // this.route('search/:query/p:num', 'search', function(query, num) { + // ... + // }); + // + route: function(route, name, callback) { + Backbone.history || (Backbone.history = new History); + if (!_.isRegExp(route)) route = this._routeToRegExp(route); + if (!callback) callback = this[name]; + Backbone.history.route(route, _.bind(function(fragment) { + var args = this._extractParameters(route, fragment); + callback && callback.apply(this, args); + this.trigger.apply(this, ['route:' + name].concat(args)); + Backbone.history.trigger('route', this, name, args); + }, this)); + return this; + }, + + // Simple proxy to `Backbone.history` to save a fragment into the history. + navigate: function(fragment, options) { + Backbone.history.navigate(fragment, options); + }, + + // Bind all defined routes to `Backbone.history`. We have to reverse the + // order of the routes here to support behavior where the most general + // routes can be defined at the bottom of the route map. + _bindRoutes: function() { + if (!this.routes) return; + var routes = []; + for (var route in this.routes) { + routes.unshift([route, this.routes[route]]); + } + for (var i = 0, l = routes.length; i < l; i++) { + this.route(routes[i][0], routes[i][1], this[routes[i][1]]); + } + }, + + // Convert a route string into a regular expression, suitable for matching + // against the current location hash. + _routeToRegExp: function(route) { + route = route.replace(escapeRegExp, '\\$&') + .replace(namedParam, '([^\/]+)') + .replace(splatParam, '(.*?)'); + return new RegExp('^' + route + '$'); + }, + + // Given a route, and a URL fragment that it matches, return the array of + // extracted parameters. + _extractParameters: function(route, fragment) { + return route.exec(fragment).slice(1); + } + + }); + + // Backbone.History + // ---------------- + + // Handles cross-browser history management, based on URL fragments. If the + // browser does not support `onhashchange`, falls back to polling. + var History = Backbone.History = function() { + this.handlers = []; + _.bindAll(this, 'checkUrl'); + }; + + // Cached regex for cleaning leading hashes and slashes . + var routeStripper = /^[#\/]/; + + // Cached regex for detecting MSIE. + var isExplorer = /msie [\w.]+/; + + // Has the history handling already been started? + History.started = false; + + // Set up all inheritable **Backbone.History** properties and methods. + _.extend(History.prototype, Events, { + + // The default interval to poll for hash changes, if necessary, is + // twenty times a second. + interval: 50, + + // Gets the true hash value. Cannot use location.hash directly due to bug + // in Firefox where location.hash will always be decoded. + getHash: function(windowOverride) { + var loc = windowOverride ? windowOverride.location : window.location; + var match = loc.href.match(/#(.*)$/); + return match ? match[1] : ''; + }, + + // Get the cross-browser normalized URL fragment, either from the URL, + // the hash, or the override. + getFragment: function(fragment, forcePushState) { + if (fragment == null) { + if (this._hasPushState || forcePushState) { + fragment = window.location.pathname; + var search = window.location.search; + if (search) fragment += search; + } else { + fragment = this.getHash(); + } + } + if (!fragment.indexOf(this.options.root)) fragment = fragment.substr(this.options.root.length); + return fragment.replace(routeStripper, ''); + }, + + // Start the hash change handling, returning `true` if the current URL matches + // an existing route, and `false` otherwise. + start: function(options) { + if (History.started) throw new Error("Backbone.history has already been started"); + History.started = true; + + // Figure out the initial configuration. Do we need an iframe? + // Is pushState desired ... is it available? + this.options = _.extend({}, {root: '/'}, this.options, options); + this._wantsHashChange = this.options.hashChange !== false; + this._wantsPushState = !!this.options.pushState; + this._hasPushState = !!(this.options.pushState && window.history && window.history.pushState); + var fragment = this.getFragment(); + var docMode = document.documentMode; + var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7)); + + if (oldIE) { + this.iframe = $('':""),a._keyEvent=!1;return K},_generateMonthYearHeader:function(a,b,c,d,e,f,g,h){var i=this._get(a,"changeMonth"),j=this._get(a,"changeYear"),k=this._get(a,"showMonthAfterYear"),l='
    ',m="";if(f||!i)m+=''+g[b]+"";else{var n=d&&d.getFullYear()==c,o=e&&e.getFullYear()==c;m+='"}k||(l+=m+(f||!i||!j?" ":""));if(!a.yearshtml){a.yearshtml="";if(f||!j)l+=''+c+"";else{var q=this._get(a,"yearRange").split(":"),r=(new Date).getFullYear(),s=function(a){var b=a.match(/c[+-].*/)?c+parseInt(a.substring(1),10):a.match(/[+-].*/)?r+parseInt(a,10):parseInt(a,10);return isNaN(b)?r:b},t=s(q[0]),u=Math.max(t,s(q[1]||""));t=d?Math.max(t,d.getFullYear()):t,u=e?Math.min(u,e.getFullYear()):u,a.yearshtml+='",l+=a.yearshtml,a.yearshtml=null}}l+=this._get(a,"yearSuffix"),k&&(l+=(f||!i||!j?" ":"")+m),l+="
    ";return l},_adjustInstDate:function(a,b,c){var d=a.drawYear+(c=="Y"?b:0),e=a.drawMonth+(c=="M"?b:0),f=Math.min(a.selectedDay,this._getDaysInMonth(d,e))+(c=="D"?b:0),g=this._restrictMinMax(a,this._daylightSavingAdjust(new Date(d,e,f)));a.selectedDay=g.getDate(),a.drawMonth=a.selectedMonth=g.getMonth(),a.drawYear=a.selectedYear=g.getFullYear(),(c=="M"||c=="Y")&&this._notifyChange(a)},_restrictMinMax:function(a,b){var c=this._getMinMaxDate(a,"min"),d=this._getMinMaxDate(a,"max"),e=c&&bd?d:e;return e},_notifyChange:function(a){var b=this._get(a,"onChangeMonthYear");b&&b.apply(a.input?a.input[0]:null,[a.selectedYear,a.selectedMonth+1,a])},_getNumberOfMonths:function(a){var b=this._get(a,"numberOfMonths");return b==null?[1,1]:typeof b=="number"?[1,b]:b},_getMinMaxDate:function(a,b){return this._determineDate(a,this._get(a,b+"Date"),null)},_getDaysInMonth:function(a,b){return 32-this._daylightSavingAdjust(new Date(a,b,32)).getDate()},_getFirstDayOfMonth:function(a,b){return(new Date(a,b,1)).getDay()},_canAdjustMonth:function(a,b,c,d){var e=this._getNumberOfMonths(a),f=this._daylightSavingAdjust(new Date(c,d+(b<0?b:e[0]*e[1]),1));b<0&&f.setDate(this._getDaysInMonth(f.getFullYear(),f.getMonth()));return this._isInRange(a,f)},_isInRange:function(a,b){var c=this._getMinMaxDate(a,"min"),d=this._getMinMaxDate(a,"max");return(!c||b.getTime()>=c.getTime())&&(!d||b.getTime()<=d.getTime())},_getFormatConfig:function(a){var b=this._get(a,"shortYearCutoff");b=typeof b!="string"?b:(new Date).getFullYear()%100+parseInt(b,10);return{shortYearCutoff:b,dayNamesShort:this._get(a,"dayNamesShort"),dayNames:this._get(a,"dayNames"),monthNamesShort:this._get(a,"monthNamesShort"),monthNames:this._get(a,"monthNames")}},_formatDate:function(a,b,c,d){b||(a.currentDay=a.selectedDay,a.currentMonth=a.selectedMonth,a.currentYear=a.selectedYear);var e=b?typeof b=="object"?b:this._daylightSavingAdjust(new Date(d,c,b)):this._daylightSavingAdjust(new Date +(a.currentYear,a.currentMonth,a.currentDay));return this.formatDate(this._get(a,"dateFormat"),e,this._getFormatConfig(a))}}),$.fn.datepicker=function(a){if(!this.length)return this;$.datepicker.initialized||($(document).mousedown($.datepicker._checkExternalClick).find("body").append($.datepicker.dpDiv),$.datepicker.initialized=!0);var b=Array.prototype.slice.call(arguments,1);if(typeof a=="string"&&(a=="isDisabled"||a=="getDate"||a=="widget"))return $.datepicker["_"+a+"Datepicker"].apply($.datepicker,[this[0]].concat(b));if(a=="option"&&arguments.length==2&&typeof arguments[1]=="string")return $.datepicker["_"+a+"Datepicker"].apply($.datepicker,[this[0]].concat(b));return this.each(function(){typeof a=="string"?$.datepicker["_"+a+"Datepicker"].apply($.datepicker,[this].concat(b)):$.datepicker._attachDatepicker(this,a)})},$.datepicker=new Datepicker,$.datepicker.initialized=!1,$.datepicker.uuid=(new Date).getTime(),$.datepicker.version="1.8.17",window["DP_jQuery_"+dpuuid]=$}(jQuery),function(a,b){var c="ui-dialog ui-widget ui-widget-content ui-corner-all ",d={buttons:!0,height:!0,maxHeight:!0,maxWidth:!0,minHeight:!0,minWidth:!0,width:!0},e={maxHeight:!0,maxWidth:!0,minHeight:!0,minWidth:!0},f=a.attrFn||{val:!0,css:!0,html:!0,text:!0,data:!0,width:!0,height:!0,offset:!0,click:!0};a.widget("ui.dialog",{options:{autoOpen:!0,buttons:{},closeOnEscape:!0,closeText:"close",dialogClass:"",draggable:!0,hide:null,height:"auto",maxHeight:!1,maxWidth:!1,minHeight:150,minWidth:150,modal:!1,position:{my:"center",at:"center",collision:"fit",using:function(b){var c=a(this).css(b).offset().top;c<0&&a(this).css("top",b.top-c)}},resizable:!0,show:null,stack:!0,title:"",width:300,zIndex:1e3},_create:function(){this.originalTitle=this.element.attr("title"),typeof this.originalTitle!="string"&&(this.originalTitle=""),this.options.title=this.options.title||this.originalTitle;var b=this,d=b.options,e=d.title||" ",f=a.ui.dialog.getTitleId(b.element),g=(b.uiDialog=a("
    ")).appendTo(document.body).hide().addClass(c+d.dialogClass).css({zIndex:d.zIndex}).attr("tabIndex",-1).css("outline",0).keydown(function(c){d.closeOnEscape&&!c.isDefaultPrevented()&&c.keyCode&&c.keyCode===a.ui.keyCode.ESCAPE&&(b.close(c),c.preventDefault())}).attr({role:"dialog","aria-labelledby":f}).mousedown(function(a){b.moveToTop(!1,a)}),h=b.element.show().removeAttr("title").addClass("ui-dialog-content ui-widget-content").appendTo(g),i=(b.uiDialogTitlebar=a("
    ")).addClass("ui-dialog-titlebar ui-widget-header ui-corner-all ui-helper-clearfix").prependTo(g),j=a('').addClass("ui-dialog-titlebar-close ui-corner-all").attr("role","button").hover(function(){j.addClass("ui-state-hover")},function(){j.removeClass("ui-state-hover")}).focus(function(){j.addClass("ui-state-focus")}).blur(function(){j.removeClass("ui-state-focus")}).click(function(a){b.close(a);return!1}).appendTo(i),k=(b.uiDialogTitlebarCloseText=a("")).addClass("ui-icon ui-icon-closethick").text(d.closeText).appendTo(j),l=a("").addClass("ui-dialog-title").attr("id",f).html(e).prependTo(i);a.isFunction(d.beforeclose)&&!a.isFunction(d.beforeClose)&&(d.beforeClose=d.beforeclose),i.find("*").add(i).disableSelection(),d.draggable&&a.fn.draggable&&b._makeDraggable(),d.resizable&&a.fn.resizable&&b._makeResizable(),b._createButtons(d.buttons),b._isOpen=!1,a.fn.bgiframe&&g.bgiframe()},_init:function(){this.options.autoOpen&&this.open()},destroy:function(){var a=this;a.overlay&&a.overlay.destroy(),a.uiDialog.hide(),a.element.unbind(".dialog").removeData("dialog").removeClass("ui-dialog-content ui-widget-content").hide().appendTo("body"),a.uiDialog.remove(),a.originalTitle&&a.element.attr("title",a.originalTitle);return a},widget:function(){return this.uiDialog},close:function(b){var c=this,d,e;if(!1!==c._trigger("beforeClose",b)){c.overlay&&c.overlay.destroy(),c.uiDialog.unbind("keypress.ui-dialog"),c._isOpen=!1,c.options.hide?c.uiDialog.hide(c.options.hide,function(){c._trigger("close",b)}):(c.uiDialog.hide(),c._trigger("close",b)),a.ui.dialog.overlay.resize(),c.options.modal&&(d=0,a(".ui-dialog").each(function(){this!==c.uiDialog[0]&&(e=a(this).css("z-index"),isNaN(e)||(d=Math.max(d,e)))}),a.ui.dialog.maxZ=d);return c}},isOpen:function(){return this._isOpen},moveToTop:function(b,c){var d=this,e=d.options,f;if(e.modal&&!b||!e.stack&&!e.modal)return d._trigger("focus",c);e.zIndex>a.ui.dialog.maxZ&&(a.ui.dialog.maxZ=e.zIndex),d.overlay&&(a.ui.dialog.maxZ+=1,d.overlay.$el.css("z-index",a.ui.dialog.overlay.maxZ=a.ui.dialog.maxZ)),f={scrollTop:d.element.scrollTop(),scrollLeft:d.element.scrollLeft()},a.ui.dialog.maxZ+=1,d.uiDialog.css("z-index",a.ui.dialog.maxZ),d.element.attr(f),d._trigger("focus",c);return d},open:function(){if(!this._isOpen){var b=this,c=b.options,d=b.uiDialog;b.overlay=c.modal?new a.ui.dialog.overlay(b):null,b._size(),b._position(c.position),d.show(c.show),b.moveToTop(!0),c.modal&&d.bind("keydown.ui-dialog",function(b){if(b.keyCode===a.ui.keyCode.TAB){var c=a(":tabbable",this),d=c.filter(":first"),e=c.filter(":last");if(b.target===e[0]&&!b.shiftKey){d.focus(1);return!1}if(b.target===d[0]&&b.shiftKey){e.focus(1);return!1}}}),a(b.element.find(":tabbable").get().concat(d.find(".ui-dialog-buttonpane :tabbable").get().concat(d.get()))).eq(0).focus(),b._isOpen=!0,b._trigger("open");return b}},_createButtons:function(b){var c=this,d=!1,e=a("
    ").addClass("ui-dialog-buttonpane ui-widget-content ui-helper-clearfix"),g=a("
    ").addClass("ui-dialog-buttonset").appendTo(e);c.uiDialog.find(".ui-dialog-buttonpane").remove(),typeof b=="object"&&b!==null&&a.each(b,function(){return!(d=!0)}),d&&(a.each(b,function(b,d){d=a.isFunction(d)?{click:d,text:b}:d;var e=a('').click(function(){d.click.apply(c.element[0],arguments)}).appendTo(g);a.each(d,function(a,b){a!=="click"&&(a in f?e[a](b):e.attr(a,b))}),a.fn.button&&e.button()}),e.appendTo(c.uiDialog))},_makeDraggable:function(){function f(a){return{position:a.position,offset:a.offset}}var b=this,c=b.options,d=a(document),e;b.uiDialog.draggable({cancel:".ui-dialog-content, .ui-dialog-titlebar-close",handle:".ui-dialog-titlebar",containment:"document",start:function(d,g){e=c.height==="auto"?"auto":a(this).height(),a(this).height(a(this).height()).addClass("ui-dialog-dragging"),b._trigger("dragStart",d,f(g))},drag:function(a,c){b._trigger("drag",a,f(c))},stop:function(g,h){c.position=[h.position.left-d.scrollLeft(),h.position.top-d.scrollTop()],a(this).removeClass("ui-dialog-dragging").height(e),b._trigger("dragStop",g,f(h)),a.ui.dialog.overlay.resize()}})},_makeResizable:function(c){function h(a){return{originalPosition:a.originalPosition,originalSize:a.originalSize,position:a.position,size:a.size}}c=c===b?this.options.resizable:c;var d=this,e=d.options,f=d.uiDialog.css("position"),g=typeof c=="string"?c:"n,e,s,w,se,sw,ne,nw";d.uiDialog.resizable({cancel:".ui-dialog-content",containment:"document",alsoResize:d.element,maxWidth:e.maxWidth,maxHeight:e.maxHeight,minWidth:e.minWidth,minHeight:d._minHeight(),handles:g,start:function(b,c){a(this).addClass("ui-dialog-resizing"),d._trigger("resizeStart",b,h(c))},resize:function(a,b){d._trigger("resize",a,h(b))},stop:function(b,c){a(this).removeClass("ui-dialog-resizing"),e.height=a(this).height(),e.width=a(this).width(),d._trigger("resizeStop",b,h(c)),a.ui.dialog.overlay.resize()}}).css("position",f).find(".ui-resizable-se").addClass("ui-icon ui-icon-grip-diagonal-se")},_minHeight:function(){var a=this.options;return a.height==="auto"?a.minHeight:Math.min(a.minHeight,a.height)},_position:function(b){var c=[],d=[0,0],e;if(b){if(typeof b=="string"||typeof b=="object"&&"0"in b)c=b.split?b.split(" "):[b[0],b[1]],c.length===1&&(c[1]=c[0]),a.each(["left","top"],function(a,b){+c[a]===c[a]&&(d[a]=c[a],c[a]=b)}),b={my:c.join(" "),at:c.join(" "),offset:d.join(" ")};b=a.extend({},a.ui.dialog.prototype.options.position,b)}else b=a.ui.dialog.prototype.options.position;e=this.uiDialog.is(":visible"),e||this.uiDialog.show(),this.uiDialog.css({top:0,left:0}).position(a.extend({of:window},b)),e||this.uiDialog.hide()},_setOptions:function(b){var c=this,f={},g=!1;a.each(b,function(a,b){c._setOption(a,b),a in d&&(g=!0),a in e&&(f[a]=b)}),g&&this._size(),this.uiDialog.is(":data(resizable)")&&this.uiDialog.resizable("option",f)},_setOption:function(b,d){var e=this,f=e.uiDialog;switch(b){case"beforeclose":b="beforeClose";break;case"buttons":e._createButtons(d);break;case"closeText":e.uiDialogTitlebarCloseText.text(""+d);break;case"dialogClass":f.removeClass(e.options.dialogClass).addClass(c+d);break;case"disabled":d?f.addClass("ui-dialog-disabled"):f.removeClass("ui-dialog-disabled");break;case"draggable":var g=f.is(":data(draggable)");g&&!d&&f.draggable("destroy"),!g&&d&&e._makeDraggable();break;case"position":e._position(d);break;case"resizable":var h=f.is(":data(resizable)");h&&!d&&f.resizable("destroy"),h&&typeof d=="string"&&f.resizable("option","handles",d),!h&&d!==!1&&e._makeResizable(d);break;case"title":a(".ui-dialog-title",e.uiDialogTitlebar).html(""+(d||" "))}a.Widget.prototype._setOption.apply(e,arguments)},_size:function(){var b=this.options,c,d,e=this.uiDialog.is(":visible");this.element.show().css({width:"auto",minHeight:0,height:0}),b.minWidth>b.width&&(b.width=b.minWidth),c=this.uiDialog.css({height:"auto",width:b.width}).height(),d=Math.max(0,b.minHeight-c);if(b.height==="auto")if(a.support.minHeight)this.element.css({minHeight:d,height:"auto"});else{this.uiDialog.show();var f=this.element.css("height","auto").height();e||this.uiDialog.hide(),this.element.height(Math.max(f,d))}else this.element.height(Math.max(b.height-c,0));this.uiDialog.is(":data(resizable)")&&this.uiDialog.resizable("option","minHeight",this._minHeight())}}),a.extend(a.ui.dialog,{version:"1.8.17",uuid:0,maxZ:0,getTitleId:function(a){var b=a.attr("id");b||(this.uuid+=1,b=this.uuid);return"ui-dialog-title-"+b},overlay:function(b){this.$el=a.ui.dialog.overlay.create(b)}}),a.extend(a.ui.dialog.overlay,{instances:[],oldInstances:[],maxZ:0,events:a.map("focus,mousedown,mouseup,keydown,keypress,click".split(","),function(a){return a+".dialog-overlay"}).join(" "),create:function(b){this.instances.length===0&&(setTimeout(function(){a.ui.dialog.overlay.instances.length&&a(document).bind(a.ui.dialog.overlay.events,function(b){if(a(b.target).zIndex()").addClass("ui-widget-overlay")).appendTo(document.body).css({width:this.width(),height:this.height()});a.fn.bgiframe&&c.bgiframe(),this.instances.push(c);return c},destroy:function(b){var c=a.inArray(b,this.instances);c!=-1&&this.oldInstances.push(this.instances.splice(c,1)[0]),this.instances.length===0&&a([document,window]).unbind(".dialog-overlay"),b.remove();var d=0;a.each(this.instances,function(){d=Math.max(d,this.css("z-index"))}),this.maxZ=d},height:function(){var b,c;if(a.browser.msie&&a.browser.version<7){b=Math.max(document.documentElement.scrollHeight,document.body.scrollHeight),c=Math.max(document.documentElement.offsetHeight,document.body.offsetHeight);return b0?b.left-e:Math.max(b.left-c.collisionPosition.left,b.left)},top:function(b,c){var d=a(window),e=c.collisionPosition.top+c.collisionHeight-d.height()-d.scrollTop();b.top=e>0?b.top-e:Math.max(b.top-c.collisionPosition.top,b.top)}},flip:{left:function(b,c){if(c.at[0]!==e){var d=a(window),f=c.collisionPosition.left+c.collisionWidth-d.width()-d.scrollLeft(),g=c.my[0]==="left"?-c.elemWidth:c.my[0]==="right"?c.elemWidth:0,h=c.at[0]==="left"?c.targetWidth:-c.targetWidth,i=-2*c.offset[0];b.left+=c.collisionPosition.left<0?g+h+i:f>0?g+h+i:0}},top:function(b,c){if(c.at[1]!==e){var d=a(window),f=c.collisionPosition.top+c.collisionHeight-d.height()-d.scrollTop(),g=c.my[1]==="top"?-c.elemHeight:c.my[1]==="bottom"?c.elemHeight:0,h=c.at[1]==="top"?c.targetHeight:-c.targetHeight,i=-2*c.offset[1];b.top+=c.collisionPosition.top<0?g+h+i:f>0?g+h+i:0}}}},a.offset.setOffset||(a.offset.setOffset=function(b,c){/static/.test(a.curCSS(b,"position"))&&(b.style.position="relative");var d=a(b),e=d.offset(),f=parseInt(a.curCSS(b,"top",!0),10)||0,g=parseInt(a.curCSS(b,"left",!0),10)||0,h={top:c.top-e.top+f,left:c.left-e.left+g};"using"in c?c.using.call(b,h):d.css(h)},a.fn.offset=function(b){var c=this[0];if(!c||!c.ownerDocument)return null;if(b)return this.each(function(){a.offset.setOffset(this,b)});return h.call(this)}),function(){var b=document.getElementsByTagName("body")[0],c=document.createElement("div"),d,e,g,h,i;d=document.createElement(b?"div":"body"),g={visibility:"hidden",width:0,height:0,border:0,margin:0,background:"none"},b&&jQuery.extend(g,{position:"absolute",left:"-1000px",top:"-1000px"});for(var j in g)d.style[j]=g[j];d.appendChild(c),e=b||document.documentElement,e.insertBefore(d,e.firstChild),c.style.cssText="position: absolute; left: 10.7432222px; top: 10.432325px; height: 30px; width: 201px;",h=a(c).offset(function(a,b){return b}).offset(),d.innerHTML="",e.removeChild(d),i=h.top+h.left+(b?2e3:0),f.fractions=i>21&&i<22}()}(jQuery),function(a,b){a.widget("ui.progressbar",{options:{value:0,max:100},min:0,_create:function(){this.element.addClass("ui-progressbar ui-widget ui-widget-content ui-corner-all").attr({role:"progressbar","aria-valuemin":this.min,"aria-valuemax":this.options.max,"aria-valuenow":this._value()}),this.valueDiv=a("
    ").appendTo(this.element),this.oldValue=this._value(),this._refreshValue()},destroy:function(){this.element.removeClass("ui-progressbar ui-widget ui-widget-content ui-corner-all").removeAttr("role").removeAttr("aria-valuemin").removeAttr("aria-valuemax").removeAttr("aria-valuenow"),this.valueDiv.remove(),a.Widget.prototype.destroy.apply(this,arguments)},value:function(a){if(a===b)return this._value();this._setOption("value",a);return this},_setOption:function(b,c){b==="value"&&(this.options.value=c,this._refreshValue(),this._value()===this.options.max&&this._trigger("complete")),a.Widget.prototype._setOption.apply(this,arguments)},_value:function(){var a=this.options.value;typeof a!="number"&&(a=0);return Math.min(this.options.max,Math.max(this.min,a))},_percentage:function(){return 100*this._value()/this.options.max},_refreshValue:function(){var a=this.value(),b=this._percentage();this.oldValue!==a&&(this.oldValue=a,this._trigger("change")),this.valueDiv.toggle(a>this.min).toggleClass("ui-corner-right",a===this.options.max).width(b.toFixed(0)+"%"),this.element.attr("aria-valuenow",a)}}),a.extend(a.ui.progressbar,{version:"1.8.17"})}(jQuery),function(a,b){var c=5;a.widget("ui.slider",a.ui.mouse,{widgetEventPrefix:"slide",options:{animate:!1,distance:0,max:100,min:0,orientation:"horizontal",range:!1,step:1,value:0,values:null},_create:function(){var b=this,d=this.options,e=this.element.find(".ui-slider-handle").addClass("ui-state-default ui-corner-all"),f="",g=d.values&&d.values.length||1,h=[];this._keySliding=!1,this._mouseSliding=!1,this._animateOff=!0,this._handleIndex=null,this._detectOrientation(),this._mouseInit(),this.element.addClass("ui-slider ui-slider-"+this.orientation+" ui-widget"+" ui-widget-content"+" ui-corner-all"+(d.disabled?" ui-slider-disabled ui-disabled":"")),this.range=a([]),d.range&&(d.range===!0&&(d.values||(d.values=[this._valueMin(),this._valueMin()]),d.values.length&&d.values.length!==2&&(d.values=[d.values[0],d.values[0]])),this.range=a("
    ").appendTo(this.element).addClass("ui-slider-range ui-widget-header"+(d.range==="min"||d.range==="max"?" ui-slider-range-"+d.range:"")));for(var i=e.length;ic&&(f=c,g=a(this),i=b)}),c.range===!0&&this.values(1)===c.min&&(i+=1,g=a(this.handles[i])),j=this._start(b,i);if(j===!1)return!1;this._mouseSliding=!0,h._handleIndex=i,g.addClass("ui-state-active").focus(),k=g.offset(),l=!a(b.target).parents().andSelf().is(".ui-slider-handle"),this._clickOffset=l?{left:0,top:0}:{left:b.pageX-k.left-g.width()/2,top:b.pageY-k.top-g.height()/2-(parseInt(g.css("borderTopWidth"),10)||0)-(parseInt(g.css("borderBottomWidth"),10)||0)+(parseInt(g.css("marginTop"),10)||0)},this.handles.hasClass("ui-state-hover")||this._slide(b,i,e),this._animateOff=!0;return!0},_mouseStart:function(a){return!0},_mouseDrag:function(a){var b={x:a.pageX,y:a.pageY},c=this._normValueFromMouse(b);this._slide(a,this._handleIndex,c);return!1},_mouseStop:function(a){this.handles.removeClass("ui-state-active"),this._mouseSliding=!1,this._stop(a,this._handleIndex),this._change(a,this._handleIndex),this._handleIndex=null,this._clickOffset=null,this._animateOff=!1;return!1},_detectOrientation:function(){this.orientation=this.options.orientation==="vertical"?"vertical":"horizontal"},_normValueFromMouse:function(a){var b,c,d,e,f;this.orientation==="horizontal"?(b=this.elementSize.width,c=a.x-this.elementOffset.left-(this._clickOffset?this._clickOffset.left:0)):(b=this.elementSize.height,c=a.y-this.elementOffset.top-(this._clickOffset?this._clickOffset.top:0)),d=c/b,d>1&&(d=1),d<0&&(d=0),this.orientation==="vertical"&&(d=1-d),e=this._valueMax()-this._valueMin(),f=this._valueMin()+d*e;return this._trimAlignValue(f)},_start:function(a,b){var c={handle:this.handles[b],value:this.value()};this.options.values&&this.options.values.length&&(c.value=this.values(b),c.values=this.values());return this._trigger("start",a,c)},_slide:function(a,b,c){var d,e,f;this.options.values&&this.options.values.length?(d=this.values(b?0:1),this.options.values.length===2&&this.options.range===!0&&(b===0&&c>d||b===1&&c1)this.options.values[b]=this._trimAlignValue(c),this._refreshValue(),this._change(null,b);else{if(!arguments.length)return this._values();if(!a.isArray(arguments[0]))return this.options.values&&this.options.values.length?this._values(b):this.value();d=this.options.values,e=arguments[0];for(f=0;f=this._valueMax())return this._valueMax();var b=this.options.step>0?this.options.step:1,c=(a-this._valueMin())%b,d=a-c;Math.abs(c)*2>=b&&(d+=c>0?b:-b);return parseFloat(d.toFixed(5))},_valueMin:function(){return this.options.min},_valueMax:function(){return this.options.max},_refreshValue:function(){var b=this.options.range,c=this.options,d=this,e=this._animateOff?!1:c.animate,f,g={},h,i,j,k;this.options.values&&this.options.values.length?this.handles.each(function(b,i){f=(d.values(b)-d._valueMin())/(d._valueMax()-d._valueMin())*100,g[d.orientation==="horizontal"?"left":"bottom"]=f+"%",a(this).stop(1,1)[e?"animate":"css"](g,c.animate),d.options.range===!0&&(d.orientation==="horizontal"?(b===0&&d.range.stop(1,1)[e?"animate":"css"]({left:f+"%"},c.animate),b===1&&d.range[e?"animate":"css"]({width:f-h+"%"},{queue:!1,duration:c.animate})):(b===0&&d.range.stop(1,1)[e?"animate":"css"]({bottom:f+"%"},c.animate),b===1&&d.range[e?"animate":"css"]({height:f-h+"%"},{queue:!1,duration:c.animate}))),h=f}):(i=this.value(),j=this._valueMin(),k=this._valueMax(),f=k!==j?(i-j)/(k-j)*100:0,g[d.orientation==="horizontal"?"left":"bottom"]=f+"%",this.handle.stop(1,1)[e?"animate":"css"](g,c.animate),b==="min"&&this.orientation==="horizontal"&&this.range.stop(1,1)[e?"animate":"css"]({width:f+"%"},c.animate),b==="max"&&this.orientation==="horizontal"&&this.range[e?"animate":"css"]({width:100-f+"%"},{queue:!1,duration:c.animate}),b==="min"&&this.orientation==="vertical"&&this.range.stop(1,1)[e?"animate":"css"]({height:f+"%"},c.animate),b==="max"&&this.orientation==="vertical"&&this.range[e?"animate":"css"]({height:100-f+"%"},{queue:!1,duration:c.animate}))}}),a.extend(a.ui.slider,{version:"1.8.17"})}(jQuery),function(a,b){function f(){return++d}function e(){return++c}var c=0,d=0;a.widget("ui.tabs",{options:{add:null,ajaxOptions:null,cache:!1,cookie:null,collapsible:!1,disable:null,disabled:[],enable:null,event:"click",fx:null,idPrefix:"ui-tabs-",load:null,panelTemplate:"
    ",remove:null,select:null,show:null,spinner:"Loading…",tabTemplate:"
  • #{label}
  • "},_create:function(){this._tabify(!0)},_setOption:function(a,b){if(a=="selected"){if(this.options.collapsible&&b==this.options.selected)return;this.select(b)}else this.options[a]=b,this._tabify()},_tabId:function(a){return a.title&&a.title.replace(/\s/g,"_").replace(/[^\w\u00c0-\uFFFF-]/g,"")||this.options.idPrefix+e()},_sanitizeSelector:function(a){return a.replace(/:/g,"\\:")},_cookie:function(){var b=this.cookie||(this.cookie=this.options.cookie.name||"ui-tabs-"+f());return a.cookie.apply(null,[b].concat(a.makeArray(arguments)))},_ui:function(a,b){return{tab:a,panel:b,index:this.anchors.index(a)}},_cleanup:function(){this.lis.filter(".ui-state-processing").removeClass("ui-state-processing").find("span:data(label.tabs)").each(function(){var b=a(this);b.html(b.data("label.tabs")).removeData("label.tabs")})},_tabify:function(c){function m(b,c){b.css("display",""),!a.support.opacity&&c.opacity&&b[0].style.removeAttribute("filter")}var d=this,e=this.options,f=/^#.+/;this.list=this.element.find("ol,ul").eq(0),this.lis=a(" > li:has(a[href])",this.list),this.anchors=this.lis.map(function(){return a("a",this)[0]}),this.panels=a([]),this.anchors.each(function(b,c){var g=a(c).attr("href"),h=g.split("#")[0],i;h&&(h===location.toString().split("#")[0]||(i=a("base")[0])&&h===i.href)&&(g=c.hash,c.href=g);if(f.test(g))d.panels=d.panels.add(d.element.find(d._sanitizeSelector(g)));else if(g&&g!=="#"){a.data(c,"href.tabs",g),a.data(c,"load.tabs",g.replace(/#.*$/,""));var j=d._tabId(c);c.href="#"+j;var k=d.element.find("#"+j);k.length||(k=a(e.panelTemplate).attr("id",j).addClass("ui-tabs-panel ui-widget-content ui-corner-bottom").insertAfter(d.panels[b-1]||d.list),k.data("destroy.tabs",!0)),d.panels=d.panels.add(k)}else e.disabled.push(b)}),c?(this.element.addClass("ui-tabs ui-widget ui-widget-content ui-corner-all"),this.list.addClass("ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all"),this.lis.addClass("ui-state-default ui-corner-top"),this.panels.addClass("ui-tabs-panel ui-widget-content ui-corner-bottom"),e.selected===b?(location.hash&&this.anchors.each(function(a,b){if(b.hash==location.hash){e.selected=a;return!1}}),typeof e.selected!="number"&&e.cookie&&(e.selected=parseInt(d._cookie(),10)),typeof e.selected!="number"&&this.lis.filter(".ui-tabs-selected").length&&(e.selected=this.lis.index(this.lis.filter(".ui-tabs-selected"))),e.selected=e.selected||(this.lis.length?0:-1)):e.selected===null&&(e.selected=-1),e.selected=e.selected>=0&&this.anchors[e.selected]||e.selected<0?e.selected:0,e.disabled=a.unique(e.disabled.concat(a.map(this.lis.filter(".ui-state-disabled"),function(a,b){return d.lis.index(a)}))).sort(),a.inArray(e.selected,e.disabled)!=-1&&e.disabled.splice(a.inArray(e.selected,e.disabled),1),this.panels.addClass("ui-tabs-hide"),this.lis.removeClass("ui-tabs-selected ui-state-active"),e.selected>=0&&this.anchors.length&&(d.element.find(d._sanitizeSelector(d.anchors[e.selected].hash)).removeClass("ui-tabs-hide"),this.lis.eq(e.selected).addClass("ui-tabs-selected ui-state-active"),d.element.queue("tabs",function(){d._trigger("show",null,d._ui(d.anchors[e.selected],d.element.find(d._sanitizeSelector(d.anchors[e.selected].hash))[0]))}),this.load(e.selected)),a(window).bind("unload",function(){d.lis.add(d.anchors).unbind(".tabs"),d.lis=d.anchors=d.panels=null})):e.selected=this.lis.index(this.lis.filter(".ui-tabs-selected")),this.element[e.collapsible?"addClass":"removeClass"]("ui-tabs-collapsible"),e.cookie&&this._cookie(e.selected,e.cookie);for(var g=0,h;h=this.lis[g];g++)a(h)[a.inArray(g,e.disabled)!=-1&&!a(h).hasClass("ui-tabs-selected")?"addClass":"removeClass"]("ui-state-disabled");e.cache===!1&&this.anchors.removeData("cache.tabs"),this.lis.add(this.anchors).unbind(".tabs");if(e.event!=="mouseover"){var i=function(a,b){b.is(":not(.ui-state-disabled)")&&b.addClass("ui-state-"+a)},j=function(a,b){b.removeClass("ui-state-"+a)};this.lis.bind("mouseover.tabs",function(){i("hover",a(this))}),this.lis.bind("mouseout.tabs",function(){j("hover",a(this))}),this.anchors.bind("focus.tabs",function(){i("focus",a(this).closest("li"))}),this.anchors.bind("blur.tabs",function(){j("focus",a(this).closest("li"))})}var k,l;e.fx&&(a.isArray(e.fx)?(k=e.fx[0],l=e.fx[1]):k=l=e.fx);var n=l?function(b,c){a(b).closest("li").addClass("ui-tabs-selected ui-state-active"),c.hide().removeClass("ui-tabs-hide").animate(l,l.duration||"normal",function(){m(c,l),d._trigger("show",null,d._ui(b,c[0]))})}:function(b,c){a(b).closest("li").addClass("ui-tabs-selected ui-state-active"),c.removeClass("ui-tabs-hide"),d._trigger("show",null,d._ui(b,c[0]))},o=k?function(a,b){b.animate(k,k.duration||"normal",function(){d.lis.removeClass("ui-tabs-selected ui-state-active"),b.addClass("ui-tabs-hide"),m(b,k),d.element.dequeue("tabs")})}:function(a,b,c){d.lis.removeClass("ui-tabs-selected ui-state-active"),b.addClass("ui-tabs-hide"),d.element.dequeue("tabs")};this.anchors.bind(e.event+".tabs",function(){var b=this,c=a(b).closest("li"),f=d.panels.filter(":not(.ui-tabs-hide)"),g=d.element.find(d._sanitizeSelector(b.hash));if(c.hasClass("ui-tabs-selected")&&!e.collapsible||c.hasClass("ui-state-disabled")||c.hasClass("ui-state-processing" +)||d.panels.filter(":animated").length||d._trigger("select",null,d._ui(this,g[0]))===!1){this.blur();return!1}e.selected=d.anchors.index(this),d.abort();if(e.collapsible){if(c.hasClass("ui-tabs-selected")){e.selected=-1,e.cookie&&d._cookie(e.selected,e.cookie),d.element.queue("tabs",function(){o(b,f)}).dequeue("tabs"),this.blur();return!1}if(!f.length){e.cookie&&d._cookie(e.selected,e.cookie),d.element.queue("tabs",function(){n(b,g)}),d.load(d.anchors.index(this)),this.blur();return!1}}e.cookie&&d._cookie(e.selected,e.cookie);if(g.length)f.length&&d.element.queue("tabs",function(){o(b,f)}),d.element.queue("tabs",function(){n(b,g)}),d.load(d.anchors.index(this));else throw"jQuery UI Tabs: Mismatching fragment identifier.";a.browser.msie&&this.blur()}),this.anchors.bind("click.tabs",function(){return!1})},_getIndex:function(a){typeof a=="string"&&(a=this.anchors.index(this.anchors.filter("[href$="+a+"]")));return a},destroy:function(){var b=this.options;this.abort(),this.element.unbind(".tabs").removeClass("ui-tabs ui-widget ui-widget-content ui-corner-all ui-tabs-collapsible").removeData("tabs"),this.list.removeClass("ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all"),this.anchors.each(function(){var b=a.data(this,"href.tabs");b&&(this.href=b);var c=a(this).unbind(".tabs");a.each(["href","load","cache"],function(a,b){c.removeData(b+".tabs")})}),this.lis.unbind(".tabs").add(this.panels).each(function(){a.data(this,"destroy.tabs")?a(this).remove():a(this).removeClass(["ui-state-default","ui-corner-top","ui-tabs-selected","ui-state-active","ui-state-hover","ui-state-focus","ui-state-disabled","ui-tabs-panel","ui-widget-content","ui-corner-bottom","ui-tabs-hide"].join(" "))}),b.cookie&&this._cookie(null,b.cookie);return this},add:function(c,d,e){e===b&&(e=this.anchors.length);var f=this,g=this.options,h=a(g.tabTemplate.replace(/#\{href\}/g,c).replace(/#\{label\}/g,d)),i=c.indexOf("#")?this._tabId(a("a",h)[0]):c.replace("#","");h.addClass("ui-state-default ui-corner-top").data("destroy.tabs",!0);var j=f.element.find("#"+i);j.length||(j=a(g.panelTemplate).attr("id",i).data("destroy.tabs",!0)),j.addClass("ui-tabs-panel ui-widget-content ui-corner-bottom ui-tabs-hide"),e>=this.lis.length?(h.appendTo(this.list),j.appendTo(this.list[0].parentNode)):(h.insertBefore(this.lis[e]),j.insertBefore(this.panels[e])),g.disabled=a.map(g.disabled,function(a,b){return a>=e?++a:a}),this._tabify(),this.anchors.length==1&&(g.selected=0,h.addClass("ui-tabs-selected ui-state-active"),j.removeClass("ui-tabs-hide"),this.element.queue("tabs",function(){f._trigger("show",null,f._ui(f.anchors[0],f.panels[0]))}),this.load(0)),this._trigger("add",null,this._ui(this.anchors[e],this.panels[e]));return this},remove:function(b){b=this._getIndex(b);var c=this.options,d=this.lis.eq(b).remove(),e=this.panels.eq(b).remove();d.hasClass("ui-tabs-selected")&&this.anchors.length>1&&this.select(b+(b+1=b?--a:a}),this._tabify(),this._trigger("remove",null,this._ui(d.find("a")[0],e[0]));return this},enable:function(b){b=this._getIndex(b);var c=this.options;if(a.inArray(b,c.disabled)!=-1){this.lis.eq(b).removeClass("ui-state-disabled"),c.disabled=a.grep(c.disabled,function(a,c){return a!=b}),this._trigger("enable",null,this._ui(this.anchors[b],this.panels[b]));return this}},disable:function(a){a=this._getIndex(a);var b=this,c=this.options;a!=c.selected&&(this.lis.eq(a).addClass("ui-state-disabled"),c.disabled.push(a),c.disabled.sort(),this._trigger("disable",null,this._ui(this.anchors[a],this.panels[a])));return this},select:function(a){a=this._getIndex(a);if(a==-1)if(this.options.collapsible&&this.options.selected!=-1)a=this.options.selected;else return this;this.anchors.eq(a).trigger(this.options.event+".tabs");return this},load:function(b){b=this._getIndex(b);var c=this,d=this.options,e=this.anchors.eq(b)[0],f=a.data(e,"load.tabs");this.abort();if(!f||this.element.queue("tabs").length!==0&&a.data(e,"cache.tabs"))this.element.dequeue("tabs");else{this.lis.eq(b).addClass("ui-state-processing");if(d.spinner){var g=a("span",e);g.data("label.tabs",g.html()).html(d.spinner)}this.xhr=a.ajax(a.extend({},d.ajaxOptions,{url:f,success:function(f,g){c.element.find(c._sanitizeSelector(e.hash)).html(f),c._cleanup(),d.cache&&a.data(e,"cache.tabs",!0),c._trigger("load",null,c._ui(c.anchors[b],c.panels[b]));try{d.ajaxOptions.success(f,g)}catch(h){}},error:function(a,f,g){c._cleanup(),c._trigger("load",null,c._ui(c.anchors[b],c.panels[b]));try{d.ajaxOptions.error(a,f,b,e)}catch(g){}}})),c.element.dequeue("tabs");return this}},abort:function(){this.element.queue([]),this.panels.stop(!1,!0),this.element.queue("tabs",this.element.queue("tabs").splice(-2,2)),this.xhr&&(this.xhr.abort(),delete this.xhr),this._cleanup();return this},url:function(a,b){this.anchors.eq(a).removeData("cache.tabs").data("load.tabs",b);return this},length:function(){return this.anchors.length}}),a.extend(a.ui.tabs,{version:"1.8.17"}),a.extend(a.ui.tabs.prototype,{rotation:null,rotate:function(a,b){var c=this,d=this.options,e=c._rotate||(c._rotate=function(b){clearTimeout(c.rotation),c.rotation=setTimeout(function(){var a=d.selected;c.select(++a 0) { + gebtn_yields_comments = true; +} + +// Some browsers return node.localName in upper case, some in lower case. +// +var localname_is_uppercase = true; +if(div.localName && div.localName == "div") { + localname_is_uppercase = false; +} + +// Allow the testing div to be garbage-collected. +// +div = null; + + +// Modify the TAG find function to account for a namespace selector. +// +$.expr.find.TAG = function(match,context,isXML) { + var ns = getNamespaceURI(match[2]); + var ln = match[3]; + var res; + if(typeof context.getElementsByTagNameNS != "undefined") { + // Easy case - we have getElementsByTagNameNS + res = context.getElementsByTagNameNS(ns,ln); + } else if(typeof context.selectNodes != "undefined") { + // Use xpath if possible (not available on HTML DOM nodes in IE) + if(context.ownerDocument) { + context.ownerDocument.setProperty("SelectionLanguage","XPath"); + } else { + context.setProperty("SelectionLanguage","XPath"); + } + var predicate = ""; + if(ns != "*") { + if(ln != "*") { + predicate="namespace-uri()='"+ns+"' and local-name()='"+ln+"'"; + } else { + predicate="namespace-uri()='"+ns+"'"; + } + } else { + if(ln != "*") { + predicate="local-name()='"+ln+"'"; + } + } + if(predicate) { + res = context.selectNodes("descendant-or-self::*["+predicate+"]"); + } else { + res = context.selectNodes("descendant-or-self::*"); + } + } else { + // Otherwise, we need to simulate using getElementsByTagName + res = context.getElementsByTagName(ln); + if(gebtn_yields_comments && ln == "*") { + var tmp = []; + for(var i=0; res[i]; i++) { + if(res[i].nodeType == 1) { + tmp.push(res[i]); + } + } + res = tmp; + } + if(res && ns != "*") { + var tmp = []; + for(var i=0; res[i]; i++) { + if(res[i].namespaceURI == ns || res[i].tagUrn == ns) { + tmp.push(res[i]); + } + } + res = tmp; + } + } + return res; +}; + + +// Check whether a node is part of an XML document. +// Copied verbatim from jQuery sources, needed in TAG preFilter below. +// +var isXML = function(elem){ + return elem.nodeType === 9 && elem.documentElement.nodeName !== "HTML" || + !!elem.ownerDocument && elem.ownerDocument.documentElement.nodeName !== "HTML"; +}; + + +// Modify the TAG preFilter function to work with modified match regexp. +// This normalises case of the tag name if we're in a HTML document. +// +$.expr.preFilter.TAG = function(match, curLoop, inplace, result, not, isXML) { + var ln = match[3]; + if(!isXML) { + if(localname_is_uppercase) { + ln = ln.toUpperCase(); + } else { + ln = ln.toLowerCase(); + } + } + return [match[0],getNamespaceURI(match[2]),ln]; +}; + + +// Modify the TAG filter function to account for a namespace selector. +// +$.expr.filter.TAG = function(elem,match) { + var ns = match[1]; + var ln = match[2]; + var e_ns = elem.namespaceURI ? elem.namespaceURI : elem.tagUrn; + var e_ln = elem.localName ? elem.localName : elem.tagName; + if(ns == "*" || e_ns == ns || (ns == "" && !e_ns)) { + return ((ln == "*" && elem.nodeType == 1) || e_ln == ln); + } + return false; +}; + + +// Modify the ATTR match regexp to extract a namespace selector. +// This is basically ([namespace|])(attrname)(op)(quote)(pattern)(quote) +// +setExprMatchRegex("ATTR",/\[\s*((?:((?:[\w\u00c0-\uFFFF\*_-]*\|)?)((?:[\w\u00c0-\uFFFF_-]|\\.)+)))\s*(?:(\S?=)\s*(['"]*)(.*?)\5|)\s*\]/); + + +// Modify the ATTR preFilter function to account for new regexp match groups, +// and normalise the namespace URI. +// +$.expr.preFilter.ATTR = function(match, curLoop, inplace, result, not, isXML) { + var name = match[3].replace(/\\/g, ""); + if(!isXML && $.expr.attrMap[name]) { + match[3] = $.expr.attrMap[name]; + } + if( match[4] == "~=" ) { + match[6] = " " + match[6] + " "; + } + if(!match[2] || match[2] == "|") { + match[2] = ""; + } else { + match[2] = getNamespaceURI(match[2]); + } + return match; +}; + + +// Modify the ATTR filter function to account for namespace selector. +// Unfortunately this means factoring out the attribute-checking code +// into a separate function, since it might be called multiple times. +// +var filter_attr = function(result,type,check) { + var value = result + ""; + return result == null ? + type === "!=" : + type === "=" ? + value === check : + type === "*=" ? + value.indexOf(check) >= 0 : + type === "~=" ? + (" " + value + " ").indexOf(check) >= 0 : + !check ? + value && result !== false : + type === "!=" ? + value != check : + type === "^=" ? + value.indexOf(check) === 0 : + type === "$=" ? + value.substr(value.length - check.length) === check : + type === "|=" ? + value === check || value.substr(0,check.length+1)===check+"-" : + false; +} + + +$.expr.filter.ATTR = function(elem, match) { + var ns = match[2]; + var name = match[3]; + var type = match[4]; + var check = match[6]; + var result; + // No namespace, just use ordinary attribute lookup. + if(ns == "") { + result = $.expr.attrHandle[name] ? + $.expr.attrHandle[name](elem) : + elem[name] != null ? + elem[name] : + elem.getAttribute(name); + return filter_attr(result,type,check); + } + // Directly use getAttributeNS if applicable and available + if(ns != "*" && typeof elem.getAttributeNS != "undefined") { + return filter_attr(elem.getAttributeNS(ns,name),type,check); + } + // Need to iterate over all attributes, either because we couldn't + // look it up or because we need to match all namespaces. + var attrs = elem.attributes; + for(var i=0; attrs[i]; i++) { + var ln = attrs[i].localName; + if(!ln) { + ln = attrs[i].nodeName + var idx = ln.indexOf(":"); + if(idx >= 0) { + ln = ln.substr(idx+1); + } + } + if(ln == name) { + result = attrs[i].nodeValue; + if(ns == "*" || attrs[i].namespaceURI == ns) { + if(filter_attr(result,type,check)) { + return true; + } + } + if(attrs[i].namespaceURI === "" && attrs[i].prefix) { + if(attrs[i].prefix == default_xmlns_rev[ns]) { + if(filter_attr(result,type,check)) { + return true; + } + } + } + } + } + return false; +}; + + +})(jQuery); + diff -Nru eoxserver-0.4.0beta2/eoxserver/webclient/static/libcoverage.js/libcoverage.complete.min.js eoxserver-0.3.2/eoxserver/webclient/static/libcoverage.js/libcoverage.complete.min.js --- eoxserver-0.4.0beta2/eoxserver/webclient/static/libcoverage.js/libcoverage.complete.min.js 1970-01-01 00:00:00.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/webclient/static/libcoverage.js/libcoverage.complete.min.js 2013-12-10 14:57:00.000000000 +0000 @@ -0,0 +1,26 @@ +var namespace=function(c,b,a){var c=c.split(b||"."),a=a||window,d,b=0;for(d=c.length;b) + +*/ + +/** + * Contains XMLHttpRequest.js + * Copyright 2007 Sergey Ilinsky (http://www.ilinsky.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + */ + +/** + * OpenLayers.Util.pagePosition is based on Yahoo's getXY method, which is + * Copyright (c) 2006, Yahoo! Inc. + * All rights reserved. + * + * Redistribution and use of this software 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 Yahoo! Inc. nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission of Yahoo! Inc. + * + * 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 THE COPYRIGHT OWNER OR CONTRIBUTORS 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. + */ +var OpenLayers={VERSION_NUMBER:"Release 2.12-rc6",singleFile:!0,_getScriptLocation:function(){for(var a=/(^|(.*?\/))(OpenLayers[^\/]*?\.js)(\?|$)/,b=document.getElementsByTagName("script"),c,d="",e=0,f=b.length;ethis.right)this.right=b.right;if(null==this.top||b.top>this.top)this.top=b.top}}},containsLonLat:function(a,b){"boolean"===typeof b&&(b={inclusive:b});var b=b||{},c=this.contains(a.lon,a.lat,b.inclusive),d=b.worldBounds;d&&!c&&(c=d.getWidth(),d=Math.round((a.lon-(d.left+d.right)/2)/c),c=this.containsLonLat({lon:a.lon-d*c,lat:a.lat},{inclusive:b.inclusive}));return c},containsPixel:function(a,b){return this.contains(a.x, +a.y,b)},contains:function(a,b,c){null==c&&(c=!0);if(null==a||null==b)return!1;var a=OpenLayers.Util.toFloat(a),b=OpenLayers.Util.toFloat(b),d=!1;return d=c?a>=this.left&&a<=this.right&&b>=this.bottom&&b<=this.top:a>this.left&&athis.bottom&&b=c.bottom&&a.top<=c.top||c.top>a.bottom&&c.top=c.left&&a.left<=c.right||c.left>=a.left&&c.left<=a.right,f=a.right>=c.left&&a.right<=c.right||c.right>=a.left&&c.right<=a.right,d=(a.bottom>=c.bottom&&a.bottom<=c.top||c.bottom>=a.bottom&&c.bottom<=a.top||d)&&(e||f);if(b.worldBounds&&!d){var g=b.worldBounds,e=g.getWidth(),f=!g.containsBounds(c),g=!g.containsBounds(a);f&&!g?(a=a.add(-e,0),d=c.intersectsBounds(a, +{inclusive:b.inclusive})):g&&!f&&(c=c.add(-e,0),d=a.intersectsBounds(c,{inclusive:b.inclusive}))}return d},containsBounds:function(a,b,c){null==b&&(b=!1);null==c&&(c=!0);var d=this.contains(a.left,a.bottom,c),e=this.contains(a.right,a.bottom,c),f=this.contains(a.left,a.top,c),a=this.contains(a.right,a.top,c);return b?d||e||f||a:d&&e&&f&&a},determineQuadrant:function(a){var b="",c=this.getCenterLonLat(),b=b+(a.lat=a.right&&e.right>a.right;)e=e.add(-f,0);c=e.left+c;ca.left&&e.right-d>a.right)&&(e=e.add(-f,0))}return e},CLASS_NAME:"OpenLayers.Bounds"});OpenLayers.Bounds.fromString=function(a,b){var c=a.split(",");return OpenLayers.Bounds.fromArray(c,b)};OpenLayers.Bounds.fromArray=function(a,b){return!0===b?new OpenLayers.Bounds(a[1],a[0],a[3],a[2]):new OpenLayers.Bounds(a[0],a[1],a[2],a[3])}; +OpenLayers.Bounds.fromSize=function(a){return new OpenLayers.Bounds(0,a.h,a.w,0)};OpenLayers.Bounds.oppositeQuadrant=function(a){var b;b=""+("t"==a.charAt(0)?"b":"t");return b+="l"==a.charAt(1)?"r":"l"};OpenLayers.Element={visible:function(a){return"none"!=OpenLayers.Util.getElement(a).style.display},toggle:function(){for(var a=0,b=arguments.length;aa.right;)b.lon-=a.getWidth()}return b},CLASS_NAME:"OpenLayers.LonLat"}); +OpenLayers.LonLat.fromString=function(a){a=a.split(",");return new OpenLayers.LonLat(a[0],a[1])};OpenLayers.LonLat.fromArray=function(a){var b=OpenLayers.Util.isArray(a);return new OpenLayers.LonLat(b&&a[0],b&&a[1])};OpenLayers.Pixel=OpenLayers.Class({x:0,y:0,initialize:function(a,b){this.x=parseFloat(a);this.y=parseFloat(b)},toString:function(){return"x="+this.x+",y="+this.y},clone:function(){return new OpenLayers.Pixel(this.x,this.y)},equals:function(a){var b=!1;null!=a&&(b=this.x==a.x&&this.y==a.y||isNaN(this.x)&&isNaN(this.y)&&isNaN(a.x)&&isNaN(a.y));return b},distanceTo:function(a){return Math.sqrt(Math.pow(this.x-a.x,2)+Math.pow(this.y-a.y,2))},add:function(a,b){if(null==a||null==b)throw new TypeError("Pixel.add cannot receive null values"); +return new OpenLayers.Pixel(this.x+a,this.y+b)},offset:function(a){var b=this.clone();a&&(b=this.add(a.x,a.y));return b},CLASS_NAME:"OpenLayers.Pixel"});OpenLayers.Size=OpenLayers.Class({w:0,h:0,initialize:function(a,b){this.w=parseFloat(a);this.h=parseFloat(b)},toString:function(){return"w="+this.w+",h="+this.h},clone:function(){return new OpenLayers.Size(this.w,this.h)},equals:function(a){var b=!1;null!=a&&(b=this.w==a.w&&this.h==a.h||isNaN(this.w)&&isNaN(this.h)&&isNaN(a.w)&&isNaN(a.h));return b},CLASS_NAME:"OpenLayers.Size"});OpenLayers.Console={log:function(){},debug:function(){},info:function(){},warn:function(){},error:function(){},userError:function(a){alert(a)},assert:function(){},dir:function(){},dirxml:function(){},trace:function(){},group:function(){},groupEnd:function(){},time:function(){},timeEnd:function(){},profile:function(){},profileEnd:function(){},count:function(){},CLASS_NAME:"OpenLayers.Console"}; +(function(){for(var a=document.getElementsByTagName("script"),b=0,c=a.length;b=0;c--)a[c]==b&&a.splice(c,1);return a};OpenLayers.Util.indexOf=function(a,b){if(typeof a.indexOf=="function")return a.indexOf(b);for(var c=0,d=a.length;c=0&&parseFloat(h)<1){a.style.filter="alpha(opacity="+h*100+")";a.style.opacity=h}else if(parseFloat(h)==1){a.style.filter="";a.style.opacity=""}}; +OpenLayers.Util.createDiv=function(a,b,c,d,e,f,g,h){var i=document.createElement("div");if(d)i.style.backgroundImage="url("+d+")";a||(a=OpenLayers.Util.createUniqueID("OpenLayersDiv"));e||(e="absolute");OpenLayers.Util.modifyDOMElement(i,a,b,c,e,f,g,h);return i}; +OpenLayers.Util.createImage=function(a,b,c,d,e,f,g,h){var i=document.createElement("img");a||(a=OpenLayers.Util.createUniqueID("OpenLayersDiv"));e||(e="relative");OpenLayers.Util.modifyDOMElement(i,a,b,c,e,f,null,g);if(h){i.style.display="none";b=function(){i.style.display="";OpenLayers.Event.stopObservingElement(i)};OpenLayers.Event.observe(i,"load",b);OpenLayers.Event.observe(i,"error",b)}i.style.alt=a;i.galleryImg="no";if(d)i.src=d;return i};OpenLayers.IMAGE_RELOAD_ATTEMPTS=0; +OpenLayers.Util.alphaHackNeeded=null;OpenLayers.Util.alphaHack=function(){if(OpenLayers.Util.alphaHackNeeded==null){var a=navigator.appVersion.split("MSIE"),a=parseFloat(a[1]),b=false;try{b=!!document.body.filters}catch(c){}OpenLayers.Util.alphaHackNeeded=b&&a>=5.5&&a<7}return OpenLayers.Util.alphaHackNeeded}; +OpenLayers.Util.modifyAlphaImageDiv=function(a,b,c,d,e,f,g,h,i){OpenLayers.Util.modifyDOMElement(a,b,c,d,f,null,null,i);b=a.childNodes[0];if(e)b.src=e;OpenLayers.Util.modifyDOMElement(b,a.id+"_innerImage",null,d,"relative",g);if(OpenLayers.Util.alphaHack()){if(a.style.display!="none")a.style.display="inline-block";h==null&&(h="scale");a.style.filter="progid:DXImageTransform.Microsoft.AlphaImageLoader(src='"+b.src+"', sizingMethod='"+h+"')";if(parseFloat(a.style.opacity)>=0&&parseFloat(a.style.opacity)< +1)a.style.filter=a.style.filter+(" alpha(opacity="+a.style.opacity*100+")");b.style.filter="alpha(opacity=0)"}};OpenLayers.Util.createAlphaImageDiv=function(a,b,c,d,e,f,g,h,i){var j=OpenLayers.Util.createDiv(),i=OpenLayers.Util.createImage(null,null,null,null,null,null,null,i);i.className="olAlphaImg";j.appendChild(i);OpenLayers.Util.modifyAlphaImageDiv(j,a,b,c,d,e,f,g,h);return j};OpenLayers.Util.upperCaseObject=function(a){var b={},c;for(c in a)b[c.toUpperCase()]=a[c];return b}; +OpenLayers.Util.applyDefaults=function(a,b){var a=a||{},c=typeof window.Event=="function"&&b instanceof window.Event,d;for(d in b)if(a[d]===void 0||!c&&b.hasOwnProperty&&b.hasOwnProperty(d)&&!a.hasOwnProperty(d))a[d]=b[d];if(!c&&b&&b.hasOwnProperty&&b.hasOwnProperty("toString")&&!a.hasOwnProperty("toString"))a.toString=b.toString;return a}; +OpenLayers.Util.getParameterString=function(a){var b=[],c;for(c in a){var d=a[c];if(d!=null&&typeof d!="function"){if(typeof d=="object"&&d.constructor==Array){for(var e=[],f,g=0,h=d.length;g1.0E-12&&--m>0;){var l=Math.sin(k),n=Math.cos(k),q=Math.sqrt(h*l*h*l+(g*j-i*h*n)*(g*j-i*h*n));if(q==0)return 0;var n=i*j+g*h*n,p=Math.atan2(q,n),r=Math.asin(g* +h*l/q),s=Math.cos(r)*Math.cos(r),l=n-2*i*j/s,t=c/16*s*(4+c*(4-3*s)),o=k,k=f+(1-t)*c*Math.sin(r)*(p+t*q*(l+t*n*(-1+2*l*l)))}if(m==0)return NaN;d=s*(d*d-e*e)/(e*e);c=d/1024*(256+d*(-128+d*(74-47*d)));return(e*(1+d/16384*(4096+d*(-768+d*(320-175*d))))*(p-c*q*(l+c/4*(n*(-1+2*l*l)-c/6*l*(-3+4*q*q)*(-3+4*l*l))))).toFixed(3)/1E3}; +OpenLayers.Util.destinationVincenty=function(a,b,c){for(var d=OpenLayers.Util,e=d.VincentyConstants,f=e.a,g=e.b,h=e.f,e=a.lon,a=a.lat,i=d.rad(b),b=Math.sin(i),i=Math.cos(i),a=(1-h)*Math.tan(d.rad(a)),j=1/Math.sqrt(1+a*a),k=a*j,o=Math.atan2(a,i),a=j*b,m=1-a*a,f=m*(f*f-g*g)/(g*g),l=1+f/16384*(4096+f*(-768+f*(320-175*f))),n=f/1024*(256+f*(-128+f*(74-47*f))),f=c/(g*l),q=2*Math.PI;Math.abs(f-q)>1.0E-12;)var p=Math.cos(2*o+f),r=Math.sin(f),s=Math.cos(f),t=n*r*(p+n/4*(s*(-1+2*p*p)-n/6*p*(-3+4*r*r)*(-3+4* +p*p))),q=f,f=c/(g*l)+t;c=k*r-j*s*i;g=Math.atan2(k*s+j*r*i,(1-h)*Math.sqrt(a*a+c*c));b=Math.atan2(r*b,j*s-k*r*i);i=h/16*m*(4+h*(4-3*m));p=b-(1-i)*h*a*(f+i*r*(p+i*s*(-1+2*p*p)));Math.atan2(a,-c);return new OpenLayers.LonLat(e+d.deg(p),d.deg(g))}; +OpenLayers.Util.getParameters=function(a){var a=a===null||a===void 0?window.location.href:a,b="";if(OpenLayers.String.contains(a,"?"))var b=a.indexOf("?")+1,c=OpenLayers.String.contains(a,"#")?a.indexOf("#"):a.length,b=a.substring(b,c);for(var a={},b=b.split(/[&;]/),c=0,d=b.length;c1?1/a:a};OpenLayers.Util.getResolutionFromScale=function(a,b){var c;if(a){b==null&&(b="degrees");c=1/(OpenLayers.Util.normalizeScale(a)*OpenLayers.INCHES_PER_UNIT[b]*OpenLayers.DOTS_PER_INCH)}return c}; +OpenLayers.Util.getScaleFromResolution=function(a,b){b==null&&(b="degrees");return a*OpenLayers.INCHES_PER_UNIT[b]*OpenLayers.DOTS_PER_INCH}; +OpenLayers.Util.pagePosition=function(a){var b=[0,0],c=OpenLayers.Util.getViewportElement();if(!a||a==window||a==c)return b;var d=OpenLayers.IS_GECKO&&document.getBoxObjectFor&&OpenLayers.Element.getStyle(a,"position")=="absolute"&&(a.style.top==""||a.style.left==""),e=null;if(a.getBoundingClientRect){a=a.getBoundingClientRect();e=c.scrollTop;b[0]=a.left+c.scrollLeft;b[1]=a.top+e}else if(document.getBoxObjectFor&&!d){a=document.getBoxObjectFor(a);c=document.getBoxObjectFor(c);b[0]=a.screenX-c.screenX; +b[1]=a.screenY-c.screenY}else{b[0]=a.offsetLeft;b[1]=a.offsetTop;e=a.offsetParent;if(e!=a)for(;e;){b[0]=b[0]+e.offsetLeft;b[1]=b[1]+e.offsetTop;e=e.offsetParent}c=OpenLayers.BROWSER_NAME;if(c=="opera"||c=="safari"&&OpenLayers.Element.getStyle(a,"position")=="absolute")b[1]=b[1]-document.body.offsetTop;for(e=a.offsetParent;e&&e!=document.body;){b[0]=b[0]-e.scrollLeft;if(c!="opera"||e.tagName!="TR")b[1]=b[1]-e.scrollTop;e=e.offsetParent}}return b}; +OpenLayers.Util.getViewportElement=function(){var a=arguments.callee.viewportElement;if(a==void 0){a=OpenLayers.BROWSER_NAME=="msie"&&document.compatMode!="CSS1Compat"?document.body:document.documentElement;arguments.callee.viewportElement=a}return a}; +OpenLayers.Util.isEquivalentUrl=function(a,b,c){c=c||{};OpenLayers.Util.applyDefaults(c,{ignoreCase:true,ignorePort80:true,ignoreHash:true});var a=OpenLayers.Util.createUrlObject(a,c),b=OpenLayers.Util.createUrlObject(b,c),d;for(d in a)if(d!=="args"&&a[d]!=b[d])return false;for(d in a.args){if(a.args[d]!=b.args[d])return false;delete b.args[d]}for(d in b.args)return false;return true}; +OpenLayers.Util.createUrlObject=function(a,b){b=b||{};if(!/^\w+:\/\//.test(a)){var c=window.location,d=c.port?":"+c.port:"",d=c.protocol+"//"+c.host.split(":").shift()+d;if(a.indexOf("/")===0)a=d+a;else{c=c.pathname.split("/");c.pop();a=d+c.join("/")+"/"+a}}b.ignoreCase&&(a=a.toLowerCase());c=document.createElement("a");c.href=a;d={};d.host=c.host.split(":").shift();d.protocol=c.protocol;d.port=b.ignorePort80?c.port=="80"||c.port=="0"?"":c.port:c.port==""||c.port=="0"?"80":c.port;d.hash=b.ignoreHash|| +c.hash==="#"?"":c.hash;var e=c.search;if(!e){e=a.indexOf("?");e=e!=-1?a.substr(e):""}d.args=OpenLayers.Util.getParameters(e);d.pathname=c.pathname.charAt(0)=="/"?c.pathname:"/"+c.pathname;return d};OpenLayers.Util.removeTail=function(a){var b=null,b=a.indexOf("?"),c=a.indexOf("#");return b=b==-1?c!=-1?a.substr(0,c):a:c!=-1?a.substr(0,Math.min(b,c)):a.substr(0,b)};OpenLayers.IS_GECKO=function(){var a=navigator.userAgent.toLowerCase();return a.indexOf("webkit")==-1&&a.indexOf("gecko")!=-1}(); +OpenLayers.CANVAS_SUPPORTED=function(){var a=document.createElement("canvas");return!(!a.getContext||!a.getContext("2d"))}();OpenLayers.BROWSER_NAME=function(){var a="",b=navigator.userAgent.toLowerCase();b.indexOf("opera")!=-1?a="opera":b.indexOf("msie")!=-1?a="msie":b.indexOf("safari")!=-1?a="safari":b.indexOf("mozilla")!=-1&&(a=b.indexOf("firefox")!=-1?"firefox":"mozilla");return a}();OpenLayers.Util.getBrowserName=function(){return OpenLayers.BROWSER_NAME}; +OpenLayers.Util.getRenderedDimensions=function(a,b,c){var d,e,f=document.createElement("div");f.style.visibility="hidden";for(var g=c&&c.containerElement?c.containerElement:document.body,h=false,i=null,j=g;j&&j.tagName.toLowerCase()!="body";){var k=OpenLayers.Element.getStyle(j,"position");if(k=="absolute"){h=true;break}else if(k&&k!="static")break;j=j.parentNode}if(h&&(g.clientHeight===0||g.clientWidth===0)){i=document.createElement("div");i.style.visibility="hidden";i.style.position="absolute"; +i.style.overflow="visible";i.style.width=document.body.clientWidth+"px";i.style.height=document.body.clientHeight+"px";i.appendChild(f)}f.style.position="absolute";if(b)if(b.w){d=b.w;f.style.width=d+"px"}else if(b.h){e=b.h;f.style.height=e+"px"}if(c&&c.displayClass)f.className=c.displayClass;b=document.createElement("div");b.innerHTML=a;b.style.overflow="visible";if(b.childNodes){a=0;for(c=b.childNodes.length;a=60){f=f-60;d=d+1;if(d>=60){d=d-60;e=e+1}}e<10&&(e="0"+e);e=e+"\u00b0";if(c.indexOf("dm")>=0){d<10&&(d="0"+d);e=e+(d+"'");if(c.indexOf("dms")>=0){f<10&&(f="0"+f);e=e+(f+'"')}}return e=b=="lon"?e+(a<0?OpenLayers.i18n("W"):OpenLayers.i18n("E")):e+(a<0?OpenLayers.i18n("S"):OpenLayers.i18n("N"))};OpenLayers.Event={observers:!1,KEY_SPACE:32,KEY_BACKSPACE:8,KEY_TAB:9,KEY_RETURN:13,KEY_ESC:27,KEY_LEFT:37,KEY_UP:38,KEY_RIGHT:39,KEY_DOWN:40,KEY_DELETE:46,element:function(a){return a.target||a.srcElement},isSingleTouch:function(a){return a.touches&&1==a.touches.length},isMultiTouch:function(a){return a.touches&&1=f&&0<=o&&1>=o)&&(d?(h=a.x1+f*h,o=a.y1+f*i,e=new OpenLayers.Geometry.Point(h,o)):e=!0));if(c)if(e){if(d){a=[a,b];b=0;a:for(;2>b;++b){f=a[b];for(i=1;3>i;++i)if(h=f["x"+i],o=f["y"+i],d=Math.sqrt(Math.pow(h-e.x,2)+Math.pow(o-e.y,2)),db;++b){h=a[b];o=a[(b+1)%2];for(i=1;3>i;++i)if(f={x:h["x"+i],y:h["y"+i]},g=OpenLayers.Geometry.distanceToSegment(f,o),g.distance=k||(1<=k?(e=g,f=h):(e+=k*i,f+=k*j));return{distance:Math.sqrt(Math.pow(e-c,2)+Math.pow(f-d,2)),x:e,y:f}};OpenLayers.Geometry.Collection=OpenLayers.Class(OpenLayers.Geometry,{components:null,componentTypes:null,initialize:function(a){OpenLayers.Geometry.prototype.initialize.apply(this,arguments);this.components=[];null!=a&&this.addComponents(a)},destroy:function(){this.components.length=0;this.components=null;OpenLayers.Geometry.prototype.destroy.apply(this,arguments)},clone:function(){for(var a=eval("new "+this.CLASS_NAME+"()"),b=0,c=this.components.length;bf)break;if(!(i.x2Math.max(g,h))&&!(Math.max(j,k)h&&(i>j.y1&&ij.y2))break;e=c?{distance:e.distance,x0:e.x,y0:e.y,x1:h,y1:i}:e.distance}else if(a instanceof OpenLayers.Geometry.LineString){var g=this.getSortedSegments(),h=a.getSortedSegments(),m,l,n=h.length,q={point:!0},k=0,o=g.length;a:for(;kb.length)return this;var c=function(a,b,d,i){for(var j=0,k=0,o=b,m;oj&&(j=m,k=o)}j>i&&k!=b&&(e.push(k),c(a,b,k,i),c(a,k,d,i))},d=b.length-1,e=[];e.push(0);for(e.push(d);b[0].equals(b[d]);)d--,e.push(d);c(b,0,d,a);a=[];e.sort(function(a,b){return a-b});for(d=0;d=g&&c<=h||g>=h&&c<=g&&c>=h)){j=-1;break}}else{i=b((a-f)*((h-g)/(f-e))+h,14);if(i==c&&(e=e&&a<=f||e>f&&a<=e&&a>=f)){j=-1;break}i<=c||g!=h&&(iMath.max(g,h))||(e=e&&af&&a=f)&&++j}return-1==j?1:!!(j&1)},intersects:function(a){var b=!1;if("OpenLayers.Geometry.Point"== +a.CLASS_NAME)b=this.containsPoint(a);else if("OpenLayers.Geometry.LineString"==a.CLASS_NAME)b=a.intersects(this);else if("OpenLayers.Geometry.LinearRing"==a.CLASS_NAME)b=OpenLayers.Geometry.LineString.prototype.intersects.apply(this,[a]);else for(var c=0,d=a.components.length;cthis.duration&&this.stop()},CLASS_NAME:"OpenLayers.Tween"});OpenLayers.Easing={CLASS_NAME:"OpenLayers.Easing"};OpenLayers.Easing.Linear={easeIn:function(a,b,c,d){return c*a/d+b},easeOut:function(a,b,c,d){return c*a/d+b},easeInOut:function(a,b,c,d){return c*a/d+b},CLASS_NAME:"OpenLayers.Easing.Linear"}; +OpenLayers.Easing.Expo={easeIn:function(a,b,c,d){return 0==a?b:c*Math.pow(2,10*(a/d-1))+b},easeOut:function(a,b,c,d){return a==d?b+c:c*(-Math.pow(2,-10*a/d)+1)+b},easeInOut:function(a,b,c,d){return 0==a?b:a==d?b+c:1>(a/=d/2)?c/2*Math.pow(2,10*(a-1))+b:c/2*(-Math.pow(2,-10*--a)+2)+b},CLASS_NAME:"OpenLayers.Easing.Expo"}; +OpenLayers.Easing.Quad={easeIn:function(a,b,c,d){return c*(a/=d)*a+b},easeOut:function(a,b,c,d){return-c*(a/=d)*(a-2)+b},easeInOut:function(a,b,c,d){return 1>(a/=d/2)?c/2*a*a+b:-c/2*(--a*(a-2)-1)+b},CLASS_NAME:"OpenLayers.Easing.Quad"};OpenLayers.Projection=OpenLayers.Class({proj:null,projCode:null,titleRegEx:/\+title=[^\+]*/,initialize:function(a,b){OpenLayers.Util.extend(this,b);this.projCode=a;window.Proj4js&&(this.proj=new Proj4js.Proj(a))},getCode:function(){return this.proj?this.proj.srsCode:this.projCode},getUnits:function(){return this.proj?this.proj.units:null},toString:function(){return this.getCode()},equals:function(a){var b=!1;a&&(a instanceof OpenLayers.Projection||(a=new OpenLayers.Projection(a)),window.Proj4js&& +this.proj.defData&&a.proj.defData?b=this.proj.defData.replace(this.titleRegEx,"")==a.proj.defData.replace(this.titleRegEx,""):a.getCode&&(b=this.getCode(),a=a.getCode(),b=b==a||!!OpenLayers.Projection.transforms[b]&&OpenLayers.Projection.transforms[b][a]===OpenLayers.Projection.nullTransform));return b},destroy:function(){delete this.proj;delete this.projCode},CLASS_NAME:"OpenLayers.Projection"});OpenLayers.Projection.transforms={}; +OpenLayers.Projection.defaults={"EPSG:4326":{units:"degrees",maxExtent:[-180,-90,180,90],yx:!0},"CRS:84":{units:"degrees",maxExtent:[-180,-90,180,90]},"EPSG:900913":{units:"m",maxExtent:[-2.003750834E7,-2.003750834E7,2.003750834E7,2.003750834E7]}}; +OpenLayers.Projection.addTransform=function(a,b,c){if(c===OpenLayers.Projection.nullTransform){var d=OpenLayers.Projection.defaults[a];d&&!OpenLayers.Projection.defaults[b]&&(OpenLayers.Projection.defaults[b]=d)}OpenLayers.Projection.transforms[a]||(OpenLayers.Projection.transforms[a]={});OpenLayers.Projection.transforms[a][b]=c}; +OpenLayers.Projection.transform=function(a,b,c){if(b&&c)if(b instanceof OpenLayers.Projection||(b=new OpenLayers.Projection(b)),c instanceof OpenLayers.Projection||(c=new OpenLayers.Projection(c)),b.proj&&c.proj)a=Proj4js.transform(b.proj,c.proj,a);else{var b=b.getCode(),c=c.getCode(),d=OpenLayers.Projection.transforms;if(d[b]&&d[b][c])d[b][c](a)}return a};OpenLayers.Projection.nullTransform=function(a){return a}; +(function(){function a(a){a.x=180*a.x/d;a.y=180/Math.PI*(2*Math.atan(Math.exp(a.y/d*Math.PI))-Math.PI/2);return a}function b(a){a.x=a.x*d/180;a.y=Math.log(Math.tan((90+a.y)*Math.PI/360))/Math.PI*d;return a}function c(c,d){var e=OpenLayers.Projection.addTransform,f=OpenLayers.Projection.nullTransform,g,m,l,n,q;g=0;for(m=d.length;gparseFloat(navigator.appVersion.split("MSIE")[1])?this.events.register("resize",this,this.updateSize):(this.updateSizeDestroy=OpenLayers.Function.bind(this.updateSize,this),OpenLayers.Event.observe(window,"resize",this.updateSizeDestroy));if(this.theme){for(var c=!0,d=document.getElementsByTagName("link"), +e=0,f=d.length;eb?b=0:b>this.layers.length&&(b=this.layers.length);if(c!=b){this.layers.splice(c,1);this.layers.splice(b,0,a);for(var c=0,d=this.layers.length;c=this.minPx.x+h?Math.round(a):0;b=f<=this.maxPx.y-i&&f>=this.minPx.y+i?Math.round(b):0;if(a||b){this.dragging||(this.dragging= +!0,this.events.triggerEvent("movestart"));this.center=null;a&&(this.layerContainerDiv.style.left=parseInt(this.layerContainerDiv.style.left)-a+"px",this.minPx.x-=a,this.maxPx.x-=a);b&&(this.layerContainerDiv.style.top=parseInt(this.layerContainerDiv.style.top)-b+"px",this.minPx.y-=b,this.maxPx.y-=b);d=0;for(e=this.layers.length;dc)for(var d=a|0,e=b.length;dthis.restrictedExtent.getWidth()?a=new OpenLayers.LonLat(g.lon,a.lat):f.leftthis.restrictedExtent.right&& +(a=a.add(this.restrictedExtent.right-f.right,0));f.getHeight()>this.restrictedExtent.getHeight()?a=new OpenLayers.LonLat(a.lon,g.lat):f.bottomthis.restrictedExtent.top&&(a=a.add(0,this.restrictedExtent.top-f.top))}}e=e||this.isValidZoomLevel(b)&&b!=this.getZoom();f=this.isValidLonLat(a)&&!a.equals(this.center);if(e||f||d){d||this.events.triggerEvent("movestart");f&&(!e&&this.center&&this.centerLayerContainer(a),this.center= +a.clone());a=e?this.getResolutionForZoom(b):this.getResolution();if(e||null==this.layerContainerOrigin){this.layerContainerOrigin=this.getCachedCenter();this.layerContainerDiv.style.left="0px";this.layerContainerDiv.style.top="0px";var f=this.getMaxExtent({restricted:!0}),h=f.getCenterLonLat(),g=this.center.lon-h.lon,h=h.lat-this.center.lat,i=Math.round(f.getWidth()/a),j=Math.round(f.getHeight()/a);this.minPx={x:(this.size.w-i)/2-g/a,y:(this.size.h-j)/2-h/a};this.maxPx={x:this.minPx.x+Math.round(f.getWidth()/ +a),y:this.minPx.y+Math.round(f.getHeight()/a)}}e&&(this.zoom=b,this.resolution=a);a=this.getExtent();this.baseLayer.visibility&&(this.baseLayer.moveTo(a,e,c.dragging),c.dragging||this.baseLayer.events.triggerEvent("moveend",{zoomChanged:e}));a=this.baseLayer.getExtent();for(b=this.layers.length-1;0<=b;--b)if(f=this.layers[b],f!==this.baseLayer&&!f.isBaseLayer&&(g=f.calculateInRange(),f.inRange!=g&&((f.inRange=g)||f.display(!1),this.events.triggerEvent("changelayer",{layer:f,property:"visibility"})), +g&&f.visibility))f.moveTo(a,e,c.dragging),c.dragging||f.events.triggerEvent("moveend",{zoomChanged:e});this.events.triggerEvent("move");d||this.events.triggerEvent("moveend");if(e){b=0;for(c=this.popups.length;b=this.minResolution&&a<=this.maxResolution);return a},setIsBaseLayer:function(a){a!=this.isBaseLayer&&(this.isBaseLayer=a,null!=this.map&&this.map.events.triggerEvent("changebaselayer",{layer:this}))},initResolutions:function(){var a, +b,c,d={},e=!0;a=0;for(b=this.RESOLUTION_PROPERTIES.length;a=a||"number"!==typeof d&&"number"!==typeof c)){b=Array(a);var e=2;"number"==typeof c&&"number"==typeof d&&(e=Math.pow(d/c,1/(a-1)));var f;if("number"===typeof d)for(f=0;f=a&&(f=h,e=c),h<=a){g=h;break}c=f-g;c=0f)break;f=e}else if(this.resolutions[c]this.layer.opacity&&(a.filter="alpha(opacity="+ +100*this.layer.opacity+")");a.position="absolute";this.layerAlphaHack&&(a.paddingTop=a.height,a.height="0",a.width="100%");this.frame&&this.frame.appendChild(this.imgDiv)}return this.imgDiv},initImage:function(){this.events.triggerEvent(this._loadEvent);var a=this.getImage();if(this.url&&a.getAttribute("src")==this.url)this.onImageLoad();else{var b=OpenLayers.Function.bind(function(){OpenLayers.Event.stopObservingElement(a);OpenLayers.Event.observe(a,"load",OpenLayers.Function.bind(this.onImageLoad, +this));OpenLayers.Event.observe(a,"error",OpenLayers.Function.bind(this.onImageError,this));this.imageReloadAttempts=0;this.setImgSrc(this.url)},this);a.getAttribute("src")==this.blankImageUrl?b():(OpenLayers.Event.observe(a,"load",b),OpenLayers.Event.observe(a,"error",b),this.crossOriginKeyword&&a.removeAttribute("crossorigin"),a.src=this.blankImageUrl)}},setImgSrc:function(a){var b=this.imgDiv;b.style.visibility="hidden";b.style.opacity=0;a&&(this.crossOriginKeyword&&("data:"!==a.substr(0,5)?b.setAttribute("crossorigin", +this.crossOriginKeyword):b.removeAttribute("crossorigin")),b.src=a)},getTile:function(){return this.frame?this.frame:this.getImage()},createBackBuffer:function(){if(this.imgDiv&&!this.isLoading){var a;this.frame?(a=this.frame.cloneNode(!1),a.appendChild(this.imgDiv)):a=this.imgDiv;this.imgDiv=null;return a}},onImageLoad:function(){var a=this.imgDiv;OpenLayers.Event.stopObservingElement(a);a.style.visibility="inherit";a.style.opacity=this.layer.opacity;this.isLoading=!1;this.canvasContext=null;this.events.triggerEvent("loadend"); +if(7>parseFloat(navigator.appVersion.split("MSIE")[1])&&this.layer&&this.layer.div){var b=document.createElement("span");b.style.display="none";var c=this.layer.div;c.appendChild(b);window.setTimeout(function(){b.parentNode===c&&b.parentNode.removeChild(b)},0)}!0===this.layerAlphaHack&&(a.style.filter="progid:DXImageTransform.Microsoft.AlphaImageLoader(src='"+a.src+"', sizingMethod='scale')")},onImageError:function(){var a=this.imgDiv;null!=a.src&&(this.imageReloadAttempts++,this.imageReloadAttempts<= +OpenLayers.IMAGE_RELOAD_ATTEMPTS?this.setImgSrc(this.layer.getURL(this.bounds)):(OpenLayers.Element.addClass(a,"olImageLoadError"),this.events.triggerEvent("loaderror"),this.onImageLoad()))},getCanvasContext:function(){if(OpenLayers.CANVAS_SUPPORTED&&this.imgDiv&&!this.isLoading){if(!this.canvasContext){var a=document.createElement("canvas");a.width=this.size.w;a.height=this.size.h;this.canvasContext=a.getContext("2d");this.canvasContext.drawImage(this.imgDiv,0,0)}return this.canvasContext}},CLASS_NAME:"OpenLayers.Tile.Image"});OpenLayers.Layer.Grid=OpenLayers.Class(OpenLayers.Layer.HTTPRequest,{tileSize:null,tileOriginCorner:"bl",tileOrigin:null,tileOptions:null,tileClass:OpenLayers.Tile.Image,grid:null,singleTile:!1,ratio:1.5,buffer:0,transitionEffect:null,numLoadingTiles:0,tileLoadingDelay:85,serverResolutions:null,moveTimerId:null,deferMoveGriddedTiles:null,tileQueueId:null,tileQueue:null,loading:!1,backBuffer:null,gridResolution:null,backBufferResolution:null,backBufferLonLat:null,backBufferTimerId:null,removeBackBufferDelay:null, +className:null,initialize:function(a,b,c,d){OpenLayers.Layer.HTTPRequest.prototype.initialize.apply(this,arguments);this.grid=[];this.tileQueue=[];null===this.removeBackBufferDelay&&(this.removeBackBufferDelay=this.singleTile?0:2500);null===this.className&&(this.className=this.singleTile?"olLayerGridSingleTile":"olLayerGrid");OpenLayers.Animation.isNative||(this.deferMoveGriddedTiles=OpenLayers.Function.bind(function(){this.moveGriddedTiles(true);this.moveTimerId=null},this))},setMap:function(a){OpenLayers.Layer.HTTPRequest.prototype.setMap.call(this, +a);OpenLayers.Element.addClass(this.div,this.className)},removeMap:function(){null!==this.moveTimerId&&(window.clearTimeout(this.moveTimerId),this.moveTimerId=null);this.clearTileQueue();null!==this.backBufferTimerId&&(window.clearTimeout(this.backBufferTimerId),this.backBufferTimerId=null)},destroy:function(){this.removeBackBuffer();this.clearGrid();this.tileSize=this.grid=null;OpenLayers.Layer.HTTPRequest.prototype.destroy.apply(this,arguments)},clearGrid:function(){this.clearTileQueue();if(this.grid){for(var a= +0,b=this.grid.length;aa){a=c;break}if(-1===b)throw"no appropriate resolution in serverResolutions";}return a},getServerZoom:function(){var a=this.getServerResolution();return this.serverResolutions?OpenLayers.Util.indexOf(this.serverResolutions,a):this.map.getZoomForResolution(a)+(this.zoomOffset||0)},transformDiv:function(a){this.div.style.width=100*a+"%";this.div.style.height=100*a+"%";var b=this.map.getSize(),c=parseInt(this.map.layerContainerDiv.style.left,10),d=(parseInt(this.map.layerContainerDiv.style.top, +10)-b.h/2)*(a-1);this.div.style.left=(c-b.w/2)*(a-1)+"%";this.div.style.top=d+"%"},getResolutionScale:function(){return parseInt(this.div.style.width,10)/100},applyBackBuffer:function(a){null!==this.backBufferTimerId&&this.removeBackBuffer();var b=this.backBuffer;if(!b){b=this.createBackBuffer();if(!b)return;this.div.insertBefore(b,this.div.firstChild);this.backBuffer=b;var c=this.grid[0][0].bounds;this.backBufferLonLat={lon:c.left,lat:c.top};this.backBufferResolution=this.gridResolution}var c=b.style, +d=this.backBufferResolution/a;c.width=100*d+"%";c.height=100*d+"%";a=this.getViewPortPxFromLonLat(this.backBufferLonLat,a);c=parseInt(this.map.layerContainerDiv.style.left,10);d=parseInt(this.map.layerContainerDiv.style.top,10);b.style.left=Math.round(a.x-c)+"%";b.style.top=Math.round(a.y-d)+"%"},createBackBuffer:function(){var a;if(0=a.bottom-j*this.buffer||m-e*(a-1))this.shiftColumn(!0);else if(c<-e*a)this.shiftColumn(!1);else if(d>-f*(a-1))this.shiftRow(!0);else if(d<-f*a)this.shiftRow(!1);else break}},shiftRow:function(a){for(var b=this.grid,c=b[a?0:this.grid.length-1],d=this.getServerResolution(),e=a?-this.tileSize.h:this.tileSize.h,d=d*-e,f=a? +b.pop():b.shift(),g=0,h=c.length;ga;){var e=this.grid.pop();c=0;for(d=e.length;cb;)e=this.grid[c],f=e.pop(),this.destroyTile(f)},onMapResize:function(){this.singleTile&&(this.clearGrid(),this.setTileSize())},getTileBounds:function(a){var b=this.maxExtent,c=this.getResolution(),d=c*this.tileSize.w,c=c*this.tileSize.h,e=this.getLonLatFromViewPortPx(a),a=b.left+d*Math.floor((e.lon-b.left)/d),b=b.bottom+ +c*Math.floor((e.lat-b.bottom)/c);return new OpenLayers.Bounds(a,b,a+d,b+c)},CLASS_NAME:"OpenLayers.Layer.Grid"});OpenLayers.Layer.XYZ=OpenLayers.Class(OpenLayers.Layer.Grid,{isBaseLayer:!0,sphericalMercator:!1,zoomOffset:0,serverResolutions:null,initialize:function(a,b,c){if(c&&c.sphericalMercator||this.sphericalMercator)c=OpenLayers.Util.extend({projection:"EPSG:900913",numZoomLevels:19},c);OpenLayers.Layer.Grid.prototype.initialize.apply(this,[a||this.name,b||this.url,{},c])},clone:function(a){null==a&&(a=new OpenLayers.Layer.XYZ(this.name,this.url,this.getOptions()));return a=OpenLayers.Layer.Grid.prototype.clone.apply(this, +[a])},getURL:function(a){var a=this.getXYZ(a),b=this.url;OpenLayers.Util.isArray(b)&&(b=this.selectUrl(""+a.x+a.y+a.z,b));return OpenLayers.String.format(b,a)},getXYZ:function(a){var b=this.getServerResolution(),c=Math.round((a.left-this.maxExtent.left)/(b*this.tileSize.w)),a=Math.round((this.maxExtent.top-a.top)/(b*this.tileSize.h)),b=this.getServerZoom();if(this.wrapDateLine)var d=Math.pow(2,b),c=(c%d+d)%d;return{x:c,y:a,z:b}},setMap:function(a){OpenLayers.Layer.Grid.prototype.setMap.apply(this, +arguments);this.tileOrigin||(this.tileOrigin=new OpenLayers.LonLat(this.maxExtent.left,this.maxExtent.bottom))},CLASS_NAME:"OpenLayers.Layer.XYZ"});OpenLayers.Layer.OSM=OpenLayers.Class(OpenLayers.Layer.XYZ,{name:"OpenStreetMap",url:["http://a.tile.openstreetmap.org/${z}/${x}/${y}.png","http://b.tile.openstreetmap.org/${z}/${x}/${y}.png","http://c.tile.openstreetmap.org/${z}/${x}/${y}.png"],attribution:"Data CC-By-SA by OpenStreetMap",sphericalMercator:!0,wrapDateLine:!0,tileOptions:null,initialize:function(a,b,c){OpenLayers.Layer.XYZ.prototype.initialize.apply(this,arguments);this.tileOptions=OpenLayers.Util.extend({crossOriginKeyword:"anonymous"}, +this.options&&this.options.tileOptions)},clone:function(a){null==a&&(a=new OpenLayers.Layer.OSM(this.name,this.url,this.getOptions()));return a=OpenLayers.Layer.XYZ.prototype.clone.apply(this,[a])},CLASS_NAME:"OpenLayers.Layer.OSM"});OpenLayers.Format=OpenLayers.Class({options:null,externalProjection:null,internalProjection:null,data:null,keepData:!1,initialize:function(a){OpenLayers.Util.extend(this,a);this.options=a},destroy:function(){},read:function(){throw Error("Read not implemented.");},write:function(){throw Error("Write not implemented.");},CLASS_NAME:"OpenLayers.Format"});OpenLayers.Format.XML=OpenLayers.Class(OpenLayers.Format,{namespaces:null,namespaceAlias:null,defaultPrefix:null,readers:{},writers:{},xmldom:null,initialize:function(a){window.ActiveXObject&&(this.xmldom=new ActiveXObject("Microsoft.XMLDOM"));OpenLayers.Format.prototype.initialize.apply(this,[a]);this.namespaces=OpenLayers.Util.extend({},this.namespaces);this.namespaceAlias={};for(var b in this.namespaces)this.namespaceAlias[this.namespaces[b]]=b},destroy:function(){this.xmldom=null;OpenLayers.Format.prototype.destroy.apply(this, +arguments)},setNamespace:function(a,b){this.namespaces[a]=b;this.namespaceAlias[b]=a},read:function(a){var b=a.indexOf("<");0window.opera.version()&&(b=-b)):a.detail&&(b=-a.detail/ +3),this.delta+=b,this.interval)?(window.clearTimeout(this._timeoutId),this._timeoutId=window.setTimeout(OpenLayers.Function.bind(function(){this.wheelZoom(a)},this),this.interval)):this.wheelZoom(a)),OpenLayers.Event.stop(a))}},wheelZoom:function(a){var b=this.delta;this.delta=0;b&&(this.mousePosition&&(a.xy=this.mousePosition),a.xy||(a.xy=this.map.getPixelFromLonLat(this.map.getCenter())),0>b?this.callback("down",[a,this.cumulative?b:-1]):this.callback("up",[a,this.cumulative?b:1]))},mousemove:function(a){this.mousePosition= +a.xy},activate:function(a){if(OpenLayers.Handler.prototype.activate.apply(this,arguments)){var b=this.wheelListener;OpenLayers.Event.observe(window,"DOMMouseScroll",b);OpenLayers.Event.observe(window,"mousewheel",b);OpenLayers.Event.observe(document,"mousewheel",b);return!0}return!1},deactivate:function(a){if(OpenLayers.Handler.prototype.deactivate.apply(this,arguments)){var b=this.wheelListener;OpenLayers.Event.stopObserving(window,"DOMMouseScroll",b);OpenLayers.Event.stopObserving(window,"mousewheel", +b);OpenLayers.Event.stopObserving(document,"mousewheel",b);return!0}return!1},CLASS_NAME:"OpenLayers.Handler.MouseWheel"});OpenLayers.Geometry.MultiLineString=OpenLayers.Class(OpenLayers.Geometry.Collection,{componentTypes:["OpenLayers.Geometry.LineString"],split:function(a,b){for(var c=null,d=b&&b.mutual,e,f,g,h,i=[],j=[a],k=0,o=this.components.length;kthis.maxZIndex&&(this.maxZIndex=b)},getNextElement:function(a){a+=1;if(aa.left&&f.righta.left&&f.leftthis.opacity&&a.setOpacity(this.opacity);this.map&&this.map.getExtent()&&(a.map=this.map,this.drawMarker(a))},removeMarker:function(a){this.markers&&this.markers.length&&(OpenLayers.Util.removeItem(this.markers,a),a.erase())},clearMarkers:function(){if(null!=this.markers)for(;0=this.resFactor||a<=1/this.resFactor);return a},calculateBounds:function(a){a||(a=this.getMapBounds());var b=a.getCenterLonLat(),c=a.getWidth()*this.ratio,a=a.getHeight()*this.ratio;this.bounds=new OpenLayers.Bounds(b.lon-c/2,b.lat-a/2,b.lon+c/2,b.lat+a/2)},triggerRead:function(a){this.response&&!(a&&!0===a.noAbort)&& +(this.layer.protocol.abort(this.response),this.layer.events.triggerEvent("loadend"));this.layer.events.triggerEvent("loadstart");this.response=this.layer.protocol.read(OpenLayers.Util.applyDefaults({filter:this.createFilter(),callback:this.merge,scope:this},a))},createFilter:function(){var a=new OpenLayers.Filter.Spatial({type:OpenLayers.Filter.Spatial.BBOX,value:this.bounds,projection:this.layer.projection});this.layer.filter&&(a=new OpenLayers.Filter.Logical({type:OpenLayers.Filter.Logical.AND, +filters:[this.layer.filter,a]}));return a},merge:function(a){this.layer.destroyFeatures();if((a=a.features)&&0=this.down.xy.distanceTo(a.xy))&&this.touch&&this.down.touches.length===this.last.touches.length)for(var a=0,c=this.down.touches.length;athis.pixelTolerance){b= +!1;break}return b},getTouchDistance:function(a,b){return Math.sqrt(Math.pow(a.clientX-b.clientX,2)+Math.pow(a.clientY-b.clientY,2))},passesDblclickTolerance:function(){var a=!0;this.down&&this.first&&(a=this.down.xy.distanceTo(this.first.xy)<=this.dblclickTolerance);return a},clearTimer:function(){null!=this.timerId&&(window.clearTimeout(this.timerId),this.timerId=null);null!=this.rightclickTimerId&&(window.clearTimeout(this.rightclickTimerId),this.rightclickTimerId=null)},delayedCall:function(a){this.timerId= +null;a&&this.callback("click",[a])},getEventInfo:function(a){var b;if(a.touches){var c=a.touches.length;b=Array(c);for(var d,e=0;e=-this.MAX_PIXEL&&a<=this.MAX_PIXEL&&b>=-this.MAX_PIXEL&&b<=this.MAX_PIXEL},setExtent:function(a,b){var c=OpenLayers.Renderer.Elements.prototype.setExtent.apply(this,arguments),d=this.getResolution(),e=-a.left/d,d=a.top/d;if(b)return this.left=e,this.top=d,this.rendererRoot.setAttributeNS(null, +"viewBox","0 0 "+this.size.w+" "+this.size.h),this.translate(this.xOffset,0),!0;(e=this.translate(e-this.left+this.xOffset,d-this.top))||this.setExtent(a,!0);return c&&e},translate:function(a,b){if(this.inValidRange(a,b,!0)){var c="";if(a||b)c="translate("+a+","+b+")";this.root.setAttributeNS(null,"transform",c);this.translationParameters={x:a,y:b};return!0}return!1},setSize:function(a){OpenLayers.Renderer.prototype.setSize.apply(this,arguments);this.rendererRoot.setAttributeNS(null,"width",this.size.w); +this.rendererRoot.setAttributeNS(null,"height",this.size.h)},getNodeType:function(a,b){var c=null;switch(a.CLASS_NAME){case "OpenLayers.Geometry.Point":c=b.externalGraphic?"image":this.isComplexSymbol(b.graphicName)?"svg":"circle";break;case "OpenLayers.Geometry.Rectangle":c="rect";break;case "OpenLayers.Geometry.LineString":c="polyline";break;case "OpenLayers.Geometry.LinearRing":c="polygon";break;case "OpenLayers.Geometry.Polygon":case "OpenLayers.Geometry.Curve":c="path"}return c},setStyle:function(a, +b,c){var b=b||a._style,c=c||a._options,d=parseFloat(a.getAttributeNS(null,"r")),e=1,f;if("OpenLayers.Geometry.Point"==a._geometryClass&&d){a.style.visibility="";if(!1===b.graphic)a.style.visibility="hidden";else if(b.externalGraphic){f=this.getPosition(a);b.graphicTitle&&(a.setAttributeNS(null,"title",b.graphicTitle),d=a.getElementsByTagName("title"),0i;)f.removeChild(f.lastChild);for(var j=0;jd)i=(c-g)/(h-f),h=0>h?-d:d,c=g+(h-f)*i;if(c<-e||c>e)i=(h-f)/(c-g),c=0>c?-e:e,h=f+(c-g)*i;return h+","+c},getShortString:function(a){var b=this.getResolution(),c=(a.x-this.featureDx)/b+this.left,a=this.top-a.y/b;return this.inValidRange(c,a)?c+","+a:!1},getPosition:function(a){return{x:parseFloat(a.getAttributeNS(null, +"cx")),y:parseFloat(a.getAttributeNS(null,"cy"))}},importSymbol:function(a){this.defs||(this.defs=this.createDefs());var b=this.container.id+"-"+a,c=document.getElementById(b);if(null!=c)return c;var d=OpenLayers.Renderer.symbol[a];if(!d)throw Error(a+" is not a valid symbol name");var a=this.nodeFactory(b,"symbol"),e=this.nodeFactory(null,"polygon");a.appendChild(e);for(var c=new OpenLayers.Bounds(Number.MAX_VALUE,Number.MAX_VALUE,0,0),f=[],g,h,i=0;i"+this.contentDiv.innerHTML+"",b=this.map?this.map.div:document.body,c=OpenLayers.Util.getRenderedDimensions(a, +null,{displayClass:this.displayClass,containerElement:b}),d=this.getSafeContentSize(c),e=null;d.equals(c)?e=c:(c={w:d.wa.w-this.map.paddingForPopups.right&&(c.x=a.w-this.map.paddingForPopups.right-this.size.w); +b.ya.h-this.map.paddingForPopups.bottom&&(c.y=a.h-this.map.paddingForPopups.bottom-this.size.h);this.map.pan(b.x-c.x,b.y-c.y)},registerEvents:function(){this.events=new OpenLayers.Events(this,this.div,null,!0);this.events.on({mousedown:this.onmousedown,mousemove:this.onmousemove,mouseup:this.onmouseup,click:this.onclick,mouseout:this.onmouseout,dblclick:this.ondblclick,touchstart:function(a){OpenLayers.Event.stop(a,!0)}, +scope:this})},onmousedown:function(a){this.mousedown=!0;OpenLayers.Event.stop(a,!0)},onmousemove:function(a){this.mousedown&&OpenLayers.Event.stop(a,!0)},onmouseup:function(a){this.mousedown&&(this.mousedown=!1,OpenLayers.Event.stop(a,!0))},onclick:function(a){OpenLayers.Event.stop(a,!0)},onmouseout:function(){this.mousedown=!1},ondblclick:function(a){OpenLayers.Event.stop(a,!0)},CLASS_NAME:"OpenLayers.Popup"});OpenLayers.Popup.WIDTH=200;OpenLayers.Popup.HEIGHT=200;OpenLayers.Popup.COLOR="white"; +OpenLayers.Popup.OPACITY=1;OpenLayers.Popup.BORDER="0px";OpenLayers.Popup.Anchored=OpenLayers.Class(OpenLayers.Popup,{relativePosition:null,keepInMap:!0,anchor:null,initialize:function(a,b,c,d,e,f,g){OpenLayers.Popup.prototype.initialize.apply(this,[a,b,c,d,f,g]);this.anchor=null!=e?e:{size:new OpenLayers.Size(0,0),offset:new OpenLayers.Pixel(0,0)}},destroy:function(){this.relativePosition=this.anchor=null;OpenLayers.Popup.prototype.destroy.apply(this,arguments)},show:function(){this.updatePosition();OpenLayers.Popup.prototype.show.apply(this,arguments)}, +moveTo:function(a){var b=this.relativePosition;this.relativePosition=this.calculateRelativePosition(a);a=this.calculateNewPx(a);OpenLayers.Popup.prototype.moveTo.apply(this,Array(a));this.relativePosition!=b&&this.updateRelativePosition()},setSize:function(a){OpenLayers.Popup.prototype.setSize.apply(this,arguments);this.lonlat&&this.map&&this.moveTo(this.map.getLayerPxFromLonLat(this.lonlat))},calculateRelativePosition:function(a){a=this.map.getLonLatFromLayerPx(a);a=this.map.getExtent().determineQuadrant(a); +return OpenLayers.Bounds.oppositeQuadrant(a)},updateRelativePosition:function(){},calculateNewPx:function(a){var a=a.offset(this.anchor.offset),b=this.size||this.contentSize,c="t"==this.relativePosition.charAt(0);a.y+=c?-b.h:this.anchor.size.h;c="l"==this.relativePosition.charAt(1);a.x+=c?-b.w:this.anchor.size.w;return a},CLASS_NAME:"OpenLayers.Popup.Anchored"});OpenLayers.Popup.Framed=OpenLayers.Class(OpenLayers.Popup.Anchored,{imageSrc:null,imageSize:null,isAlphaImage:!1,positionBlocks:null,blocks:null,fixedRelativePosition:!1,initialize:function(a,b,c,d,e,f,g){OpenLayers.Popup.Anchored.prototype.initialize.apply(this,arguments);this.fixedRelativePosition&&(this.updateRelativePosition(),this.calculateRelativePosition=function(){return this.relativePosition});this.contentDiv.style.position="absolute";this.contentDiv.style.zIndex=1;f&&(this.closeDiv.style.zIndex= +1);this.groupDiv.style.position="absolute";this.groupDiv.style.top="0px";this.groupDiv.style.left="0px";this.groupDiv.style.height="100%";this.groupDiv.style.width="100%"},destroy:function(){this.isAlphaImage=this.imageSize=this.imageSrc=null;this.fixedRelativePosition=!1;this.positionBlocks=null;for(var a=0;ai?0:i)+"px";d.div.style.height=(0>j?0:j)+"px";d.div.style.left=null!=e?e+"px":"";d.div.style.bottom=null!=f?f+"px":"";d.div.style.right=null!=g?g+"px":"";d.div.style.top=null!=h?h+"px":"";d.image.style.left=c.position.x+"px";d.image.style.top=c.position.y+"px"}this.contentDiv.style.left=this.padding.left+"px";this.contentDiv.style.top=this.padding.top+"px"}},CLASS_NAME:"OpenLayers.Popup.Framed"});OpenLayers.ProxyHost=""; +OpenLayers.Request={DEFAULT_CONFIG:{method:"GET",url:window.location.href,async:!0,user:void 0,password:void 0,params:null,proxy:OpenLayers.ProxyHost,headers:{},data:null,callback:function(){},success:null,failure:null,scope:null},URL_SPLIT_REGEX:/([^:]*:)\/\/([^:]*:?[^@]*@)?([^:\/\?]*):?([^\/\?]*)/,events:new OpenLayers.Events(this),makeSameOrigin:function(a,b){var c=0!==a.indexOf("http"),d=!c&&a.match(this.URL_SPLIT_REGEX);if(d){var e=window.location,c=d[1]==e.protocol&&d[3]==e.hostname,d=d[4], +e=e.port;if(80!=d&&""!=d||"80"!=e&&""!=e)c=c&&d==e}c||(b?a="function"==typeof b?b(a):b+encodeURIComponent(a):OpenLayers.Console.warn(OpenLayers.i18n("proxyNeeded"),{url:a}));return a},issue:function(a){var b=OpenLayers.Util.extend(this.DEFAULT_CONFIG,{proxy:OpenLayers.ProxyHost}),a=OpenLayers.Util.applyDefaults(a,b),b=!1,c;for(c in a.headers)a.headers.hasOwnProperty(c)&&"x-requested-with"===c.toLowerCase()&&(b=!0);!1===b&&(a.headers["X-Requested-With"]="XMLHttpRequest");var d=new OpenLayers.Request.XMLHttpRequest, +e=OpenLayers.Util.urlAppend(a.url,OpenLayers.Util.getParameterString(a.params||{})),e=OpenLayers.Request.makeSameOrigin(e,a.proxy);d.open(a.method,e,a.async,a.user,a.password);for(var f in a.headers)d.setRequestHeader(f,a.headers[f]);var g=this.events,h=this;d.onreadystatechange=function(){d.readyState==OpenLayers.Request.XMLHttpRequest.DONE&&!1!==g.triggerEvent("complete",{request:d,config:a,requestUrl:e})&&h.runCallbacks({request:d,config:a,requestUrl:e})};!1===a.async?d.send(a.data):window.setTimeout(function(){0!== +d.readyState&&d.send(a.data)},0);return d},runCallbacks:function(a){var b=a.request,c=a.config,d=c.scope?OpenLayers.Function.bind(c.callback,c.scope):c.callback,e;c.success&&(e=c.scope?OpenLayers.Function.bind(c.success,c.scope):c.success);var f;c.failure&&(f=c.scope?OpenLayers.Function.bind(c.failure,c.scope):c.failure);"file:"==OpenLayers.Util.createUrlObject(c.url).protocol&&b.responseText&&(b.status=200);d(b);if(!b.status||200<=b.status&&300>b.status)this.events.triggerEvent("success",a),e&&e(b); +if(b.status&&(200>b.status||300<=b.status))this.events.triggerEvent("failure",a),f&&f(b)},GET:function(a){a=OpenLayers.Util.extend(a,{method:"GET"});return OpenLayers.Request.issue(a)},POST:function(a){a=OpenLayers.Util.extend(a,{method:"POST"});a.headers=a.headers?a.headers:{};"CONTENT-TYPE"in OpenLayers.Util.upperCaseObject(a.headers)||(a.headers["Content-Type"]="application/xml");return OpenLayers.Request.issue(a)},PUT:function(a){a=OpenLayers.Util.extend(a,{method:"PUT"});a.headers=a.headers? +a.headers:{};"CONTENT-TYPE"in OpenLayers.Util.upperCaseObject(a.headers)||(a.headers["Content-Type"]="application/xml");return OpenLayers.Request.issue(a)},DELETE:function(a){a=OpenLayers.Util.extend(a,{method:"DELETE"});return OpenLayers.Request.issue(a)},HEAD:function(a){a=OpenLayers.Util.extend(a,{method:"HEAD"});return OpenLayers.Request.issue(a)},OPTIONS:function(a){a=OpenLayers.Util.extend(a,{method:"OPTIONS"});return OpenLayers.Request.issue(a)}};(function(){function a(){this._object=f&&!i?new f:new window.ActiveXObject("Microsoft.XMLHTTP");this._listeners=[]}function b(){return new a}function c(a){b.onreadystatechange&&b.onreadystatechange.apply(a);a.dispatchEvent({type:"readystatechange",bubbles:!1,cancelable:!1,timeStamp:new Date+0})}function d(a){try{a.responseText=a._object.responseText}catch(b){}try{var c;var d=a._object,e=d.responseXML,f=d.responseText;h&&(f&&e&&!e.documentElement&&d.getResponseHeader("Content-Type").match(/[^\/]+\/[^\+]+\+xml/))&& +(e=new window.ActiveXObject("Microsoft.XMLDOM"),e.async=!1,e.validateOnParse=!1,e.loadXML(f));c=e&&(h&&0!=e.parseError||!e.documentElement||e.documentElement&&"parsererror"==e.documentElement.tagName)?null:e;a.responseXML=c}catch(g){}try{a.status=a._object.status}catch(i){}try{a.statusText=a._object.statusText}catch(r){}}function e(a){a._object.onreadystatechange=new window.Function}var f=window.XMLHttpRequest,g=!!window.controllers,h=window.document.all&&!window.opera,i=h&&window.navigator.userAgent.match(/MSIE 7.0/); +b.prototype=a.prototype;g&&f.wrapped&&(b.wrapped=f.wrapped);b.UNSENT=0;b.OPENED=1;b.HEADERS_RECEIVED=2;b.LOADING=3;b.DONE=4;b.prototype.readyState=b.UNSENT;b.prototype.responseText="";b.prototype.responseXML=null;b.prototype.status=0;b.prototype.statusText="";b.prototype.priority="NORMAL";b.prototype.onreadystatechange=null;b.onreadystatechange=null;b.onopen=null;b.onsend=null;b.onabort=null;b.prototype.open=function(a,f,i,m,l){delete this._headers;arguments.length<3&&(i=true);this._async=i;var n= +this,q=this.readyState,p;if(h&&i){p=function(){if(q!=b.DONE){e(n);n.abort()}};window.attachEvent("onunload",p)}b.onopen&&b.onopen.apply(this,arguments);arguments.length>4?this._object.open(a,f,i,m,l):arguments.length>3?this._object.open(a,f,i,m):this._object.open(a,f,i);this.readyState=b.OPENED;c(this);this._object.onreadystatechange=function(){if(!g||i){n.readyState=n._object.readyState;d(n);if(n._aborted)n.readyState=b.UNSENT;else{if(n.readyState==b.DONE){delete n._data;e(n);h&&i&&window.detachEvent("onunload", +p)}q!=n.readyState&&c(n);q=n.readyState}}}};b.prototype.send=function(a){b.onsend&&b.onsend.apply(this,arguments);arguments.length||(a=null);if(a&&a.nodeType){a=window.XMLSerializer?(new window.XMLSerializer).serializeToString(a):a.xml;this._headers["Content-Type"]||this._object.setRequestHeader("Content-Type","application/xml")}this._data=a;a:{this._object.send(this._data);if(g&&!this._async){this.readyState=b.OPENED;for(d(this);this.readyStateb.UNSENT)this._aborted=true;this._object.abort();e(this);this.readyState=b.UNSENT;delete this._data};b.prototype.getAllResponseHeaders=function(){return this._object.getAllResponseHeaders()};b.prototype.getResponseHeader=function(a){return this._object.getResponseHeader(a)};b.prototype.setRequestHeader=function(a,b){if(!this._headers)this._headers={};this._headers[a]=b;return this._object.setRequestHeader(a, +b)};b.prototype.addEventListener=function(a,b,c){for(var d=0,e;e=this._listeners[d];d++)if(e[0]==a&&e[1]==b&&e[2]==c)return;this._listeners.push([a,b,c])};b.prototype.removeEventListener=function(a,b,c){for(var d=0,e;e=this._listeners[d];d++)if(e[0]==a&&e[1]==b&&e[2]==c)break;e&&this._listeners.splice(d,1)};b.prototype.dispatchEvent=function(a){a={type:a.type,target:this,currentTarget:this,eventPhase:2,bubbles:a.bubbles,cancelable:a.cancelable,timeStamp:a.timeStamp,stopPropagation:function(){},preventDefault:function(){}, +initEvent:function(){}};a.type=="readystatechange"&&this.onreadystatechange&&(this.onreadystatechange.handleEvent||this.onreadystatechange).apply(this,[a]);for(var b=0,c;c=this._listeners[b];b++)c[0]==a.type&&!c[2]&&(c[1].handleEvent||c[1]).apply(this,[a])};b.prototype.toString=function(){return"[object XMLHttpRequest]"};b.toString=function(){return"[XMLHttpRequest]"};window.Function.prototype.apply||(window.Function.prototype.apply=function(a,b){b||(b=[]);a.__func=this;a.__func(b[0],b[1],b[2],b[3], +b[4]);delete a.__func});OpenLayers.Request.XMLHttpRequest=b})();OpenLayers.Marker.Box=OpenLayers.Class(OpenLayers.Marker,{bounds:null,div:null,initialize:function(a,b,c){this.bounds=a;this.div=OpenLayers.Util.createDiv();this.div.style.overflow="hidden";this.events=new OpenLayers.Events(this,this.div);this.setBorder(b,c)},destroy:function(){this.div=this.bounds=null;OpenLayers.Marker.prototype.destroy.apply(this,arguments)},setBorder:function(a,b){a||(a="red");b||(b=2);this.div.style.border=b+"px solid "+a},draw:function(a,b){OpenLayers.Util.modifyDOMElement(this.div, +null,a,b);return this.div},onScreen:function(){var a=!1;this.map&&(a=this.map.getExtent().containsBounds(this.bounds,!0,!0));return a},display:function(a){this.div.style.display=a?"":"none"},CLASS_NAME:"OpenLayers.Marker.Box"});OpenLayers.Filter.Comparison=OpenLayers.Class(OpenLayers.Filter,{type:null,property:null,value:null,matchCase:!0,lowerBoundary:null,upperBoundary:null,initialize:function(a){OpenLayers.Filter.prototype.initialize.apply(this,[a]);this.type===OpenLayers.Filter.Comparison.LIKE&&void 0===a.matchCase&&(this.matchCase=null)},evaluate:function(a){a instanceof OpenLayers.Feature.Vector&&(a=a.attributes);var b=!1,a=a[this.property];switch(this.type){case OpenLayers.Filter.Comparison.EQUAL_TO:b=this.value; +b=!this.matchCase&&"string"==typeof a&&"string"==typeof b?a.toUpperCase()==b.toUpperCase():a==b;break;case OpenLayers.Filter.Comparison.NOT_EQUAL_TO:b=this.value;b=!this.matchCase&&"string"==typeof a&&"string"==typeof b?a.toUpperCase()!=b.toUpperCase():a!=b;break;case OpenLayers.Filter.Comparison.LESS_THAN:b=athis.value;break;case OpenLayers.Filter.Comparison.LESS_THAN_OR_EQUAL_TO:b=a<=this.value;break;case OpenLayers.Filter.Comparison.GREATER_THAN_OR_EQUAL_TO:b= +a>=this.value;break;case OpenLayers.Filter.Comparison.BETWEEN:b=a>=this.lowerBoundary&&a<=this.upperBoundary;break;case OpenLayers.Filter.Comparison.LIKE:b=RegExp(this.value,"gi").test(a)}return b},value2regex:function(a,b,c){if("."==a)throw Error("'.' is an unsupported wildCard character for OpenLayers.Filter.Comparison");a=a?a:"*";b=b?b:".";this.value=this.value.replace(RegExp("\\"+(c?c:"!")+"(.|$)","g"),"\\$1");this.value=this.value.replace(RegExp("\\"+b,"g"),".");this.value=this.value.replace(RegExp("\\"+ +a,"g"),".*");this.value=this.value.replace(RegExp("\\\\.\\*","g"),"\\"+a);return this.value=this.value.replace(RegExp("\\\\\\.","g"),"\\"+b)},regex2value:function(){var a=this.value,a=a.replace(/!/g,"!!"),a=a.replace(/(\\)?\\\./g,function(a,c){return c?a:"!."}),a=a.replace(/(\\)?\\\*/g,function(a,c){return c?a:"!*"}),a=a.replace(/\\\\/g,"\\");return a=a.replace(/\.\*/g,"*")},clone:function(){return OpenLayers.Util.extend(new OpenLayers.Filter.Comparison,this)},CLASS_NAME:"OpenLayers.Filter.Comparison"}); +OpenLayers.Filter.Comparison.EQUAL_TO="==";OpenLayers.Filter.Comparison.NOT_EQUAL_TO="!=";OpenLayers.Filter.Comparison.LESS_THAN="<";OpenLayers.Filter.Comparison.GREATER_THAN=">";OpenLayers.Filter.Comparison.LESS_THAN_OR_EQUAL_TO="<=";OpenLayers.Filter.Comparison.GREATER_THAN_OR_EQUAL_TO=">=";OpenLayers.Filter.Comparison.BETWEEN="..";OpenLayers.Filter.Comparison.LIKE="~";OpenLayers.Popup.FramedCloud=OpenLayers.Class(OpenLayers.Popup.Framed,{contentDisplayClass:"olFramedCloudPopupContent",autoSize:!0,panMapIfOutOfView:!0,imageSize:new OpenLayers.Size(1276,736),isAlphaImage:!1,fixedRelativePosition:!1,positionBlocks:{tl:{offset:new OpenLayers.Pixel(44,0),padding:new OpenLayers.Bounds(8,40,8,9),blocks:[{size:new OpenLayers.Size("auto","auto"),anchor:new OpenLayers.Bounds(0,51,22,0),position:new OpenLayers.Pixel(0,0)},{size:new OpenLayers.Size(22,"auto"),anchor:new OpenLayers.Bounds(null, +50,0,0),position:new OpenLayers.Pixel(-1238,0)},{size:new OpenLayers.Size("auto",19),anchor:new OpenLayers.Bounds(0,32,22,null),position:new OpenLayers.Pixel(0,-631)},{size:new OpenLayers.Size(22,18),anchor:new OpenLayers.Bounds(null,32,0,null),position:new OpenLayers.Pixel(-1238,-632)},{size:new OpenLayers.Size(81,35),anchor:new OpenLayers.Bounds(null,0,0,null),position:new OpenLayers.Pixel(0,-688)}]},tr:{offset:new OpenLayers.Pixel(-45,0),padding:new OpenLayers.Bounds(8,40,8,9),blocks:[{size:new OpenLayers.Size("auto", +"auto"),anchor:new OpenLayers.Bounds(0,51,22,0),position:new OpenLayers.Pixel(0,0)},{size:new OpenLayers.Size(22,"auto"),anchor:new OpenLayers.Bounds(null,50,0,0),position:new OpenLayers.Pixel(-1238,0)},{size:new OpenLayers.Size("auto",19),anchor:new OpenLayers.Bounds(0,32,22,null),position:new OpenLayers.Pixel(0,-631)},{size:new OpenLayers.Size(22,19),anchor:new OpenLayers.Bounds(null,32,0,null),position:new OpenLayers.Pixel(-1238,-631)},{size:new OpenLayers.Size(81,35),anchor:new OpenLayers.Bounds(0, +0,null,null),position:new OpenLayers.Pixel(-215,-687)}]},bl:{offset:new OpenLayers.Pixel(45,0),padding:new OpenLayers.Bounds(8,9,8,40),blocks:[{size:new OpenLayers.Size("auto","auto"),anchor:new OpenLayers.Bounds(0,21,22,32),position:new OpenLayers.Pixel(0,0)},{size:new OpenLayers.Size(22,"auto"),anchor:new OpenLayers.Bounds(null,21,0,32),position:new OpenLayers.Pixel(-1238,0)},{size:new OpenLayers.Size("auto",21),anchor:new OpenLayers.Bounds(0,0,22,null),position:new OpenLayers.Pixel(0,-629)},{size:new OpenLayers.Size(22, +21),anchor:new OpenLayers.Bounds(null,0,0,null),position:new OpenLayers.Pixel(-1238,-629)},{size:new OpenLayers.Size(81,33),anchor:new OpenLayers.Bounds(null,null,0,0),position:new OpenLayers.Pixel(-101,-674)}]},br:{offset:new OpenLayers.Pixel(-44,0),padding:new OpenLayers.Bounds(8,9,8,40),blocks:[{size:new OpenLayers.Size("auto","auto"),anchor:new OpenLayers.Bounds(0,21,22,32),position:new OpenLayers.Pixel(0,0)},{size:new OpenLayers.Size(22,"auto"),anchor:new OpenLayers.Bounds(null,21,0,32),position:new OpenLayers.Pixel(-1238, +0)},{size:new OpenLayers.Size("auto",21),anchor:new OpenLayers.Bounds(0,0,22,null),position:new OpenLayers.Pixel(0,-629)},{size:new OpenLayers.Size(22,21),anchor:new OpenLayers.Bounds(null,0,0,null),position:new OpenLayers.Pixel(-1238,-629)},{size:new OpenLayers.Size(81,33),anchor:new OpenLayers.Bounds(0,null,null,0),position:new OpenLayers.Pixel(-311,-674)}]}},minSize:new OpenLayers.Size(105,10),maxSize:new OpenLayers.Size(1200,660),initialize:function(a,b,c,d,e,f,g){this.imageSrc=OpenLayers.Util.getImageLocation("cloud-popup-relative.png"); +OpenLayers.Popup.Framed.prototype.initialize.apply(this,arguments);this.contentDiv.className=this.contentDisplayClass},CLASS_NAME:"OpenLayers.Popup.FramedCloud"});OpenLayers.Rule=OpenLayers.Class({id:null,name:null,title:null,description:null,context:null,filter:null,elseFilter:!1,symbolizer:null,symbolizers:null,minScaleDenominator:null,maxScaleDenominator:null,initialize:function(a){this.symbolizer={};OpenLayers.Util.extend(this,a);this.symbolizers&&delete this.symbolizer;this.id=OpenLayers.Util.createUniqueID(this.CLASS_NAME+"_")},destroy:function(){for(var a in this.symbolizer)this.symbolizer[a]=null;this.symbolizer=null;delete this.symbolizers},evaluate:function(a){var b= +this.getContext(a),c=!0;if(this.minScaleDenominator||this.maxScaleDenominator)var d=a.layer.map.getScale();this.minScaleDenominator&&(c=d>=OpenLayers.Style.createLiteral(this.minScaleDenominator,b));c&&this.maxScaleDenominator&&(c=dthis.granularity||Math.abs(a.xy.y-this.lastXy.y)>this.granularity)this.lastXy=a.xy;else if(b=this.map.getLonLatFromPixel(a.xy))this.displayProjection&&b.transform(this.map.getProjectionObject(),this.displayProjection),this.lastXy=a.xy,a=this.formatOutput(b),a!=this.element.innerHTML&&(this.element.innerHTML=a)},reset:function(){null!=this.emptyString&&(this.element.innerHTML=this.emptyString)},formatOutput:function(a){var b=parseInt(this.numDigits);return this.prefix+a.lon.toFixed(b)+ +this.separator+a.lat.toFixed(b)+this.suffix},CLASS_NAME:"OpenLayers.Control.MousePosition"});OpenLayers.Format.GML=OpenLayers.Class(OpenLayers.Format.XML,{featureNS:"http://mapserver.gis.umn.edu/mapserver",featurePrefix:"feature",featureName:"featureMember",layerName:"features",geometryName:"geometry",collectionName:"FeatureCollection",gmlns:"http://www.opengis.net/gml",extractAttributes:!0,xy:!0,initialize:function(a){this.regExes={trimSpace:/^\s*|\s*$/g,removeSpace:/\s*/g,splitSpace:/\s+/,trimComma:/\s*,\s*/g};OpenLayers.Format.XML.prototype.initialize.apply(this,[a])},read:function(a){"string"== +typeof a&&(a=OpenLayers.Format.XML.prototype.read.apply(this,[a]));for(var a=this.getElementsByTagNameNS(a.documentElement,this.gmlns,this.featureName),b=[],c=0;c=2*a[1]?"longdash":1==a[0]||1==a[1]?"dot":"dash":4==a.length?1*a[0]>=2*a[1]?"longdashdot":"dashdot":"solid"}},createNode:function(a,b){var c=document.createElement(a);b&&(c.id=b);c.unselectable="on";c.onselectstart=OpenLayers.Function.False;return c},nodeTypeCompare:function(a,b){var c=b,d=c.indexOf(":");-1!=d&&(c=c.substr(d+ +1));var e=a.nodeName,d=e.indexOf(":");-1!=d&&(e=e.substr(d+1));return c==e},createRenderRoot:function(){return this.nodeFactory(this.container.id+"_vmlRoot","div")},createRoot:function(a){return this.nodeFactory(this.container.id+a,"olv:group")},drawPoint:function(a,b){return this.drawCircle(a,b,1)},drawCircle:function(a,b,c){if(!isNaN(b.x)&&!isNaN(b.y)){var d=this.getResolution();a.style.left=((b.x-this.featureDx)/d-this.offset.x|0)-c+"px";a.style.top=(b.y/d-this.offset.y|0)-c+"px";b=2*c;a.style.width= +b+"px";a.style.height=b+"px";return a}return!1},drawLineString:function(a,b){return this.drawLine(a,b,!1)},drawLinearRing:function(a,b){return this.drawLine(a,b,!0)},drawLine:function(a,b,c){this.setNodeDimension(a,b);for(var d=this.getResolution(),e=b.components.length,f=Array(e),g,h,i=0;ic.status?("delete"!=a.requestType&&(a.features=this.parseFeatures(c)),a.code=OpenLayers.Protocol.Response.SUCCESS):a.code=OpenLayers.Protocol.Response.FAILURE,b.callback.call(b.scope,a))},parseFeatures:function(a){var b=a.responseXML;if(!b||!b.documentElement)b=a.responseText;return!b||0>=b.length?null:this.format.read(b)},commit:function(a,b){function c(a){for(var b= +a.features?a.features.length:0,c=Array(b),e=0;e=m&&b.callback&&(n.code=l?OpenLayers.Protocol.Response.SUCCESS:OpenLayers.Protocol.Response.FAILURE,b.callback.apply(b.scope,[n]))}var b=OpenLayers.Util.applyDefaults(b,this.options),e=[],f=0,g={};g[OpenLayers.State.INSERT]=[];g[OpenLayers.State.UPDATE]=[];g[OpenLayers.State.DELETE]=[];for(var h,i,j=[],k=0,o=a.length;kWarning! Error response on HTTP '+c.type+" to "+c.url.split("?")[0]+d+"")}),a.navBarConfig&&_.each(a.navBarConfig.items,function(a){a.show&&b.mediator.trigger(a.eventToRaise)},this),$("#loadscreen").remove()}});return new h})}).call(this); \ No newline at end of file diff -Nru eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/communicator.js eoxserver-0.3.2/eoxserver/webclient/static/scripts/communicator.js --- eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/communicator.js 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/webclient/static/scripts/communicator.js 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -(function(){"use strict";var a=this;a.define(["backbone","backbone.marionette"],function(a){var b=a.Marionette.Controller.extend({initialize:function(){this.mediator=new a.Wreqr.EventAggregator,this.mediator.on("all",function(a){"progress:change"!=a&&"router:setUrl"!=a&&console.log(a)}),this.reqres=new a.Wreqr.RequestResponse,this.command=new a.Wreqr.Commands,this.on("all")}});return new b})}).call(this); \ No newline at end of file diff -Nru eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/controller/ContentController.js eoxserver-0.3.2/eoxserver/webclient/static/scripts/controller/ContentController.js --- eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/controller/ContentController.js 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/webclient/static/scripts/controller/ContentController.js 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -(function(){"use strict";var a=this;a.require(["backbone","communicator","app"],function(a,b,c){var d=a.Marionette.Controller.extend({initialize:function(){this.listenTo(b.mediator,"dialog:open:about",this.onDialogOpenAbout),this.listenTo(b.mediator,"ui:open:layercontrol",this.onLayerControlOpen),this.listenTo(b.mediator,"ui:open:toolselection",this.onToolSelectionOpen)},onDialogOpenAbout:function(){c.dialogRegion.show(c.DialogContentView)},onLayerControlOpen:function(){_.isUndefined(c.layout.isClosed)||c.layout.isClosed?(c.leftSideBar.show(c.layout),c.layout.baseLayers.show(c.baseLayerView),c.layout.products.show(c.productsView),c.layout.overlays.show(c.overlaysView)):c.layout.close()},onToolSelectionOpen:function(){_.isUndefined(c.toolLayout.isClosed)||c.toolLayout.isClosed?(c.rightSideBar.show(c.toolLayout),c.toolLayout.selection.show(c.selectionToolsView),c.toolLayout.visualization.show(c.visualizationToolsView)):c.toolLayout.close()}});return new d})}).call(this); \ No newline at end of file diff -Nru eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/controller/DownloadController.js eoxserver-0.3.2/eoxserver/webclient/static/scripts/controller/DownloadController.js --- eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/controller/DownloadController.js 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/webclient/static/scripts/controller/DownloadController.js 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -(function(){"use strict";var a=this;a.require(["backbone","communicator","globals","app","views/DownloadView","models/DownloadModel"],function(a,b,c,d,e,f){var g=a.Marionette.Controller.extend({model:new f.DownloadModel,initialize:function(){this.model.set("products",{}),this.listenTo(b.mediator,"map:layer:change",this.onChangeLayer),this.listenTo(b.mediator,"time:change",this.onTimeChange),this.listenTo(b.mediator,"selection:changed",this.onSelectionChange),this.listenTo(b.mediator,"dialog:open:download",this.onDownloadToolOpen)},onChangeLayer:function(a){var b=c.products.find(function(b){return b.get("view").id==a.id});if(b){var d=this.model.get("products");a.visible?d[b.get("download").id]=b:delete d[b.get("download").id],this.model.set("products",d)}this.checkDownload()},onTimeChange:function(a){this.model.set("ToI",a),this.checkDownload()},onSelectionChange:function(a){null!=a?"Polygon"===a.getType()&&this.model.set("AoI",a):this.model.set("AoI",null),this.checkDownload()},checkDownload:function(){null!=this.model.get("ToI")&&null!=this.model.get("AoI")&&_.size(this.model.get("products"))>0?b.mediator.trigger("selection:enabled",{id:"download",enabled:!0}):b.mediator.trigger("selection:enabled",{id:"download",enabled:!1})},onDownloadToolOpen:function(a){a?d.viewContent.show(new e.DownloadView({model:this.model})):d.viewContent.close()}});return new g})}).call(this); \ No newline at end of file diff -Nru eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/controller/LoadingController.js eoxserver-0.3.2/eoxserver/webclient/static/scripts/controller/LoadingController.js --- eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/controller/LoadingController.js 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/webclient/static/scripts/controller/LoadingController.js 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -(function(){"use strict";var a=this;a.require(["backbone","communicator","globals","app","jquery"],function(a,b){var c=a.Marionette.Controller.extend({progress_count:0,initialize:function(){this.listenTo(b.mediator,"progress:change",this.onProgressChange)},onProgressChange:function(a){a?this.progress_count+=1:this.progress_count-=1,this.progress_count>0?$("body").addClass("wait"):$("body").removeClass("wait")}});return new c})}).call(this); \ No newline at end of file diff -Nru eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/controller/SelectionManagerController.js eoxserver-0.3.2/eoxserver/webclient/static/scripts/controller/SelectionManagerController.js --- eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/controller/SelectionManagerController.js 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/webclient/static/scripts/controller/SelectionManagerController.js 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -(function(){"use strict";var a=this;a.require(["backbone","communicator","globals","app","views/SelectionManagerView","models/SelectionModel"],function(a,b,c,d,e,f){var g=a.Marionette.Controller.extend({model:new f.SelectionModel,initialize:function(){this.model.set("selections",[]),this.listenTo(b.mediator,"selection:changed:json",this.onSelectionChange),this.listenTo(b.mediator,"ui:open:selectionManager",this.onSelectionManagerOpen)},onSelectionChange:function(a){if(null!=a){var b=this.model.get("selections");b.push(a),this.model.set("selections",b)}else this.model.set("selections",[])},onSelectionManagerOpen:function(a){a?d.viewContent.show(new e.SelectionManagerView({model:this.model})):d.viewContent.close()}});return new g})}).call(this); \ No newline at end of file diff -Nru eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/globals.js eoxserver-0.3.2/eoxserver/webclient/static/scripts/globals.js --- eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/globals.js 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/webclient/static/scripts/globals.js 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -define(["backbone","objectStore"],function(a,b){return{objects:new b,selections:new b,baseLayers:new a.Collection,products:new a.Collection,overlays:new a.Collection}}); \ No newline at end of file diff -Nru eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/init.js eoxserver-0.3.2/eoxserver/webclient/static/scripts/init.js --- eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/init.js 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/webclient/static/scripts/init.js 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -(function(){"use strict";var a=this;a.require.config({deps:["backbone.marionette","bootstrap","marionette.handlebars","main"],shim:{handlebars:{exports:"Handlebars"},backbone:{deps:["underscore","jquery"],exports:"Backbone"},bootstrap:{deps:["jquery"],exports:"jquery"},libcoverage:{deps:["backbone"]},filesaver:{exports:"saveAs"},lm:{exports:"lm"},timeslider:{deps:["d3"]},timeslider_plugins:{deps:["timeslider","libcoverage"]}},paths:{requirejs:"../bower_components/requirejs/require",jquery:"../bower_components/jquery/jquery.min",jqueryui:"../bower_components/jquery-ui/ui/minified/jquery-ui.min",backbone:"../bower_components/backbone-amd/backbone-min",underscore:"../bower_components/underscore-amd/underscore-min",d3:"../bower_components/d3/d3.min",timeslider:"../bower_components/d3.TimeSlider/d3.timeslider.min",timeslider_plugins:"../bower_components/d3.TimeSlider/d3.timeslider.plugins.min",libcoverage:"../bower_components/libcoverage/libcoverage.min",filesaver:"../bower_components/filesaver/FileSaver",lm:"../bower_components/lm.js/lm","backbone.marionette":"../bower_components/backbone.marionette/lib/core/amd/backbone.marionette.min","backbone.wreqr":"../bower_components/backbone.wreqr/lib/amd/backbone.wreqr.min","backbone.babysitter":"../bower_components/backbone.babysitter/lib/backbone.babysitter.min",bootstrap:"../bower_components/bootstrap/dist/js/bootstrap.min",text:"../bower_components/requirejs-text/text",tmpl:"../templates",handlebars:"../bower_components/require-handlebars-plugin/Handlebars",i18nprecompile:"../bower_components/require-handlebars-plugin/hbs/i18nprecompile",json2:"../bower_components/require-handlebars-plugin/hbs/json2",hbs:"../bower_components/require-handlebars-plugin/hbs","marionette.handlebars":"../bower_components/backbone.marionette.handlebars/backbone.marionette.handlebars.min",openlayers:"../bower_components/ol3/build/ol"},hbs:{disableI18n:!0}})}).call(this); \ No newline at end of file diff -Nru eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/layouts/LayerControlLayout.js eoxserver-0.3.2/eoxserver/webclient/static/scripts/layouts/LayerControlLayout.js --- eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/layouts/LayerControlLayout.js 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/webclient/static/scripts/layouts/LayerControlLayout.js 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -(function(){"use strict";var a=this;a.define(["backbone","communicator","hbs!tmpl/LayerControl","underscore"],function(a,b,c){var d=a.Marionette.Layout.extend({template:{type:"handlebars",template:c},regions:{baseLayers:"#baseLayers",products:"#products",overlays:"#overlays"},className:"panel panel-default layercontrol not-selectable",events:{},initialize:function(){},onShow:function(){this.$(".close").on("click",_.bind(this.onClose,this)),this.$el.draggable({handle:".panel-heading",containment:"#content",scroll:!1,start:function(){$(".ui-slider").detach(),$(".fa-adjust").toggleClass("active"),$(".fa-adjust").popover("hide")}})},onClose:function(){this.close()}});return d})}).call(this); \ No newline at end of file diff -Nru eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/layouts/ToolControlLayout.js eoxserver-0.3.2/eoxserver/webclient/static/scripts/layouts/ToolControlLayout.js --- eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/layouts/ToolControlLayout.js 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/webclient/static/scripts/layouts/ToolControlLayout.js 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -(function(){"use strict";var a=this;a.define(["backbone","communicator","hbs!tmpl/ToolControl","underscore"],function(a,b,c){var d=a.Marionette.Layout.extend({template:{type:"handlebars",template:c},regions:{selection:"#selection",visualization:"#visualization"},className:"panel panel-default toolcontrol not-selectable",initialize:function(){},onShow:function(){this.$(".close").on("click",_.bind(this.onClose,this)),this.$el.draggable({containment:"#content",scroll:!1,handle:".panel-heading"})},onClose:function(){this.close()}});return d})}).call(this); \ No newline at end of file diff -Nru eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/main.js eoxserver-0.3.2/eoxserver/webclient/static/scripts/main.js --- eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/main.js 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/webclient/static/scripts/main.js 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -(function(){"use strict";function a(a){a&&"undefined"!=typeof console&&console.log||(window.console={debug:function(){},trace:function(){},log:function(){},info:function(){},warn:function(){},error:function(){}})}var b=this;b.require(["backbone","app","backbone.marionette","regionManager","jquery","jqueryui","util","libcoverage"],function(c,d){$.getJSON("configuration/",function(c){a(c.debug);var e=[],f=[],g=[];_.each(c.views,function(a){e.push(a)},this),_.each(c.models,function(a){f.push(a)},this),_.each(c.templates,function(a){g.push(a.template)},this),b.require([].concat(c.mapConfig.visualizationLibs,c.mapConfig.module,c.mapConfig.model,e,f,g),function(){d.configure(c),d.start()}),setTimeout(function(){$("#loadscreen").length&&($("#loadscreen").remove(),$("#error-messages").append('
    Warning!

    There was a problem loading the application, some functionality might not be available.

    Please contact the website administrator if you have any problems.

    '))},1e4)}).fail(function(){$("#loadscreen").empty(),$("#loadscreen").html('

    There was a problem loading the configuration file, please contact the site administrator

    ')})})}).call(this); \ No newline at end of file diff -Nru eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/models/DownloadModel.js eoxserver-0.3.2/eoxserver/webclient/static/scripts/models/DownloadModel.js --- eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/models/DownloadModel.js 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/webclient/static/scripts/models/DownloadModel.js 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -(function(){"use strict";var a=this;a.define(["backbone","communicator"],function(a){var b=a.Model.extend({ToI:{},AoI:[],products:{}});return{DownloadModel:b}})}).call(this); \ No newline at end of file diff -Nru eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/models/LayerModel.js eoxserver-0.3.2/eoxserver/webclient/static/scripts/models/LayerModel.js --- eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/models/LayerModel.js 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/webclient/static/scripts/models/LayerModel.js 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -(function(){"use strict";var a=this;a.define(["backbone"],function(a){var b=a.Model.extend({name:"",timeSlider:!1,timeSliderProtocol:"",color:"",time:null,visible:null,opacity:0,view:{id:"",protocol:"",urls:[],style:"default",isBaseLayer:null,attribution:"",matrixSet:"",format:"",resolutions:[],maxExtent:[],projection:"",gutter:null,buffer:null,units:"",transitionEffect:"",isphericalMercator:null,wrapDateLine:null,zoomOffset:null,requestEncoding:"KVP"},download:{id:"",protocol:"",url:[]}});return{LayerModel:b}})}).call(this); \ No newline at end of file diff -Nru eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/models/MapModel.js eoxserver-0.3.2/eoxserver/webclient/static/scripts/models/MapModel.js --- eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/models/MapModel.js 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/webclient/static/scripts/models/MapModel.js 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -(function(){"use strict";var a=this;a.define(["backbone","communicator"],function(a){var b=a.Model.extend({visualizationLibs:[],center:[],zoom:0});return{MapModel:b}})}).call(this); \ No newline at end of file diff -Nru eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/models/NavBarCollection.js eoxserver-0.3.2/eoxserver/webclient/static/scripts/models/NavBarCollection.js --- eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/models/NavBarCollection.js 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/webclient/static/scripts/models/NavBarCollection.js 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -(function(){"use strict";var a=this;a.define(["backbone","communicator","models/NavBarItemModel"],function(a,b,c){var d=a.Collection.extend({model:c});return{NavBarCollection:d}})}).call(this); \ No newline at end of file diff -Nru eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/models/NavBarItemModel.js eoxserver-0.3.2/eoxserver/webclient/static/scripts/models/NavBarItemModel.js --- eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/models/NavBarItemModel.js 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/webclient/static/scripts/models/NavBarItemModel.js 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -(function(){"use strict";var a=this;a.define(["backbone","communicator"],function(a){var b=a.Model.extend({name:"",icon:"",content:"",eventToRaise:""});return{NavBarItemModel:b}})}).call(this); \ No newline at end of file diff -Nru eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/models/SelectionModel.js eoxserver-0.3.2/eoxserver/webclient/static/scripts/models/SelectionModel.js --- eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/models/SelectionModel.js 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/webclient/static/scripts/models/SelectionModel.js 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -(function(){"use strict";var a=this;a.define(["backbone","communicator"],function(a){var b=a.Model.extend({selections:[]});return{SelectionModel:b}})}).call(this); \ No newline at end of file diff -Nru eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/models/ToolCollection.js eoxserver-0.3.2/eoxserver/webclient/static/scripts/models/ToolCollection.js --- eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/models/ToolCollection.js 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/webclient/static/scripts/models/ToolCollection.js 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -(function(){"use strict";var a=this;a.define(["backbone","communicator","models/ToolModel"],function(a,b,c){var d=a.Collection.extend({model:c});return{ToolCollection:d}})}).call(this); \ No newline at end of file diff -Nru eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/models/ToolModel.js eoxserver-0.3.2/eoxserver/webclient/static/scripts/models/ToolModel.js --- eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/models/ToolModel.js 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/webclient/static/scripts/models/ToolModel.js 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -(function(){"use strict";var a=this;a.define(["backbone","communicator"],function(a){var b=a.Model.extend({id:"",description:"",disabledDescription:"",active:!1,enabled:!0,icon:"",type:"",size:null});return{ToolModel:b}})}).call(this); \ No newline at end of file diff -Nru eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/objectStore.js eoxserver-0.3.2/eoxserver/webclient/static/scripts/objectStore.js --- eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/objectStore.js 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/webclient/static/scripts/objectStore.js 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -define(["backbone"],function(){var a=function(){this.objects={}};return a.prototype.add=function(a,b){this.objects[a]=b,this.trigger("add",b)},a.prototype.remove=function(a){var b=this.object[a];delete this.objects[a],this.trigger("remove",b)},a.prototype.get=function(a){return this.objects[a]},_.extend(a.prototype,Backbone.Events),a}); \ No newline at end of file diff -Nru eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/regionManager.js eoxserver-0.3.2/eoxserver/webclient/static/scripts/regionManager.js --- eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/regionManager.js 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/webclient/static/scripts/regionManager.js 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -(function(){"use strict";var a=this;a.define(["backbone","communicator"],function(a,b){var c=a.Marionette.Controller.extend({initialize:function(){this._regionManager=new a.Marionette.RegionManager,b.reqres.setHandler("RM:addRegion",this.addRegion,this),b.reqres.setHandler("RM:removeRegion",this.removeRegion,this),b.reqres.setHandler("RM:getRegion",this.getRegion,this)},addRegion:function(a,b){return this._regionManager.addRegion(a,b)},removeRegion:function(a){this._regionManager.removeRegion(a)},getRegion:function(a){return this._regionManager.get(a)}});return new c})}).call(this); \ No newline at end of file diff -Nru eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/regions/DialogRegion.js eoxserver-0.3.2/eoxserver/webclient/static/scripts/regions/DialogRegion.js --- eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/regions/DialogRegion.js 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/webclient/static/scripts/regions/DialogRegion.js 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -(function(){"use strict";var a=this;a.define(["backbone","communicator","backbone.marionette"],function(a){var b=a.Marionette.Region.extend({constructor:function(){_.bindAll(this),a.Marionette.Region.prototype.constructor.apply(this,arguments),this.on("show",this.showModal,this)},getEl:function(a){var b=$(a);return b.on("hidden",this.close),b},showModal:function(a){a.on("close",this.hideModal,this),a.$el.modal("show")},hideModal:function(){}});return b})}).call(this); \ No newline at end of file diff -Nru eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/regions/UIRegion.js eoxserver-0.3.2/eoxserver/webclient/static/scripts/regions/UIRegion.js --- eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/regions/UIRegion.js 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/webclient/static/scripts/regions/UIRegion.js 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -(function(){"use strict";var a=this;a.define(["backbone","communicator","backbone.marionette"],function(a){var b=a.Marionette.Region.extend({constructor:function(){},onShow:function(a){a.$(".close").on("click",_.bind(this.onClose,this))},onClose:function(){this.close()}});return b})}).call(this); \ No newline at end of file diff -Nru eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/router.js eoxserver-0.3.2/eoxserver/webclient/static/scripts/router.js --- eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/router.js 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/webclient/static/scripts/router.js 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -(function(){"use strict";var a=this;a.require(["app","backbone","communicator","backbone.marionette"],function(a,b,c){a.addInitializer(function(){a.router=new d,b.history.start({pushState:!1})});var d=b.Marionette.AppRouter.extend({initialize:function(){this.listenTo(c.mediator,"router:setUrl",this.setUrl)},setUrl:function(b){b.x=Math.round(100*b.x)/100,b.y=Math.round(100*b.y)/100;var c="map/"+b.x+"/"+b.y+"/"+b.l;a.router.navigate(c,{trigger:!1})},routes:{"map/:x/:y/:l":"centerAndZoom"},centerAndZoom:function(a,b,d){c.mediator.trigger("map:center",{x:a,y:b,l:d})}});return d})}).call(this); \ No newline at end of file diff -Nru eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/util.js eoxserver-0.3.2/eoxserver/webclient/static/scripts/util.js --- eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/util.js 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/webclient/static/scripts/util.js 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -var padLeft=function(a,b,c){for(;a.lengthb;++b)c=(c<<5)-c+a.charCodeAt(b),c&=2147483647;for(c=Number(c).toString(16);c.length<8;)c="0"+c;return c};var getCoverageXML=function(a,b){if(!a)throw new Error("Parameters 'coverageid' is mandatory.");b=b||{},subsetCRS=b.subsetCRS||"http://www.opengis.net/def/crs/EPSG/0/4326";var c=['',""+a+""],d=[];return b.format&&c.push(""+b.format+""),!b.bbox||b.subsetX||b.subsetY||(b.subsetX=[b.bbox[0],b.bbox[2]],b.subsetY=[b.bbox[1],b.bbox[3]]),b.subsetX&&c.push("x"+b.subsetX[0]+""+b.subsetX[1]+""),b.subsetY&&c.push("y"+b.subsetY[0]+""+b.subsetY[1]+""),b.mask&&d.push(""+b.mask+""),b.multipart&&c.push("multipart/related"),c.push(""),subsetCRS&&c.push(""+subsetCRS+""),b.outputCRS&&c.push(""+b.outputCRS+""),c.push(""),c.push(""),c.join("")}; \ No newline at end of file diff -Nru eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/vendor/bootstrap.js eoxserver-0.3.2/eoxserver/webclient/static/scripts/vendor/bootstrap.js --- eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/vendor/bootstrap.js 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/webclient/static/scripts/vendor/bootstrap.js 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -!function(a){"use strict";a(function(){a.support.transition=function(){var a=function(){var a,b=document.createElement("bootstrap"),c={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(a in c)if(void 0!==b.style[a])return c[a]}();return a&&{end:a}}()})}(window.jQuery),!function(a){"use strict";var b='[data-dismiss="alert"]',c=function(c){a(c).on("click",b,this.close)};c.prototype.close=function(b){function c(){d.trigger("closed").remove()}var d,e=a(this),f=e.attr("data-target");f||(f=e.attr("href"),f=f&&f.replace(/.*(?=#[^\s]*$)/,"")),d=a(f),b&&b.preventDefault(),d.length||(d=e.hasClass("alert")?e:e.parent()),d.trigger(b=a.Event("close")),b.isDefaultPrevented()||(d.removeClass("in"),a.support.transition&&d.hasClass("fade")?d.on(a.support.transition.end,c):c())};var d=a.fn.alert;a.fn.alert=function(b){return this.each(function(){var d=a(this),e=d.data("alert");e||d.data("alert",e=new c(this)),"string"==typeof b&&e[b].call(d)})},a.fn.alert.Constructor=c,a.fn.alert.noConflict=function(){return a.fn.alert=d,this},a(document).on("click.alert.data-api",b,c.prototype.close)}(window.jQuery),!function(a){"use strict";var b=function(b,c){this.$element=a(b),this.options=a.extend({},a.fn.button.defaults,c)};b.prototype.setState=function(a){var b="disabled",c=this.$element,d=c.data(),e=c.is("input")?"val":"html";a+="Text",d.resetText||c.data("resetText",c[e]()),c[e](d[a]||this.options[a]),setTimeout(function(){"loadingText"==a?c.addClass(b).attr(b,b):c.removeClass(b).removeAttr(b)},0)},b.prototype.toggle=function(){var a=this.$element.closest('[data-toggle="buttons-radio"]');a&&a.find(".active").removeClass("active"),this.$element.toggleClass("active")};var c=a.fn.button;a.fn.button=function(c){return this.each(function(){var d=a(this),e=d.data("button"),f="object"==typeof c&&c;e||d.data("button",e=new b(this,f)),"toggle"==c?e.toggle():c&&e.setState(c)})},a.fn.button.defaults={loadingText:"loading..."},a.fn.button.Constructor=b,a.fn.button.noConflict=function(){return a.fn.button=c,this},a(document).on("click.button.data-api","[data-toggle^=button]",function(b){var c=a(b.target);c.hasClass("btn")||(c=c.closest(".btn")),c.button("toggle")})}(window.jQuery),!function(a){"use strict";var b=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,"hover"==this.options.pause&&this.$element.on("mouseenter",a.proxy(this.pause,this)).on("mouseleave",a.proxy(this.cycle,this))};b.prototype={cycle:function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},getActiveIndex:function(){return this.$active=this.$element.find(".item.active"),this.$items=this.$active.parent().children(),this.$items.index(this.$active)},to:function(b){var c=this.getActiveIndex(),d=this;if(!(b>this.$items.length-1||0>b))return this.sliding?this.$element.one("slid",function(){d.to(b)}):c==b?this.pause().cycle():this.slide(b>c?"next":"prev",a(this.$items[b]))},pause:function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition.end&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),clearInterval(this.interval),this.interval=null,this},next:function(){return this.sliding?void 0:this.slide("next")},prev:function(){return this.sliding?void 0:this.slide("prev")},slide:function(b,c){var d,e=this.$element.find(".item.active"),f=c||e[b](),g=this.interval,h="next"==b?"left":"right",i="next"==b?"first":"last",j=this;if(this.sliding=!0,g&&this.pause(),f=f.length?f:this.$element.find(".item")[i](),d=a.Event("slide",{relatedTarget:f[0],direction:h}),!f.hasClass("active")){if(this.$indicators.length&&(this.$indicators.find(".active").removeClass("active"),this.$element.one("slid",function(){var b=a(j.$indicators.children()[j.getActiveIndex()]);b&&b.addClass("active")})),a.support.transition&&this.$element.hasClass("slide")){if(this.$element.trigger(d),d.isDefaultPrevented())return;f.addClass(b),f[0].offsetWidth,e.addClass(h),f.addClass(h),this.$element.one(a.support.transition.end,function(){f.removeClass([b,h].join(" ")).addClass("active"),e.removeClass(["active",h].join(" ")),j.sliding=!1,setTimeout(function(){j.$element.trigger("slid")},0)})}else{if(this.$element.trigger(d),d.isDefaultPrevented())return;e.removeClass("active"),f.addClass("active"),this.sliding=!1,this.$element.trigger("slid")}return g&&this.cycle(),this}}};var c=a.fn.carousel;a.fn.carousel=function(c){return this.each(function(){var d=a(this),e=d.data("carousel"),f=a.extend({},a.fn.carousel.defaults,"object"==typeof c&&c),g="string"==typeof c?c:f.slide;e||d.data("carousel",e=new b(this,f)),"number"==typeof c?e.to(c):g?e[g]():f.interval&&e.pause().cycle()})},a.fn.carousel.defaults={interval:5e3,pause:"hover"},a.fn.carousel.Constructor=b,a.fn.carousel.noConflict=function(){return a.fn.carousel=c,this},a(document).on("click.carousel.data-api","[data-slide], [data-slide-to]",function(b){var c,d,e=a(this),f=a(e.attr("data-target")||(c=e.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"")),g=a.extend({},f.data(),e.data());f.carousel(g),(d=e.attr("data-slide-to"))&&f.data("carousel").pause().to(d).cycle(),b.preventDefault()})}(window.jQuery),!function(a){"use strict";var b=function(b,c){this.$element=a(b),this.options=a.extend({},a.fn.collapse.defaults,c),this.options.parent&&(this.$parent=a(this.options.parent)),this.options.toggle&&this.toggle()};b.prototype={constructor:b,dimension:function(){var a=this.$element.hasClass("width");return a?"width":"height"},show:function(){var b,c,d,e;if(!this.transitioning&&!this.$element.hasClass("in")){if(b=this.dimension(),c=a.camelCase(["scroll",b].join("-")),d=this.$parent&&this.$parent.find("> .accordion-group > .in"),d&&d.length){if(e=d.data("collapse"),e&&e.transitioning)return;d.collapse("hide"),e||d.data("collapse",null)}this.$element[b](0),this.transition("addClass",a.Event("show"),"shown"),a.support.transition&&this.$element[b](this.$element[0][c])}},hide:function(){var b;!this.transitioning&&this.$element.hasClass("in")&&(b=this.dimension(),this.reset(this.$element[b]()),this.transition("removeClass",a.Event("hide"),"hidden"),this.$element[b](0))},reset:function(a){var b=this.dimension();return this.$element.removeClass("collapse")[b](a||"auto")[0].offsetWidth,this.$element[null!==a?"addClass":"removeClass"]("collapse"),this},transition:function(b,c,d){var e=this,f=function(){"show"==c.type&&e.reset(),e.transitioning=0,e.$element.trigger(d)};this.$element.trigger(c),c.isDefaultPrevented()||(this.transitioning=1,this.$element[b]("in"),a.support.transition&&this.$element.hasClass("collapse")?this.$element.one(a.support.transition.end,f):f())},toggle:function(){this[this.$element.hasClass("in")?"hide":"show"]()}};var c=a.fn.collapse;a.fn.collapse=function(c){return this.each(function(){var d=a(this),e=d.data("collapse"),f=a.extend({},a.fn.collapse.defaults,d.data(),"object"==typeof c&&c);e||d.data("collapse",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.collapse.defaults={toggle:!0},a.fn.collapse.Constructor=b,a.fn.collapse.noConflict=function(){return a.fn.collapse=c,this},a(document).on("click.collapse.data-api","[data-toggle=collapse]",function(b){var c,d=a(this),e=d.attr("data-target")||b.preventDefault()||(c=d.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,""),f=a(e).data("collapse")?"toggle":d.data();d[a(e).hasClass("in")?"addClass":"removeClass"]("collapsed"),a(e).collapse(f)})}(window.jQuery),!function(a){"use strict";function b(){a(d).each(function(){c(a(this)).removeClass("open")})}function c(b){var c,d=b.attr("data-target");return d||(d=b.attr("href"),d=d&&/#/.test(d)&&d.replace(/.*(?=#[^\s]*$)/,"")),c=d&&a(d),c&&c.length||(c=b.parent()),c}var d="[data-toggle=dropdown]",e=function(b){var c=a(b).on("click.dropdown.data-api",this.toggle);a("html").on("click.dropdown.data-api",function(){c.parent().removeClass("open")})};e.prototype={constructor:e,toggle:function(){var d,e,f=a(this);if(!f.is(".disabled, :disabled"))return d=c(f),e=d.hasClass("open"),b(),e||d.toggleClass("open"),f.focus(),!1},keydown:function(b){var e,f,g,h,i;if(/(38|40|27)/.test(b.keyCode)&&(e=a(this),b.preventDefault(),b.stopPropagation(),!e.is(".disabled, :disabled"))){if(g=c(e),h=g.hasClass("open"),!h||h&&27==b.keyCode)return 27==b.which&&g.find(d).focus(),e.click();f=a("[role=menu] li:not(.divider):visible a",g),f.length&&(i=f.index(f.filter(":focus")),38==b.keyCode&&i>0&&i--,40==b.keyCode&&i').appendTo(document.body),this.$backdrop.click("static"==this.options.backdrop?a.proxy(this.$element[0].focus,this.$element[0]):a.proxy(this.hide,this)),d&&this.$backdrop[0].offsetWidth,this.$backdrop.addClass("in"),!b)return;d?this.$backdrop.one(a.support.transition.end,b):b()}else!this.isShown&&this.$backdrop?(this.$backdrop.removeClass("in"),a.support.transition&&this.$element.hasClass("fade")?this.$backdrop.one(a.support.transition.end,b):b()):b&&b()}};var c=a.fn.modal;a.fn.modal=function(c){return this.each(function(){var d=a(this),e=d.data("modal"),f=a.extend({},a.fn.modal.defaults,d.data(),"object"==typeof c&&c);e||d.data("modal",e=new b(this,f)),"string"==typeof c?e[c]():f.show&&e.show()})},a.fn.modal.defaults={backdrop:!0,keyboard:!0,show:!0},a.fn.modal.Constructor=b,a.fn.modal.noConflict=function(){return a.fn.modal=c,this},a(document).on("click.modal.data-api",'[data-toggle="modal"]',function(b){var c=a(this),d=c.attr("href"),e=a(c.attr("data-target")||d&&d.replace(/.*(?=#[^\s]+$)/,"")),f=e.data("modal")?"toggle":a.extend({remote:!/#/.test(d)&&d},e.data(),c.data());b.preventDefault(),e.modal(f).one("hide",function(){c.focus()})})}(window.jQuery),!function(a){"use strict";var b=function(a,b){this.init("tooltip",a,b)};b.prototype={constructor:b,init:function(b,c,d){var e,f,g,h,i;for(this.type=b,this.$element=a(c),this.options=this.getOptions(d),this.enabled=!0,g=this.options.trigger.split(" "),i=g.length;i--;)h=g[i],"click"==h?this.$element.on("click."+this.type,this.options.selector,a.proxy(this.toggle,this)):"manual"!=h&&(e="hover"==h?"mouseenter":"focus",f="hover"==h?"mouseleave":"blur",this.$element.on(e+"."+this.type,this.options.selector,a.proxy(this.enter,this)),this.$element.on(f+"."+this.type,this.options.selector,a.proxy(this.leave,this)));this.options.selector?this._options=a.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},getOptions:function(b){return b=a.extend({},a.fn[this.type].defaults,this.$element.data(),b),b.delay&&"number"==typeof b.delay&&(b.delay={show:b.delay,hide:b.delay}),b},enter:function(b){var c,d=a.fn[this.type].defaults,e={};return this._options&&a.each(this._options,function(a,b){d[a]!=b&&(e[a]=b)},this),c=a(b.currentTarget)[this.type](e).data(this.type),c.options.delay&&c.options.delay.show?(clearTimeout(this.timeout),c.hoverState="in",void(this.timeout=setTimeout(function(){"in"==c.hoverState&&c.show()},c.options.delay.show))):c.show()},leave:function(b){var c=a(b.currentTarget)[this.type](this._options).data(this.type);return this.timeout&&clearTimeout(this.timeout),c.options.delay&&c.options.delay.hide?(c.hoverState="out",void(this.timeout=setTimeout(function(){"out"==c.hoverState&&c.hide()},c.options.delay.hide))):c.hide()},show:function(){var b,c,d,e,f,g,h=a.Event("show");if(this.hasContent()&&this.enabled){if(this.$element.trigger(h),h.isDefaultPrevented())return;switch(b=this.tip(),this.setContent(),this.options.animation&&b.addClass("fade"),f="function"==typeof this.options.placement?this.options.placement.call(this,b[0],this.$element[0]):this.options.placement,b.detach().css({top:0,left:0,display:"block"}),this.options.container?b.appendTo(this.options.container):b.insertAfter(this.$element),c=this.getPosition(),d=b[0].offsetWidth,e=b[0].offsetHeight,f){case"bottom":g={top:c.top+c.height,left:c.left+c.width/2-d/2};break;case"top":g={top:c.top-e,left:c.left+c.width/2-d/2};break;case"left":g={top:c.top+c.height/2-e/2,left:c.left-d};break;case"right":g={top:c.top+c.height/2-e/2,left:c.left+c.width}}this.applyPlacement(g,f),this.$element.trigger("shown")}},applyPlacement:function(a,b){var c,d,e,f,g=this.tip(),h=g[0].offsetWidth,i=g[0].offsetHeight;g.offset(a).addClass(b).addClass("in"),c=g[0].offsetWidth,d=g[0].offsetHeight,"top"==b&&d!=i&&(a.top=a.top+i-d,f=!0),"bottom"==b||"top"==b?(e=0,a.left<0&&(e=-2*a.left,a.left=0,g.offset(a),c=g[0].offsetWidth,d=g[0].offsetHeight),this.replaceArrow(e-h+c,c,"left")):this.replaceArrow(d-i,d,"top"),f&&g.offset(a)},replaceArrow:function(a,b,c){this.arrow().css(c,a?50*(1-a/b)+"%":"")},setContent:function(){var a=this.tip(),b=this.getTitle();a.find(".tooltip-inner")[this.options.html?"html":"text"](b),a.removeClass("fade in top bottom left right")},hide:function(){function b(){var b=setTimeout(function(){c.off(a.support.transition.end).detach()},500);c.one(a.support.transition.end,function(){clearTimeout(b),c.detach()})}var c=this.tip(),d=a.Event("hide");return this.$element.trigger(d),d.isDefaultPrevented()?void 0:(c.removeClass("in"),a.support.transition&&this.$tip.hasClass("fade")?b():c.detach(),this.$element.trigger("hidden"),this)},fixTitle:function(){var a=this.$element;(a.attr("title")||"string"!=typeof a.attr("data-original-title"))&&a.attr("data-original-title",a.attr("title")||"").attr("title","")},hasContent:function(){return this.getTitle()},getPosition:function(){var b=this.$element[0];return a.extend({},"function"==typeof b.getBoundingClientRect?b.getBoundingClientRect():{width:b.offsetWidth,height:b.offsetHeight},this.$element.offset())},getTitle:function(){var a,b=this.$element,c=this.options;return a=b.attr("data-original-title")||("function"==typeof c.title?c.title.call(b[0]):c.title)},tip:function(){return this.$tip=this.$tip||a(this.options.template)},arrow:function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},validate:function(){this.$element[0].parentNode||(this.hide(),this.$element=null,this.options=null)},enable:function(){this.enabled=!0},disable:function(){this.enabled=!1},toggleEnabled:function(){this.enabled=!this.enabled},toggle:function(b){var c=b?a(b.currentTarget)[this.type](this._options).data(this.type):this;c.tip().hasClass("in")?c.hide():c.show()},destroy:function(){this.hide().$element.off("."+this.type).removeData(this.type)}};var c=a.fn.tooltip;a.fn.tooltip=function(c){return this.each(function(){var d=a(this),e=d.data("tooltip"),f="object"==typeof c&&c;e||d.data("tooltip",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.tooltip.Constructor=b,a.fn.tooltip.defaults={animation:!0,placement:"top",selector:!1,template:'
    ',trigger:"hover focus",title:"",delay:0,html:!1,container:!1},a.fn.tooltip.noConflict=function(){return a.fn.tooltip=c,this}}(window.jQuery),!function(a){"use strict";var b=function(a,b){this.init("popover",a,b)};b.prototype=a.extend({},a.fn.tooltip.Constructor.prototype,{constructor:b,setContent:function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content")[this.options.html?"html":"text"](c),a.removeClass("fade top bottom left right in")},hasContent:function(){return this.getTitle()||this.getContent()},getContent:function(){var a,b=this.$element,c=this.options;return a=("function"==typeof c.content?c.content.call(b[0]):c.content)||b.attr("data-content")},tip:function(){return this.$tip||(this.$tip=a(this.options.template)),this.$tip},destroy:function(){this.hide().$element.off("."+this.type).removeData(this.type)}});var c=a.fn.popover;a.fn.popover=function(c){return this.each(function(){var d=a(this),e=d.data("popover"),f="object"==typeof c&&c;e||d.data("popover",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.popover.Constructor=b,a.fn.popover.defaults=a.extend({},a.fn.tooltip.defaults,{placement:"right",trigger:"click",content:"",template:'

    '}),a.fn.popover.noConflict=function(){return a.fn.popover=c,this}}(window.jQuery),!function(a){"use strict";function b(b,c){var d,e=a.proxy(this.process,this),f=a(a(b).is("body")?window:b);this.options=a.extend({},a.fn.scrollspy.defaults,c),this.$scrollElement=f.on("scroll.scroll-spy.data-api",e),this.selector=(this.options.target||(d=a(b).attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,"")||"")+" .nav li > a",this.$body=a("body"),this.refresh(),this.process()}b.prototype={constructor:b,refresh:function(){var b,c=this;this.offsets=a([]),this.targets=a([]),b=this.$body.find(this.selector).map(function(){var b=a(this),d=b.data("target")||b.attr("href"),e=/^#\w/.test(d)&&a(d);return e&&e.length&&[[e.position().top+(!a.isWindow(c.$scrollElement.get(0))&&c.$scrollElement.scrollTop()),d]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){c.offsets.push(this[0]),c.targets.push(this[1])})},process:function(){var a,b=this.$scrollElement.scrollTop()+this.options.offset,c=this.$scrollElement[0].scrollHeight||this.$body[0].scrollHeight,d=c-this.$scrollElement.height(),e=this.offsets,f=this.targets,g=this.activeTarget;if(b>=d)return g!=(a=f.last()[0])&&this.activate(a);for(a=e.length;a--;)g!=f[a]&&b>=e[a]&&(!e[a+1]||b<=e[a+1])&&this.activate(f[a])},activate:function(b){var c,d;this.activeTarget=b,a(this.selector).parent(".active").removeClass("active"),d=this.selector+'[data-target="'+b+'"],'+this.selector+'[href="'+b+'"]',c=a(d).parent("li").addClass("active"),c.parent(".dropdown-menu").length&&(c=c.closest("li.dropdown").addClass("active")),c.trigger("activate")}};var c=a.fn.scrollspy;a.fn.scrollspy=function(c){return this.each(function(){var d=a(this),e=d.data("scrollspy"),f="object"==typeof c&&c;e||d.data("scrollspy",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.scrollspy.Constructor=b,a.fn.scrollspy.defaults={offset:10},a.fn.scrollspy.noConflict=function(){return a.fn.scrollspy=c,this},a(window).on("load",function(){a('[data-spy="scroll"]').each(function(){var b=a(this);b.scrollspy(b.data())})})}(window.jQuery),!function(a){"use strict";var b=function(b){this.element=a(b)};b.prototype={constructor:b,show:function(){var b,c,d,e=this.element,f=e.closest("ul:not(.dropdown-menu)"),g=e.attr("data-target");g||(g=e.attr("href"),g=g&&g.replace(/.*(?=#[^\s]*$)/,"")),e.parent("li").hasClass("active")||(b=f.find(".active:last a")[0],d=a.Event("show",{relatedTarget:b}),e.trigger(d),d.isDefaultPrevented()||(c=a(g),this.activate(e.parent("li"),f),this.activate(c,c.parent(),function(){e.trigger({type:"shown",relatedTarget:b})})))},activate:function(b,c,d){function e(){f.removeClass("active").find("> .dropdown-menu > .active").removeClass("active"),b.addClass("active"),g?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu")&&b.closest("li.dropdown").addClass("active"),d&&d()}var f=c.find("> .active"),g=d&&a.support.transition&&f.hasClass("fade");g?f.one(a.support.transition.end,e):e(),f.removeClass("in")}};var c=a.fn.tab;a.fn.tab=function(c){return this.each(function(){var d=a(this),e=d.data("tab");e||d.data("tab",e=new b(this)),"string"==typeof c&&e[c]()})},a.fn.tab.Constructor=b,a.fn.tab.noConflict=function(){return a.fn.tab=c,this},a(document).on("click.tab.data-api",'[data-toggle="tab"], [data-toggle="pill"]',function(b){b.preventDefault(),a(this).tab("show")})}(window.jQuery),!function(a){"use strict";var b=function(b,c){this.$element=a(b),this.options=a.extend({},a.fn.typeahead.defaults,c),this.matcher=this.options.matcher||this.matcher,this.sorter=this.options.sorter||this.sorter,this.highlighter=this.options.highlighter||this.highlighter,this.updater=this.options.updater||this.updater,this.source=this.options.source,this.$menu=a(this.options.menu),this.shown=!1,this.listen()};b.prototype={constructor:b,select:function(){var a=this.$menu.find(".active").attr("data-value");return this.$element.val(this.updater(a)).change(),this.hide()},updater:function(a){return a},show:function(){var b=a.extend({},this.$element.position(),{height:this.$element[0].offsetHeight});return this.$menu.insertAfter(this.$element).css({top:b.top+b.height,left:b.left}).show(),this.shown=!0,this},hide:function(){return this.$menu.hide(),this.shown=!1,this},lookup:function(){var b;return this.query=this.$element.val(),!this.query||this.query.length"+b+""})},render:function(b){var c=this;return b=a(b).map(function(b,d){return b=a(c.options.item).attr("data-value",d),b.find("a").html(c.highlighter(d)),b[0]}),b.first().addClass("active"),this.$menu.html(b),this},next:function(){var b=this.$menu.find(".active").removeClass("active"),c=b.next();c.length||(c=a(this.$menu.find("li")[0])),c.addClass("active")},prev:function(){var a=this.$menu.find(".active").removeClass("active"),b=a.prev();b.length||(b=this.$menu.find("li").last()),b.addClass("active")},listen:function(){this.$element.on("focus",a.proxy(this.focus,this)).on("blur",a.proxy(this.blur,this)).on("keypress",a.proxy(this.keypress,this)).on("keyup",a.proxy(this.keyup,this)),this.eventSupported("keydown")&&this.$element.on("keydown",a.proxy(this.keydown,this)),this.$menu.on("click",a.proxy(this.click,this)).on("mouseenter","li",a.proxy(this.mouseenter,this)).on("mouseleave","li",a.proxy(this.mouseleave,this))},eventSupported:function(a){var b=a in this.$element;return b||(this.$element.setAttribute(a,"return;"),b="function"==typeof this.$element[a]),b},move:function(a){if(this.shown){switch(a.keyCode){case 9:case 13:case 27:a.preventDefault();break;case 38:a.preventDefault(),this.prev();break;case 40:a.preventDefault(),this.next()}a.stopPropagation()}},keydown:function(b){this.suppressKeyPressRepeat=~a.inArray(b.keyCode,[40,38,9,13,27]),this.move(b)},keypress:function(a){this.suppressKeyPressRepeat||this.move(a)},keyup:function(a){switch(a.keyCode){case 40:case 38:case 16:case 17:case 18:break;case 9:case 13:if(!this.shown)return;this.select();break;case 27:if(!this.shown)return;this.hide();break;default:this.lookup()}a.stopPropagation(),a.preventDefault()},focus:function(){this.focused=!0},blur:function(){this.focused=!1,!this.mousedover&&this.shown&&this.hide()},click:function(a){a.stopPropagation(),a.preventDefault(),this.select(),this.$element.focus()},mouseenter:function(b){this.mousedover=!0,this.$menu.find(".active").removeClass("active"),a(b.currentTarget).addClass("active")},mouseleave:function(){this.mousedover=!1,!this.focused&&this.shown&&this.hide()}};var c=a.fn.typeahead;a.fn.typeahead=function(c){return this.each(function(){var d=a(this),e=d.data("typeahead"),f="object"==typeof c&&c;e||d.data("typeahead",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.typeahead.defaults={source:[],items:8,menu:'',item:'
  • ',minLength:1},a.fn.typeahead.Constructor=b,a.fn.typeahead.noConflict=function(){return a.fn.typeahead=c,this},a(document).on("focus.typeahead.data-api",'[data-provide="typeahead"]',function(){var b=a(this);b.data("typeahead")||b.typeahead(b.data())})}(window.jQuery),!function(a){"use strict";var b=function(b,c){this.options=a.extend({},a.fn.affix.defaults,c),this.$window=a(window).on("scroll.affix.data-api",a.proxy(this.checkPosition,this)).on("click.affix.data-api",a.proxy(function(){setTimeout(a.proxy(this.checkPosition,this),1)},this)),this.$element=a(b),this.checkPosition()};b.prototype.checkPosition=function(){if(this.$element.is(":visible")){var b,c=a(document).height(),d=this.$window.scrollTop(),e=this.$element.offset(),f=this.options.offset,g=f.bottom,h=f.top,i="affix affix-top affix-bottom";"object"!=typeof f&&(g=h=f),"function"==typeof h&&(h=f.top()),"function"==typeof g&&(g=f.bottom()),b=null!=this.unpin&&d+this.unpin<=e.top?!1:null!=g&&e.top+this.$element.height()>=c-g?"bottom":null!=h&&h>=d?"top":!1,this.affixed!==b&&(this.affixed=b,this.unpin="bottom"==b?e.top-d:null,this.$element.removeClass(i).addClass("affix"+(b?"-"+b:"")))}};var c=a.fn.affix;a.fn.affix=function(c){return this.each(function(){var d=a(this),e=d.data("affix"),f="object"==typeof c&&c;e||d.data("affix",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.affix.Constructor=b,a.fn.affix.defaults={offset:0},a.fn.affix.noConflict=function(){return a.fn.affix=c,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var b=a(this),c=b.data();c.offset=c.offset||{},c.offsetBottom&&(c.offset.bottom=c.offsetBottom),c.offsetTop&&(c.offset.top=c.offsetTop),b.affix(c)})})}(window.jQuery); \ No newline at end of file diff -Nru eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/vendor/modernizr.js eoxserver-0.3.2/eoxserver/webclient/static/scripts/vendor/modernizr.js --- eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/vendor/modernizr.js 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/webclient/static/scripts/vendor/modernizr.js 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -window.Modernizr=function(a,b,c){function d(a){t.cssText=a}function e(a,b){return d(x.join(a+";")+(b||""))}function f(a,b){return typeof a===b}function g(a,b){return!!~(""+a).indexOf(b)}function h(a,b){for(var d in a){var e=a[d];if(!g(e,"-")&&t[e]!==c)return"pfx"==b?e:!0}return!1}function i(a,b,d){for(var e in a){var g=b[a[e]];if(g!==c)return d===!1?a[e]:f(g,"function")?g.bind(d||b):g}return!1}function j(a,b,c){var d=a.charAt(0).toUpperCase()+a.slice(1),e=(a+" "+z.join(d+" ")+d).split(" ");return f(b,"string")||f(b,"undefined")?h(e,b):(e=(a+" "+A.join(d+" ")+d).split(" "),i(e,b,c))}function k(){o.input=function(c){for(var d=0,e=c.length;e>d;d++)E[c[d]]=!!(c[d]in u);return E.list&&(E.list=!(!b.createElement("datalist")||!a.HTMLDataListElement)),E}("autocomplete autofocus list placeholder max min multiple pattern required step".split(" ")),o.inputtypes=function(a){for(var d,e,f,g=0,h=a.length;h>g;g++)u.setAttribute("type",e=a[g]),d="text"!==u.type,d&&(u.value=v,u.style.cssText="position:absolute;visibility:hidden;",/^range$/.test(e)&&u.style.WebkitAppearance!==c?(q.appendChild(u),f=b.defaultView,d=f.getComputedStyle&&"textfield"!==f.getComputedStyle(u,null).WebkitAppearance&&0!==u.offsetHeight,q.removeChild(u)):/^(search|tel)$/.test(e)||(d=/^(url|email)$/.test(e)?u.checkValidity&&u.checkValidity()===!1:u.value!=v)),D[a[g]]=!!d;return D}("search tel url email datetime date month week time datetime-local number range color".split(" "))}var l,m,n="2.6.3",o={},p=!0,q=b.documentElement,r="modernizr",s=b.createElement(r),t=s.style,u=b.createElement("input"),v=":)",w={}.toString,x=" -webkit- -moz- -o- -ms- ".split(" "),y="Webkit Moz O ms",z=y.split(" "),A=y.toLowerCase().split(" "),B={svg:"http://www.w3.org/2000/svg"},C={},D={},E={},F=[],G=F.slice,H=function(a,c,d,e){var f,g,h,i,j=b.createElement("div"),k=b.body,l=k||b.createElement("body");if(parseInt(d,10))for(;d--;)h=b.createElement("div"),h.id=e?e[d]:r+(d+1),j.appendChild(h);return f=["­",'"].join(""),j.id=r,(k?j:l).innerHTML+=f,l.appendChild(j),k||(l.style.background="",l.style.overflow="hidden",i=q.style.overflow,q.style.overflow="hidden",q.appendChild(l)),g=c(j,a),k?j.parentNode.removeChild(j):(l.parentNode.removeChild(l),q.style.overflow=i),!!g},I=function(b){var c=a.matchMedia||a.msMatchMedia;if(c)return c(b).matches;var d;return H("@media "+b+" { #"+r+" { position: absolute; } }",function(b){d="absolute"==(a.getComputedStyle?getComputedStyle(b,null):b.currentStyle).position}),d},J=function(){function a(a,e){e=e||b.createElement(d[a]||"div"),a="on"+a;var g=a in e;return g||(e.setAttribute||(e=b.createElement("div")),e.setAttribute&&e.removeAttribute&&(e.setAttribute(a,""),g=f(e[a],"function"),f(e[a],"undefined")||(e[a]=c),e.removeAttribute(a))),e=null,g}var d={select:"input",change:"input",submit:"form",reset:"form",error:"img",load:"img",abort:"img"};return a}(),K={}.hasOwnProperty;m=f(K,"undefined")||f(K.call,"undefined")?function(a,b){return b in a&&f(a.constructor.prototype[b],"undefined")}:function(a,b){return K.call(a,b)},Function.prototype.bind||(Function.prototype.bind=function(a){var b=this;if("function"!=typeof b)throw new TypeError;var c=G.call(arguments,1),d=function(){if(this instanceof d){var e=function(){};e.prototype=b.prototype;var f=new e,g=b.apply(f,c.concat(G.call(arguments)));return Object(g)===g?g:f}return b.apply(a,c.concat(G.call(arguments)))};return d}),C.flexbox=function(){return j("flexWrap")},C.flexboxlegacy=function(){return j("boxDirection")},C.canvas=function(){var a=b.createElement("canvas");return!(!a.getContext||!a.getContext("2d"))},C.canvastext=function(){return!(!o.canvas||!f(b.createElement("canvas").getContext("2d").fillText,"function"))},C.webgl=function(){return!!a.WebGLRenderingContext},C.touch=function(){var c;return"ontouchstart"in a||a.DocumentTouch&&b instanceof DocumentTouch?c=!0:H(["@media (",x.join("touch-enabled),("),r,")","{#modernizr{top:9px;position:absolute}}"].join(""),function(a){c=9===a.offsetTop}),c},C.geolocation=function(){return"geolocation"in navigator},C.postmessage=function(){return!!a.postMessage},C.websqldatabase=function(){return!!a.openDatabase},C.indexedDB=function(){return!!j("indexedDB",a)},C.hashchange=function(){return J("hashchange",a)&&(b.documentMode===c||b.documentMode>7)},C.history=function(){return!(!a.history||!history.pushState)},C.draganddrop=function(){var a=b.createElement("div");return"draggable"in a||"ondragstart"in a&&"ondrop"in a},C.websockets=function(){return"WebSocket"in a||"MozWebSocket"in a},C.rgba=function(){return d("background-color:rgba(150,255,150,.5)"),g(t.backgroundColor,"rgba")},C.hsla=function(){return d("background-color:hsla(120,40%,100%,.5)"),g(t.backgroundColor,"rgba")||g(t.backgroundColor,"hsla")},C.multiplebgs=function(){return d("background:url(https://),url(https://),red url(https://)"),/(url\s*\(.*?){3}/.test(t.background)},C.backgroundsize=function(){return j("backgroundSize")},C.borderimage=function(){return j("borderImage")},C.borderradius=function(){return j("borderRadius")},C.boxshadow=function(){return j("boxShadow")},C.textshadow=function(){return""===b.createElement("div").style.textShadow},C.opacity=function(){return e("opacity:.55"),/^0.55$/.test(t.opacity)},C.cssanimations=function(){return j("animationName")},C.csscolumns=function(){return j("columnCount")},C.cssgradients=function(){var a="background-image:",b="gradient(linear,left top,right bottom,from(#9f9),to(white));",c="linear-gradient(left top,#9f9, white);";return d((a+"-webkit- ".split(" ").join(b+a)+x.join(c+a)).slice(0,-a.length)),g(t.backgroundImage,"gradient")},C.cssreflections=function(){return j("boxReflect")},C.csstransforms=function(){return!!j("transform")},C.csstransforms3d=function(){var a=!!j("perspective");return a&&"webkitPerspective"in q.style&&H("@media (transform-3d),(-webkit-transform-3d){#modernizr{left:9px;position:absolute;height:3px;}}",function(b){a=9===b.offsetLeft&&3===b.offsetHeight}),a},C.csstransitions=function(){return j("transition")},C.fontface=function(){var a;return H('@font-face {font-family:"font";src:url("https://")}',function(c,d){var e=b.getElementById("smodernizr"),f=e.sheet||e.styleSheet,g=f?f.cssRules&&f.cssRules[0]?f.cssRules[0].cssText:f.cssText||"":"";a=/src/i.test(g)&&0===g.indexOf(d.split(" ")[0])}),a},C.generatedcontent=function(){var a;return H(["#",r,"{font:0/0 a}#",r,':after{content:"',v,'";visibility:hidden;font:3px/1 a}'].join(""),function(b){a=b.offsetHeight>=3}),a},C.video=function(){var a=b.createElement("video"),c=!1;try{(c=!!a.canPlayType)&&(c=new Boolean(c),c.ogg=a.canPlayType('video/ogg; codecs="theora"').replace(/^no$/,""),c.h264=a.canPlayType('video/mp4; codecs="avc1.42E01E"').replace(/^no$/,""),c.webm=a.canPlayType('video/webm; codecs="vp8, vorbis"').replace(/^no$/,""))}catch(d){}return c},C.audio=function(){var a=b.createElement("audio"),c=!1;try{(c=!!a.canPlayType)&&(c=new Boolean(c),c.ogg=a.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/,""),c.mp3=a.canPlayType("audio/mpeg;").replace(/^no$/,""),c.wav=a.canPlayType('audio/wav; codecs="1"').replace(/^no$/,""),c.m4a=(a.canPlayType("audio/x-m4a;")||a.canPlayType("audio/aac;")).replace(/^no$/,""))}catch(d){}return c},C.localstorage=function(){try{return localStorage.setItem(r,r),localStorage.removeItem(r),!0}catch(a){return!1}},C.sessionstorage=function(){try{return sessionStorage.setItem(r,r),sessionStorage.removeItem(r),!0}catch(a){return!1}},C.webworkers=function(){return!!a.Worker},C.applicationcache=function(){return!!a.applicationCache},C.svg=function(){return!!b.createElementNS&&!!b.createElementNS(B.svg,"svg").createSVGRect},C.inlinesvg=function(){var a=b.createElement("div");return a.innerHTML="",(a.firstChild&&a.firstChild.namespaceURI)==B.svg},C.smil=function(){return!!b.createElementNS&&/SVGAnimate/.test(w.call(b.createElementNS(B.svg,"animate")))},C.svgclippaths=function(){return!!b.createElementNS&&/SVGClipPath/.test(w.call(b.createElementNS(B.svg,"clipPath")))};for(var L in C)m(C,L)&&(l=L.toLowerCase(),o[l]=C[L](),F.push((o[l]?"":"no-")+l));return o.input||k(),o.addTest=function(a,b){if("object"==typeof a)for(var d in a)m(a,d)&&o.addTest(d,a[d]);else{if(a=a.toLowerCase(),o[a]!==c)return o;b="function"==typeof b?b():b,"undefined"!=typeof p&&p&&(q.className+=" "+(b?"":"no-")+a),o[a]=b}return o},d(""),s=u=null,function(a,b){function c(a,b){var c=a.createElement("p"),d=a.getElementsByTagName("head")[0]||a.documentElement;return c.innerHTML="x",d.insertBefore(c.lastChild,d.firstChild)}function d(){var a=r.elements;return"string"==typeof a?a.split(" "):a}function e(a){var b=q[a[o]];return b||(b={},p++,a[o]=p,q[p]=b),b}function f(a,c,d){if(c||(c=b),k)return c.createElement(a);d||(d=e(c));var f;return f=d.cache[a]?d.cache[a].cloneNode():n.test(a)?(d.cache[a]=d.createElem(a)).cloneNode():d.createElem(a),f.canHaveChildren&&!m.test(a)?d.frag.appendChild(f):f}function g(a,c){if(a||(a=b),k)return a.createDocumentFragment();c=c||e(a);for(var f=c.frag.cloneNode(),g=0,h=d(),i=h.length;i>g;g++)f.createElement(h[g]);return f}function h(a,b){b.cache||(b.cache={},b.createElem=a.createElement,b.createFrag=a.createDocumentFragment,b.frag=b.createFrag()),a.createElement=function(c){return r.shivMethods?f(c,a,b):b.createElem(c)},a.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+d().join().replace(/\w+/g,function(a){return b.createElem(a),b.frag.createElement(a),'c("'+a+'")'})+");return n}")(r,b.frag)}function i(a){a||(a=b);var d=e(a);return!r.shivCSS||j||d.hasCSS||(d.hasCSS=!!c(a,"article,aside,figcaption,figure,footer,header,hgroup,nav,section{display:block}mark{background:#FF0;color:#000}")),k||h(a,d),a}var j,k,l=a.html5||{},m=/^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i,n=/^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i,o="_html5shiv",p=0,q={};!function(){try{var a=b.createElement("a");a.innerHTML="",j="hidden"in a,k=1==a.childNodes.length||function(){b.createElement("a");var a=b.createDocumentFragment();return"undefined"==typeof a.cloneNode||"undefined"==typeof a.createDocumentFragment||"undefined"==typeof a.createElement}()}catch(c){j=!0,k=!0}}();var r={elements:l.elements||"abbr article aside audio bdi canvas data datalist details figcaption figure footer header hgroup mark meter nav output progress section summary time video",shivCSS:l.shivCSS!==!1,supportsUnknownElements:k,shivMethods:l.shivMethods!==!1,type:"default",shivDocument:i,createElement:f,createDocumentFragment:g};a.html5=r,i(b)}(this,b),o._version=n,o._prefixes=x,o._domPrefixes=A,o._cssomPrefixes=z,o.mq=I,o.hasEvent=J,o.testProp=function(a){return h([a])},o.testAllProps=j,o.testStyles=H,o.prefixed=function(a,b,c){return b?j(a,b,c):j(a,"pfx")},q.className=q.className.replace(/(^|\s)no-js(\s|$)/,"$1$2")+(p?" js "+F.join(" "):""),o}(this,this.document); \ No newline at end of file diff -Nru eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/views/BaseLayerSelectionView.js eoxserver-0.3.2/eoxserver/webclient/static/scripts/views/BaseLayerSelectionView.js --- eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/views/BaseLayerSelectionView.js 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/webclient/static/scripts/views/BaseLayerSelectionView.js 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -(function(){"use strict";var a=this;a.define(["backbone","communicator","underscore"],function(a){var b=a.Marionette.CollectionView.extend({tagName:"ul",className:"radio",initialize:function(){},onShow:function(){}});return{BaseLayerSelectionView:b}})}).call(this); \ No newline at end of file diff -Nru eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/views/ContentView.js eoxserver-0.3.2/eoxserver/webclient/static/scripts/views/ContentView.js --- eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/views/ContentView.js 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/webclient/static/scripts/views/ContentView.js 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -(function(){"use strict";var a=this;a.define(["backbone","communicator","underscore"],function(a){var b=a.Marionette.ItemView.extend({initialize:function(){}});return{ContentView:b}})}).call(this); \ No newline at end of file diff -Nru eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/views/DownloadView.js eoxserver-0.3.2/eoxserver/webclient/static/scripts/views/DownloadView.js --- eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/views/DownloadView.js 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/webclient/static/scripts/views/DownloadView.js 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -(function(){"use strict";var a=Backbone.Collection.extend({fetch:function(a){return a||(a={}),a.dataType="xml",Backbone.Collection.prototype.fetch.call(this,a)},parse:function(a){return WCS.Core.Parse.parse(a).coverageDescriptions}}),b=this;b.define(["backbone","communicator","globals","models/DownloadModel","hbs!tmpl/Download","hbs!tmpl/SelectCoverageListItem","hbs!tmpl/CoverageInfo","hbs!tmpl/CoverageDownloadPost","underscore"],function(b,c,d,e,f,g,h,i){var j=b.Marionette.ItemView.extend({tagName:"div",id:"modal-start-download",className:"panel panel-default download",template:{type:"handlebars",template:f},modelEvents:{reset:"onCoveragesReset"},events:{"click #btn-select-all-coverages":"onSelectAllCoveragesClicked","click #btn-invert-coverage-selection":"onInvertCoverageSelectionClicked",'change input[type="checkbox"]':"onCoverageSelected","click #btn-start-download":"onStartDownloadClicked"},initialize:function(){this.coverages=new b.Collection([])},onShow:function(){this.listenTo(this.coverages,"reset",this.onCoveragesReset),this.$(".close").on("click",_.bind(this.onClose,this)),this.$el.draggable({containment:"#content",scroll:!1,handle:".panel-heading"});var b=this.$("#download-list");b.children().remove();var c=_.map(this.model.get("products"),function(b,c){var d=new a([]),e={};b.get("timeSlider")&&(e={subsetTime:[getISODateTimeString(this.model.get("ToI").start),getISODateTimeString(this.model.get("ToI").end)]}),e.subsetCRS="http://www.opengis.net/def/crs/EPSG/0/4326";var f=this.model.get("AoI").getExtent();return e.subsetX=[f[0],f[2]],e.subsetY=[f[1],f[3]],d.url=WCS.EO.KVP.describeEOCoverageSetURL(b.get("download").url,c,e),d},this),d=_.invoke(c,"fetch");$.when.apply($,d).done(_.bind(function(){_.each(c,function(a){a.each(function(b){b.set("url",a.url)})});var a=_.flatten(_.pluck(c,"models"));this.coverages.reset(a)},this))},onSelectAllCoveragesClicked:function(){this.$('input[type="checkbox"]').prop("checked",!0).trigger("change")},onInvertCoverageSelectionClicked:function(){this.$('input[type="checkbox"]').each(function(){var a=$(this);a.prop("checked",!a.is(":checked")).trigger("change")})},onCoveragesReset:function(){var a=this.$("#download-list");this.coverages.each(function(b){var c=b.toJSON(),d=$(g(c));a.append(d),d.find("i").popover({trigger:"hover",html:!0,content:h(c),title:"Coverage Description",placement:"bottom"})},this)},onCoverageSelected:function(){this.$("input:checked").length?this.$("#btn-start-download").removeAttr("disabled"):this.$("#btn-start-download").attr("disabled","disabled")},onStartDownloadClicked:function(){var a=$("#div-downloads"),b={},c=this.model.get("AoI").getExtent();b.subsetX=[c[0],c[2]],b.subsetY=[c[1],c[3]],b.format=this.$("#select-output-format").val(),b.outputCRS=this.$("#select-output-crs").val();var d=this.model.get("AoI").getCoordinates()[0];if(d.length>5){var e=[];_.each(d,function(a){e.push(a[0]),e.push(a[1])}),b.mask=e.join(" ")}this.$('input[type="checkbox"]').each(_.bind(function(c){if($('input[type="checkbox"]')[c].checked){var d=this.coverages.models[c],e=getCoverageXML(d.get("coverageId"),b),f=d.get("url").split("?")[0]+"?",g=$(i({url:f,xml:e}));a.append(g),_.delay(function(){g.submit()},1e3*c)}},this))},onClose:function(){c.mediator.trigger("ui:close","download"),this.close()}});return{DownloadView:j}})}).call(this); \ No newline at end of file diff -Nru eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/views/LayerItemView.js eoxserver-0.3.2/eoxserver/webclient/static/scripts/views/LayerItemView.js --- eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/views/LayerItemView.js 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/webclient/static/scripts/views/LayerItemView.js 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -(function(){"use strict";var a=this;a.define(["backbone","communicator","globals","hbs!tmpl/BulletLayer","underscore"],function(a,b,c){var d=a.Marionette.ItemView.extend({tagName:"li",events:{drop:"drop",change:"onChange","click .fa-adjust":"onOpenSlider","slide .ui-slider":"onOpacityAdjust"},initialize:function(){this.$slider=$("
    ").slider({range:"max",max:100,min:0}),this.$slider.width(100)},onShow:function(){$(".sortable").sortable({revert:!0,delay:90,containment:".layercontrol .panel-body",axis:"y",forceHelperSize:!0,forcePlaceHolderSize:!0,placeholder:"sortable-placeholder",handle:".fa-sort",start:function(){$(".ui-slider").detach(),$(".fa-adjust").toggleClass("active"),$(".fa-adjust").popover("hide")},stop:function(a,b){b.item.trigger("drop",b.item.index())}}),$(".fa-adjust").popover({trigger:"manual"})},onChange:function(a){var d=!1;this.model.get("view").isBaseLayer&&(d=!0);var e={id:this.model.get("view").id,isBaseLayer:d,visible:a.target.checked},f=c.products.find(function(a){return a.get("view").id==e.id});if(f)if(e.visible&&"WMS"==f.get("view").protocol){var g="?LAYERS="+f.get("view").id+"&TRANSPARENT=true&FORMAT=image%2Fpng&SERVICE=WMS&VERSION=1.1.0&REQUEST=GetMap&STYLES=&SRS=EPSG%3A4326&BBOX=33.75,56.25,39.375,61.875&WIDTH=256&HEIGHT=256&TIME=2014-02-19T00:00:00Z/2014-02-19T00:00:00Z";$.get(f.get("view").urls[0]+g,function(){b.mediator.trigger("map:layer:change",e)},"text")}else b.mediator.trigger("map:layer:change",e);else b.mediator.trigger("map:layer:change",e)},drop:function(a,c){b.mediator.trigger("productCollection:updateSort",{model:this.model,position:c})},onOpenSlider:function(){this.$(".fa-adjust").toggleClass("active").hasClass("active")?(this.$(".fa-adjust").popover("show"),this.$(".popover-content").empty().append(this.$slider),this.$(".ui-slider").slider("option","value",100*this.model.get("opacity"))):(this.$slider.detach(),this.$(".fa-adjust").popover("hide"))},onOpacityAdjust:function(a,c){this.model.set("opacity",c.value/100),b.mediator.trigger("productCollection:updateOpacity",{model:this.model,value:c.value/100})}});return{LayerItemView:d}})}).call(this); \ No newline at end of file diff -Nru eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/views/LayerSelectionView.js eoxserver-0.3.2/eoxserver/webclient/static/scripts/views/LayerSelectionView.js --- eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/views/LayerSelectionView.js 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/webclient/static/scripts/views/LayerSelectionView.js 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -(function(){"use strict";var a=this;a.define(["backbone","communicator","underscore"],function(a,b){var c=a.Marionette.CollectionView.extend({tagName:"ul",initialize:function(){},onShow:function(){this.listenTo(b.mediator,"productCollection:updateSort",this.updateSort),$(".sortable").sortable({revert:!0,stop:function(a,b){b.item.trigger("drop",b.item.index())}})},updateSort:function(a){this.collection.remove(a.model),this.collection.each(function(b,c){var d=c;c>=a.position&&(d+=1),b.set("ordinal",d)}),a.model.set("ordinal",a.position),this.collection.add(a.model,{at:a.position}),this.render(),b.mediator.trigger("productCollection:sortUpdated")}});return{LayerSelectionView:c}})}).call(this); \ No newline at end of file diff -Nru eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/views/MapView.js eoxserver-0.3.2/eoxserver/webclient/static/scripts/views/MapView.js --- eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/views/MapView.js 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/webclient/static/scripts/views/MapView.js 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -define(["backbone","communicator","globals","openlayers","models/MapModel","filesaver"],function(a,b,c){var d=a.View.extend({onShow:function(){var a=new ol.control.MousePosition({coordinateFormat:ol.coordinate.createStringXY(4),projection:"EPSG:4326",undefinedHTML:" "});this.geojson_format=new ol.format.GeoJSON,this.map=new ol.Map({controls:ol.control.defaults().extend([a]),renderer:"canvas",target:"map",view:new ol.View({center:[9,45],zoom:6,projection:ol.proj.get("EPSG:4326")})}),console.log("Created Map");var d=this;this.map.on("moveend",function(a){var c=a.map.getView(),e=c.getCenter();b.mediator.trigger("router:setUrl",{x:e[0],y:e[1],l:c.getZoom()}),b.mediator.trigger("map:position:change",d.onGetMapExtent())}),this.listenTo(b.mediator,"map:center",this.centerMap),this.listenTo(b.mediator,"map:layer:change",this.changeLayer),this.listenTo(b.mediator,"map:set:extent",this.onSetExtent),this.listenTo(b.mediator,"productCollection:sortUpdated",this.onSortProducts),this.listenTo(b.mediator,"productCollection:updateOpacity",this.onUpdateOpacity),this.listenTo(b.mediator,"selection:activated",this.onSelectionActivated),this.listenTo(b.mediator,"map:load:geojson",this.onLoadGeoJSON),this.listenTo(b.mediator,"map:export:geojson",this.onExportGeoJSON),this.listenTo(b.mediator,"time:change",this.onTimeChange),b.reqres.setHandler("map:get:extent",_.bind(this.onGetMapExtent,this)),b.reqres.setHandler("get:selection:json",_.bind(this.onGetGeoJSON,this));var e=new ol.style.Style({fill:new ol.style.Fill({color:"rgba(255, 255, 255, 0.2)"}),stroke:new ol.style.Stroke({color:"#ffcc33",width:2}),image:new ol.style.Circle({radius:7,fill:new ol.style.Fill({color:"#ffcc33"})})});return this.source=new ol.source.Vector,this.source.on("change",this.onDone),this.vector=new ol.layer.Vector({source:this.source,style:e}),this.boxstart=void 0,this.drawControls={pointSelection:new ol.interaction.Draw({source:this.source,type:"Point"}),lineSelection:new ol.interaction.Draw({source:this.source,type:"LineString"}),polygonSelection:new ol.interaction.Draw({source:this.source,type:"Polygon"}),bboxSelection:new ol.interaction.DragBox({style:e})},this.drawControls.bboxSelection.on("boxstart",function(a){this.boxstart=a.coordinate},this),this.drawControls.bboxSelection.on("boxend",function(a){var b=a.coordinate,c=new ol.geom.Polygon([[[this.boxstart[0],this.boxstart[1]],[this.boxstart[0],b[1]],[b[0],b[1]],[b[0],this.boxstart[1]]]]),d=new ol.Feature;d.setGeometry(c),this.source.addFeature(d)},this),this.baseLayerGroup=new ol.layer.Group({layers:c.baseLayers.map(function(a){return this.createLayer(a)},this)}),this.map.addLayer(this.baseLayerGroup),this.productLayerGroup=new ol.layer.Group({layers:c.products.map(function(a){return this.createLayer(a)},this)}),this.map.addLayer(this.productLayerGroup),c.products.each(function(a){if(a.get("visible")){var c={id:a.get("view").id,isBaseLayer:!1,visible:!0};b.mediator.trigger("map:layer:change",c)}}),this.overlayLayerGroup=new ol.layer.Group({layers:c.overlays.map(function(a){return this.createLayer(a)},this)}),this.map.addLayer(this.overlayLayerGroup),this.onSortProducts(),this.map.addLayer(this.vector),$(".ol-attribution").attr("class","ol-attribution"),this},createLayer:function(a){for(var b=null,c=a.get("view"),d=ol.proj.get("EPSG:4326"),e=d.getExtent(),f=ol.extent.getWidth(e)/256,g=new Array(18),h=new Array(18),i=0;18>i;++i)g[i]=f/Math.pow(2,i+1),h[i]=i;switch(c.protocol){case"WMTS":b=new ol.layer.Tile({visible:a.get("visible"),source:new ol.source.WMTS({urls:c.urls,layer:c.id,matrixSet:c.matrixSet,format:c.format,projection:c.projection,tileGrid:new ol.tilegrid.WMTS({origin:ol.extent.getTopLeft(e),resolutions:g,matrixIds:h}),style:c.style,attributions:[new ol.Attribution({html:c.attribution})]})});break;case"WMS":b=new ol.layer.Tile({visible:a.get("visible"),source:new ol.source.TileWMS({crossOrigin:"anonymous",params:{LAYERS:c.id,VERSION:"1.1.0",FORMAT:"image/png"},url:c.urls[0]}),attribution:c.attribution})}return b&&(b.id=c.id),b},centerMap:function(a){console.log(a),this.map.getView().setCenter([parseFloat(a.x),parseFloat(a.y)]),this.map.getView().setZoom(parseInt(a.l))},changeLayer:function(a){if(a.isBaseLayer)c.baseLayers.forEach(function(b){b.get("view").id==a.id?b.set("visible",!0):b.set("visible",!1)}),this.baseLayerGroup.getLayers().forEach(function(b){b.setVisible(b.id==a.id?!0:!1)});else{var b=c.products.find(function(b){return b.get("view").id==a.id});b?(b.set("visible",a.visible),this.productLayerGroup.getLayers().forEach(function(b){b.id==a.id&&b.setVisible(a.visible)})):(c.overlays.find(function(b){return b.get("view").id==a.id}).set("visible",a.visible),this.overlayLayerGroup.getLayers().forEach(function(b){b.id==a.id&&b.setVisible(a.visible)}))}},onSortProducts:function(){var a=this.productLayerGroup.getLayers(),b={};c.products.each(function(a,d){b[a.get("view").id]=c.products.length-(d+1)});var d=_.sortBy(a.getArray(),function(a){return b[a.id]});this.productLayerGroup.setLayers(new ol.Collection(d)),console.log("Map products sorted")},onUpdateOpacity:function(a){var b=a.model.get("view").id;this.productLayerGroup.getLayers().forEach(function(c){c.id==b&&c.setOpacity(a.value)})},onSelectionActivated:function(a){if(a.active)for(key in this.drawControls){var c=this.drawControls[key];if(a.id==key)this.map.addInteraction(c);else{this.map.removeInteraction(c);var d=this.source.getFeatures();for(var e in d)this.source.removeFeature(d[e]);b.mediator.trigger("selection:changed",null)}}else for(key in this.drawControls){var c=this.drawControls[key];this.map.removeInteraction(c);var d=this.source.getFeatures();for(var e in d)this.source.removeFeature(d[e]);b.mediator.trigger("selection:changed",null)}},onLoadGeoJSON:function(a){var b=this.source.getFeatures();for(var c in b)this.source.removeFeature(b[c]);var d,e=new ol.source.GeoJSON({object:a}),f=e.getFeatures();if(f){f.constructor!=Array&&(f=[f]);for(var c=0;c-1){var d=localStorage.getObject("selections");d&&(d.splice(c,1),console.log(d),localStorage.setObject("selections",d),this.renderList())}},onSelectionSelected:function(){var a=$("input[type=radio]:checked").val();b.mediator.trigger("map:load:geojson",localStorage.getObject("selections")[a].content)},onClose:function(){b.mediator.trigger("ui:close","selectionManager"),this.close()}});return{SelectionManagerView:e}})}).call(this); \ No newline at end of file diff -Nru eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/views/TimeSliderView.js eoxserver-0.3.2/eoxserver/webclient/static/scripts/views/TimeSliderView.js --- eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/views/TimeSliderView.js 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/webclient/static/scripts/views/TimeSliderView.js 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -(function(){"use strict";var a=this;a.define(["backbone","communicator","timeslider","timeslider_plugins","globals","underscore","d3"],function(a,b,c,d,e){var f=a.Marionette.ItemView.extend({id:"timeslider",events:{selectionChanged:"onChangeTime",coverageselected:"onCoverageSelected"},initialize:function(a){this.options=a,this.active_products=[]},render:function(){},onShow:function(){this.listenTo(b.mediator,"date:selection:change",this.onDateSelectionChange),this.listenTo(b.mediator,"map:layer:change",this.changeLayer),this.listenTo(b.mediator,"map:position:change",this.updateExtent),this.listenTo(b.mediator,"date:selection:change",this.onDateSelectionChange);var a=new Date(this.options.brush.start),c=new Date(this.options.brush.end);this.activeWPSproducts=[],this.slider=new TimeSlider(this.el,{domain:{start:new Date(this.options.domain.start),end:new Date(this.options.domain.end)},brush:{start:a,end:c},debounce:300,ticksize:8,datasets:[]}),this.slider.hide(),b.mediator.trigger("time:change",{start:a,end:c})},onChangeTime:function(a){b.mediator.trigger("time:change",a.originalEvent.detail)},onDateSelectionChange:function(a){this.slider.select(a.start,a.end)},changeLayer:function(a){if(!a.isBaseLayer){var c=e.products.find(function(b){return b.get("view").id==a.id});if(c)if(a.visible&&c.get("timeSlider")){switch(c.get("timeSliderProtocol")){case"WMS":this.slider.addDataset({id:"id"+strHash(c.get("view").id),color:c.get("color"),data:new TimeSlider.Plugin.WMS({url:c.get("view").urls[0],eoid:c.get("view").id,dataset:"id"+strHash(c.get("view").id)})}),this.active_products.push("id"+strHash(c.get("view").id));break;case"EOWCS":this.slider.addDataset({id:"id"+strHash(c.get("download").id),color:c.get("color"),data:new TimeSlider.Plugin.EOWCS({url:c.get("download").url,eoid:c.get("download").id,dataset:"id"+strHash(c.get("download").id)})}),this.active_products.push("id"+strHash(c.get("download").id));break;case"WPS":var d=b.reqres.request("map:get:extent");this.slider.addDataset({id:"id"+strHash(c.get("download").id),color:c.get("color"),data:new TimeSlider.Plugin.WPS({url:c.get("download").url,eoid:c.get("download").id,dataset:"id"+strHash(c.get("download").id),bbox:[d.left,d.bottom,d.right,d.top]})}),this.activeWPSproducts.push("id"+strHash(c.get("download").id)),this.active_products.push("id"+strHash(c.get("download").id)),this.slider.updateBBox([d.left,d.bottom,d.right,d.top],"id"+strHash(c.get("download").id))}this.slider.show()}else"WMS"==c.get("timeSliderProtocol")?(this.slider.removeDataset("id"+strHash(c.get("view").id)),-1!=this.active_products.indexOf("id"+strHash(c.get("view").id))&&this.active_products.splice(this.active_products.indexOf("id"+strHash(c.get("view").id)),1)):(this.slider.removeDataset("id"+strHash(c.get("download").id)),-1!=this.activeWPSproducts.indexOf("id"+strHash(c.get("download").id))&&this.activeWPSproducts.splice(this.activeWPSproducts.indexOf("id"+strHash(c.get("download").id)),1),-1!=this.active_products.indexOf("id"+strHash(c.get("download").id))&&this.active_products.splice(this.active_products.indexOf("id"+strHash(c.get("download").id)),1)),0==this.active_products.length&&this.slider.hide()}},updateExtent:function(a){for(var b=0;b"),this.d.css({height:a.y,width:a.x,position:"relative","margin-top":"-"+a.y+"px"}),this.d.attr("title",this.model.get("disabledDescription")),this.$el.after(this.d))},onClose:function(){this.d&&(this.d.remove(),this.d=null)},onShow:function(){var a=this.model.get("size");if(null==a){var b,c;0!=this.$el.outerHeight()&&(c=this.$el.outerHeight()),0!=this.$el.outerWidth()&&(b=this.$el.outerWidth()),b&&c&&(a={x:b,y:c},this.model.set("size",a))}this.model.get("enabled")?this.$el.attr("title",this.model.get("description")):!this.d&&a&&(this.d=$("
    "),this.d.css({height:a.y,width:a.x,position:"relative","margin-top":"-"+a.y+"px"}),this.d.attr("title",this.model.get("disabledDescription")),this.$el.after(this.d))}});return{ToolItemView:c}})}).call(this); \ No newline at end of file diff -Nru eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/views/ToolSelectionView.js eoxserver-0.3.2/eoxserver/webclient/static/scripts/views/ToolSelectionView.js --- eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/views/ToolSelectionView.js 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/webclient/static/scripts/views/ToolSelectionView.js 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -(function(){"use strict";var a=this;a.define(["backbone","communicator","underscore"],function(a){var b=a.Marionette.CollectionView.extend({tagName:"div",className:"btn-group-vertical",initialize:function(){},onShow:function(){}});return{ToolSelectionView:b}})}).call(this); \ No newline at end of file diff -Nru eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/views/UIElementView.js eoxserver-0.3.2/eoxserver/webclient/static/scripts/views/UIElementView.js --- eoxserver-0.4.0beta2/eoxserver/webclient/static/scripts/views/UIElementView.js 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/webclient/static/scripts/views/UIElementView.js 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -(function(){"use strict";var a=this;a.define(["backbone","communicator","hbs!tmpl/UIElement","underscore"],function(a,b,c){var d=a.Marionette.ItemView.extend({template:{type:"handlebars",template:c},initialize:function(){}});return{UIElementView:d}})}).call(this); \ No newline at end of file diff -Nru eoxserver-0.4.0beta2/eoxserver/webclient/static/style.css eoxserver-0.3.2/eoxserver/webclient/static/style.css --- eoxserver-0.4.0beta2/eoxserver/webclient/static/style.css 1970-01-01 00:00:00.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/webclient/static/style.css 2013-12-10 14:57:00.000000000 +0000 @@ -0,0 +1,77 @@ +.container { + display: table; +} + +.row { + display: table-row; +} + +.cell { + display: table-cell; +} + +.header { + font-weight: bold; +} + +.ui-dialog { + opacity: 0.9; +} + +#div-busy-indicator { + z-index: 5000; + position: fixed; + padding: 10px; + top: 50%; + left: 50%; + text-align: center; + display: none; +} + +.tooltip { + position: absolute; + z-index: 3000; + border: 1px solid #111; + background-color: #eee; + padding: 5px; + opacity: 0.85; + display: none; +} + +.tooltip h3, .tooltip div { margin: 0; } + +.error { + background-color: #fcc; +} + +.ui-coverage-item { + padding: 3px; + margin-bottom: 2px; +} + +.ui-band-item { + margin: 0.3em; + padding: 0.15em; + cursor: move; +} + +.ui-state-highlight { + margin: 0.3em; + padding: 0.15em; +} + +.scrollable { + max-height: 120px; + width: 280px; + overflow: scroll; +} + +#div-coverage-info .ui-section { + margin: 0.2em; + padding: 0.2em; +} + +#div-coverage-info table[1] td { + border: 1px solid #111; + margin: 0px; +} diff -Nru eoxserver-0.4.0beta2/eoxserver/webclient/static/styles/main.css eoxserver-0.3.2/eoxserver/webclient/static/styles/main.css --- eoxserver-0.4.0beta2/eoxserver/webclient/static/styles/main.css 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/webclient/static/styles/main.css 1970-01-01 00:00:00.000000000 +0000 @@ -1,12 +0,0 @@ -#map{height:100%;width:100%}.ol-attribution{position:absolute;bottom:0;right:0;color:#333;background-color:#fff;padding:1px 5px 0;border:1px solid #ccc;border-radius:3px 0 0;border-width:1px 0 0 1px;line-height:1em;font-family:'Lucida Grande',Verdana,Geneva,Lucida,Arial,Helvetica,sans-serif}.ol-attribution img{height:15px!important}.ol-attribution button{display:none!important}.ol-attribution ul{list-style-type:none;margin:0;padding-left:0;font-size:.69em!important}.ol-attribution li{display:inline;padding-right:10px}.ol-zoom{top:60px!important}div .ol-mouse-position{position:absolute!important;bottom:0!important;right:auto!important;color:#333;background-color:#fff;padding:1px 5px 0;border:1px solid #ccc;border-radius:3px 0 0;border-width:1px 0 0 1px;font-size:.69em!important;line-height:1em!important}#terrain_attribution{position:absolute;bottom:2em;right:.2em;color:#333;background-color:#fff;padding:1px 5px 0;border:1px solid #ccc;border-radius:3px 0 0;border-width:1px 0 0 1px;font-family:'Lucida Grande',Verdana,Geneva,Lucida,Arial,Helvetica,sans-serif;font-weight:400;font-size:.8em;display:none;z-index:1005}#terrain_attribution h2{margin-bottom:.1em}#terrain_attribution ul{margin-top:.2em;-webkit-padding-start:10px}.ol-zoom{top:.5em;left:.5em}.ol-rotate{top:.5em;right:.5em;transition:opacity .25s}.ol-zoom-extent{top:4.643em;left:.5em}.ol-zoom-extent button{line-height:1.4em}.ol-zoom-extent button:after{content:"E"}.ol-zoom .ol-zoom-in{border-radius:2px 2px 0 0}.ol-zoom .ol-zoom-out{border-radius:0 0 2px 2px}.ol-zoom .ol-has-tooltip:hover [role=tooltip],.ol-zoom .ol-has-tooltip:focus [role=tooltip]{top:1.1em}.ol-zoomslider{position:absolute;top:4.5em;left:.5em;background:#eee;background:rgba(255,255,255,.4);border-radius:4px;outline:0;overflow:hidden;width:1.5675em;height:200px;padding:3px;margin:0}.ol-zoomslider-thumb{position:absolute;display:block;background:#7b98bc;background:rgba(0,60,136,.5);border-radius:2px;outline:0;overflow:hidden;cursor:pointer;font-size:1.14em;height:1em;width:1.375em;margin:3px;padding:0}.ol-touch .ol-zoomslider{top:5.5em;width:2.052em}.ol-touch .ol-zoomslider-thumb{width:1.8em}.ol-viewport .ol-unselectable{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent}.ol-control{position:absolute;background-color:#eee;background-color:rgba(255,255,255,.4);border-radius:4px;padding:2px}.ol-control:hover{background-color:rgba(255,255,255,.6)}.ol-control button{display:block;margin:1px;padding:0;color:#fff;font-size:1.14em;font-weight:700;text-decoration:none;text-align:center;height:1.375em;width:1.375em;line-height:.4em;background-color:#7b98bc;background-color:rgba(0,60,136,.5);border:0;border-radius:2px}.ol-control button::-moz-focus-inner{border:0;padding:0}.ol-control button:hover,.ol-control button:focus{text-decoration:none;background-color:#4c6079;background-color:rgba(0,60,136,.7)}.ol-has-tooltip [role=tooltip]{position:absolute;clip:rect(1px 1px 1px 1px);clip:rect(1px,1px,1px,1px);padding:0;border:0;height:1px;width:1px;overflow:hidden;font-weight:400;font-size:14px;text-shadow:0 0 2px #fff}.ol-has-tooltip:hover [role=tooltip],.ol-has-tooltip:focus [role=tooltip]{-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box;clip:auto;padding:0 .4em;font-size:.8em;height:1.2em;width:auto;line-height:1.2em;z-index:1100;max-height:100px;white-space:nowrap;display:inline-block;background:#FFF;background:rgba(255,255,255,.6);color:#000;border:3px solid rgba(255,255,255,0);border-left-width:0;border-radius:0 4px 4px 0;bottom:.3em;left:2.2em}.ol-touch .ol-has-tooltip:hover [role=tooltip],.ol-touch .ol-has-tooltip:focus [role=tooltip]{display:none}html,body{height:100%;width:100%;padding:0!important;overflow:hidden}#viewContent{position:relative}ul,li{list-style-type:none;list-style-position:inside}hr{margin:1em 0}.not-selectable{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}body.wait *,body.wait{cursor:wait!important}.ui-tooltip{display:inline-block}.ui-tooltip-content{font-size:.8em}#loadscreen{width:100%;height:100%;position:absolute;z-index:5000;text-align:center;opacity:.9}#loadscreen i{position:relative;top:calc(50%)}#loadscreen .warninglabel{position:relative;top:calc(50%)}div#preload{display:none}#error-messages{position:absolute;top:60px;width:40%;left:30%}.panel .close{float:right;margin:-.5em -.7em 0 0;font-size:1.2em}.panel .no-label,.modal .no-label{float:left;margin:-.55em 0 0 -.6em;font-size:1em}.modal{overflow-y:hidden!important}button[active=active]{background-color:#ccc}.ui-draggable .panel-heading{cursor:move}.sortable-placeholder{display:block;height:2em}.layercontrol{width:20em;top:10em;left:3em;font-size:.9em;position:absolute}.layercontrol ul{margin:0 0 1em 1.5em;padding:0}#baseLayers ul{margin-bottom:0}#baseLayers li{margin-bottom:0;margin-top:.2em}.layercontrol li{margin:.2em 0 .8em;padding:0 0 0 .6em}.layercontrol ul input{margin-top:2px}.layercontrol .sortable-layer{margin:.3em -.5em 0 -2em;padding:0}.layercontrol .sortable-layer .form-control{font-size:1em;line-height:1.1em;max-width:10em;overflow:auto}.fa-sort{cursor:ns-resize}.toolcontrol{width:5.2em;top:10em;right:3em;font-size:.9em;position:absolute;right:3em}.toolcontrol .panel-heading h3{text-align:center;font-size:1.8em}.toolcontrol .panel-body{text-align:center;padding:.3em .3em .1em .4em}.toolcontrol .btn-group-vertical{margin-bottom:.5em}.selectionManager,.download{width:50em;top:10em;left:50%;margin:0 0 0 -25em;font-size:1em;position:absolute}.selectionManager .input-group{margin:.3em 0}#btn-start-download{float:right}.selectionManager .panel-body,.download .panel-body{max-height:15em;overflow:auto}#content{height:100%;width:100%}#map{height:100%;width:100%;position:absolute}#bottomBar{bottom:15px;position:absolute;width:96%;left:2%}#timeslider svg.timeslider{height:4em;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}#OpenLayers_Control_Zoom_5{top:60px}.tab-header{cursor:pointer}.sidepane{width:100px;height:100px}.myfileupload-buttonbar input{position:absolute;top:0;right:0;margin:0;border:solid transparent;border-width:0 0 100px 200px;opacity:0;filter:alpha(opacity=0);-o-transform:translate(250px,-50px) scale(1);-moz-transform:translate(-300px,0) scale(4);direction:ltr;cursor:pointer}.myui-button{position:relative;cursor:pointer;text-align:center;overflow:hidden}.hero-unit{margin:50px auto 0;width:300px}#div-downloads{display:none}.popover{position:fixed}.icon-large{background-image:url(../images/glyphicons.png);background-position:24px 24px;background-repeat:no-repeat;display:inline-block;height:28px;line-height:28px;width:28px}.icon-large.icon-glass{background-position:0 0}.icon-large.icon-leaf{background-position:0 -34px}.icon-large.icon-dog{background-position:0 -69px}.icon-large.icon-user{background-position:0 -104px}.icon-large.icon-girl{background-position:0 -136px}.icon-large.icon-car{background-position:0 -168px}.icon-large.icon-user-add{background-position:0 -200px}.icon-large.icon-user-remove{background-position:0 -232px}.icon-large.icon-film{background-position:0 -264px}.icon-large.icon-magic{background-position:0 -300px}.icon-large.icon-envelope{background-position:0 -330px}.icon-large.icon-camera{background-position:0 -360px}.icon-large.icon-heart{background-position:0 -390px}.icon-large.icon-beach-umbrella{background-position:0 -422px}.icon-large.icon-train{background-position:0 -457px}.icon-large.icon-print{background-position:0 -494px}.icon-large.icon-bin{background-position:0 -528px}.icon-large.icon-trash{background-position:0 -528px}.icon-large.icon-music{background-position:0 -566px}.icon-large.icon-note{background-position:0 -601px}.icon-large.icon-cogwheel{background-position:0 -636px}.icon-large.icon-cog{background-position:0 -636px}.icon-large.icon-home{background-position:0 -670px}.icon-large.icon-snowflake{background-position:0 -706px}.icon-large.icon-fire{background-position:0 -744px}.icon-large.icon-cogwheels{background-position:0 -780px}.icon-large.icon-parents{background-position:0 -816px}.icon-large.icon-binoculars{background-position:0 -848px}.icon-large.icon-road{background-position:0 -882px}.icon-large.icon-search{background-position:0 -916px}.icon-large.icon-cars{background-position:0 -950px}.icon-large.icon-pencil{background-position:0 -985px}.icon-large.icon-bus{background-position:0 -1020px}.icon-large.icon-wifi-alt{background-position:0 -1055px}.icon-large.icon-luggage{background-position:0 -1091px}.icon-large.icon-old-man{background-position:0 -1128px}.icon-large.icon-woman{background-position:0 -1162px}.icon-large.icon-file{background-position:0 -1194px}.icon-large.icon-credit{background-position:0 -1228px}.icon-large.icon-airplane,.icon-large.icon-plane{background-position:0 -1262px}.icon-large.icon-notes{background-position:0 -1297px}.icon-large.icon-stats{background-position:0 -1332px}.icon-large.icon-charts{background-position:0 -1367px}.icon-large.icon-pie-chart{background-position:0 -1401px}.icon-large.icon-group{background-position:0 -1436px}.icon-large.icon-keys{background-position:0 -1468px}.icon-large.icon-calendar{background-position:0 -1504px}.icon-large.icon-router{background-position:0 -1539px}.icon-large.icon-camera-small{background-position:0 -1575px}.icon-large.icon-dislikes{background-position:0 -1609px}.icon-large.icon-star-empty{background-position:0 -1609px}.icon-large.icon-star{background-position:0 -1643px}.icon-large.icon-link{background-position:0 -1677px}.icon-large.icon-eye-open{background-position:-1px -1704px}.icon-large.icon-eye-close{background-position:-1px -1737px}.icon-large.icon-alarm{background-position:0 -1771px}.icon-large.icon-clock{background-position:0 -1807px}.icon-large.icon-time{background-position:0 -1807px}.icon-large.icon-stopwatch{background-position:0 -1841px}.icon-large.icon-projector{background-position:0 -1878px}.icon-large.icon-history{background-position:0 -1913px}.icon-large.icon-truck{background-position:0 -1949px}.icon-large.icon-cargo{background-position:0 -1986px}.icon-large.icon-compass{background-position:-46px 0}.icon-large.icon-keynote{background-position:-46px -34px}.icon-large.icon-attach{background-position:-46px -74px}.icon-large.icon-power{background-position:-46px -108px}.icon-large.icon-off{background-position:-46px -108px}.icon-large.icon-lightbulb{background-position:-46px -142px}.icon-large.icon-tag{background-position:-46px -178px}.icon-large.icon-tags{background-position:-46px -212px}.icon-large.icon-cleaning{background-position:-46px -246px}.icon-large.icon-ruller{background-position:-46px -281px}.icon-large.icon-gift{background-position:-46px -305px}.icon-large.icon-umbrella{background-position:-46px -340px}.icon-large.icon-book{background-position:-46px -378px}.icon-large.icon-bookmark{background-position:-44px -412px}.icon-large.icon-signal{background-position:-46px -446px}.icon-large.icon-cup{background-position:-46px -479px}.icon-large.icon-stroller{background-position:-46px -513px}.icon-large.icon-headphones{background-position:-46px -549px}.icon-large.icon-headset{background-position:-46px -583px}.icon-large.icon-warning-sign{background-position:-46px -621px}.icon-large.icon-signal{background-position:-46px -655px}.icon-large.icon-retweet{background-position:-47px -680px}.icon-large.icon-refresh{background-position:-46px -714px}.icon-large.icon-roundabout{background-position:-46px -750px}.icon-large.icon-random{background-position:-46px -787px}.icon-large.icon-heat{background-position:-46px -817px}.icon-large.icon-repeat{background-position:-46px -852px}.icon-large.icon-display{background-position:-46px -888px}.icon-large.icon-log-book{background-position:-46px -922px}.icon-large.icon-adress-book{background-position:-46px -956px}.icon-large.icon-magnet{background-position:-46px -990px}.icon-large.icon-table{background-position:-46px -1023px}.icon-large.icon-adjust{background-position:-46px -1057px}.icon-large.icon-tint{background-position:-46px -1093px}.icon-large.icon-crop{background-position:-46px -1129px}.icon-large.icon-vector-path-square{background-position:-46px -1165px}.icon-large.icon-vector-path-circle{background-position:-46px -1199px}.icon-large.icon-vector-path-polygon{background-position:-46px -1233px}.icon-large.icon-vector-path-line{background-position:-46px -1268px}.icon-large.icon-vector-path-curve{background-position:-46px -1302px}.icon-large.icon-vector-path-all{background-position:-46px -1336px}.icon-large.icon-font{background-position:-46px -1370px}.icon-large.icon-italic{background-position:-46px -1403px}.icon-large.icon-bold{background-position:-46px -1437px}.icon-large.icon-text-underline{background-position:-46px -1471px}.icon-large.icon-text-strike{background-position:-46px -1505px}.icon-large.icon-text-height{background-position:-46px -1537px}.icon-large.icon-text-width{background-position:-46px -1571px}.icon-large.icon-text-resize{background-position:-46px -1605px}.icon-large.icon-left-indent,.icon-large.icon-indent-left{background-position:-46px -1641px}.icon-large.icon-right-indent,.icon-large.icon-indent-right{background-position:-46px -1673px}.icon-large.icon-align-left{background-position:-46px -1705px}.icon-large.icon-align-center{background-position:-46px -1736px}.icon-large.icon-align-right{background-position:-46px -1767px}.icon-large.icon-justify{background-position:-46px -1798px}.icon-large.icon-align-justify{background-position:-46px -1798px}.icon-large.icon-list{background-position:-46px -1829px}.icon-large.icon-text-smaller{background-position:-46px -1860px}.icon-large.icon-text-bigger{background-position:-46px -1886px}.icon-large.icon-embed{background-position:-47px -1910px}.icon-large.icon-embed-close{background-position:-47px -1940px}.icon-large.icon-adjust{background-position:-46px -1976px}.icon-large.icon-message-full{background-position:-92px 0}.icon-large.icon-message-empty{background-position:-92px -38px}.icon-large.icon-message-in{background-position:-92px -76px}.icon-large.icon-message-out{background-position:-92px -114px}.icon-large.icon-message-plus{background-position:-92px -152px}.icon-large.icon-message-minus{background-position:-92px -185px}.icon-large.icon-message-ban{background-position:-92px -218px}.icon-large.icon-message-flag{background-position:-92px -251px}.icon-large.icon-message-lock{background-position:-92px -284px}.icon-large.icon-message-new{background-position:-92px -318px}.icon-large.icon-inbox{background-position:-92px -350px}.icon-large.icon-inbox-plus{background-position:-92px -383px}.icon-large.icon-inbox-minus{background-position:-92px -420px}.icon-large.icon-inbox-lock{background-position:-92px -457px}.icon-large.icon-inbox-in{background-position:-92px -495px}.icon-large.icon-inbox-out{background-position:-92px -531px}.icon-large.icon-computer-locked{background-position:-92px -567px}.icon-large.icon-computer-service{background-position:-92px -601px}.icon-large.icon-computer-proces{background-position:-92px -635px}.icon-large.icon-phone{background-position:-92px -669px}.icon-large.icon-database-lock{background-position:-92px -704px}.icon-large.icon-database-plus{background-position:-92px -742px}.icon-large.icon-database-minus{background-position:-92px -779px}.icon-large.icon-database-ban{background-position:-92px -816px}.icon-large.icon-folder-open{background-position:-92px -853px}.icon-large.icon-folder-plus{background-position:-92px -885px}.icon-large.icon-folder-minus{background-position:-92px -920px}.icon-large.icon-folder-lock{background-position:-92px -955px}.icon-large.icon-folder-flag{background-position:-92px -991px}.icon-large.icon-folder-new{background-position:-92px -1026px}.icon-large.icon-check{background-position:-92px -1060px}.icon-large.icon-edit{background-position:-92px -1088px}.icon-large.icon-new-window{background-position:-92px -1119px}.icon-large.icon-more-windows{background-position:-92px -1151px}.icon-large.icon-show-big-thumbnails{background-position:-92px -1184px}.icon-large.icon-th-large{background-position:-92px -1184px}.icon-large.icon-show-thumbnails{background-position:-92px -1216px}.icon-large.icon-th{background-position:-92px -1216px}.icon-large.icon-show-thumbnails-with-lines{background-position:-92px -1248px}.icon-large.icon-th-list{background-position:-92px -1248px}.icon-large.icon-show-lines{background-position:-92px -1273px}.icon-large.icon-playlist{background-position:-92px -1298px}.icon-large.icon-picture{background-position:-92px -1332px}.icon-large.icon-imac{background-position:-92px -1362px}.icon-large.icon-macbook{background-position:-92px -1394px}.icon-large.icon-ipad{background-position:-92px -1419px}.icon-large.icon-iphone{background-position:-92px -1456px}.icon-large.icon-iphone-transfer{background-position:-92px -1490px}.icon-large.icon-iphone-exchange{background-position:-92px -1524px}.icon-large.icon-ipod{background-position:-92px -1558px}.icon-large.icon-ipod-shuffle{background-position:-92px -1590px}.icon-large.icon-ear-plugs{background-position:-92px -1613px}.icon-large.icon-albums{background-position:-92px -1647px}.icon-large.icon-step-backward{background-position:-92px -1675px}.icon-large.icon-fast-backward{background-position:-92px -1703px}.icon-large.icon-rewind,.icon-large.icon-backwards{background-position:-92px -1731px}.icon-large.icon-play{background-position:-92px -1759px}.icon-large.icon-pause{background-position:-92px -1787px}.icon-large.icon-stop{background-position:-92px -1813px}.icon-large.icon-forward{background-position:-92px -1837px}.icon-large.icon-fast-forward{background-position:-92px -1865px}.icon-large.icon-step-forward{background-position:-92px -1893px}.icon-large.icon-eject{background-position:-92px -1921px}.icon-large.icon-facetime-video{background-position:-92px -1948px}.icon-large.icon-download-alt{background-position:-92px -1974px}.icon-large.icon-mute,.icon-large.icon-volume-off{background-position:-138px 4px}.icon-large.icon-volume-down{background-position:-134px -22px}.icon-large.icon-volume-up{background-position:-138px -52px}.icon-large.icon-screenshot{background-position:-138px -88px}.icon-large.icon-move{background-position:-138px -125px}.icon-large.icon-more{background-position:-138px -159px}.icon-large.icon-brightness-reduce{background-position:-138px -176px}.icon-large.icon-brightness-increase{background-position:-138px -206px}.icon-large.icon-circle-plus,.icon-large.icon-plus-sign{background-position:-138px -240px}.icon-large.icon-circle-minus,.icon-large.icon-minus-sign{background-position:-138px -276px}.icon-large.icon-circle-remove,.icon-large.icon-remove-sign{background-position:-138px -312px}.icon-large.icon-circle-ok,.icon-large.icon-ok-sign{background-position:-138px -348px}.icon-large.icon-circle-question-mark,.icon-large.icon-question-sign{background-position:-138px -384px}.icon-large.icon-circle-info,.icon-large.icon-info-sign{background-position:-138px -420px}.icon-large.icon-circle-exclamation-mark,.icon-large.icon-exclamation-sign{background-position:-138px -456px}.icon-large.icon-remove{background-position:-138px -492px}.icon-large.icon-ok{background-position:-138px -528px}.icon-large.icon-ban{background-position:-138px -564px}.icon-large.icon-download{background-position:-138px -600px}.icon-large.icon-upload{background-position:-138px -636px}.icon-large.icon-shopping-cart{background-position:-138px -672px}.icon-large.icon-lock{background-position:-138px -705px}.icon-large.icon-unlock{background-position:-138px -741px}.icon-large.icon-electricity{background-position:-138px -777px}.icon-large.icon-cart-out{background-position:-138px -811px}.icon-large.icon-cart-in{background-position:-138px -846px}.icon-large.icon-left-arrow{background-position:-138px -880px}.icon-large.icon-right-arrow{background-position:-138px -908px}.icon-large.icon-down-arrow{background-position:-138px -936px}.icon-large.icon-up-arrow{background-position:-138px -966px}.icon-large.icon-resize-small{background-position:-138px -996px}.icon-large.icon-resize-full{background-position:-138px -1030px}.icon-large.icon-circle-arrow-left{background-position:-138px -1064px}.icon-large.icon-circle-arrow-right{background-position:-138px -1100px}.icon-large.icon-circle-arrow-top,.icon-large.icon-circle-arrow-up{background-position:-138px -1136px}.icon-large.icon-circle-arrow-down{background-position:-138px -1172px}.icon-large.icon-play-button{background-position:-138px -1208px}.icon-large.icon-play-circle{background-position:-138px -1208px}.icon-large.icon-unshare{background-position:-138px -1244px}.icon-large.icon-share{background-position:-138px -1272px}.icon-large.icon-thin-right-arrow,.icon-large.icon-chevron-right{background-position:-138px -1300px}.icon-large.icon-thin-arrow-left,.icon-large.icon-chevron-left{background-position:-138px -1332px}.icon-large.icon-bluetooth{background-position:-138px -1364px}.icon-large.icon-euro{background-position:-138px -1398px}.icon-large.icon-usd{background-position:-138px -1431px}.icon-large.icon-bp{background-position:-138px -1467px}.icon-large.icon-moon{background-position:-138px -1501px}.icon-large.icon-sun{background-position:-138px -1536px}.icon-large.icon-cloud{background-position:-138px -1570px}.icon-large.icon-direction{background-position:-138px -1597px}.icon-large.icon-brush{background-position:-138px -1633px}.icon-large.icon-pen{background-position:-138px -1666px}.icon-large.icon-zoom-in{background-position:-138px -1700px}.icon-large.icon-zoom-out{background-position:-138px -1735px}.icon-large.icon-pin{background-position:-138px -1770px}.icon-large.icon-riflescope{background-position:-138px -1805px}.icon-large.icon-rotation-lock{background-position:-138px -1840px}.icon-large.icon-flash{background-position:-138px -1874px}.icon-large.icon-google-maps,.icon-large.icon-map-marker{background-position:-138px -1909px}.icon-large.icon-anchor{background-position:-138px -1943px}.icon-large.icon-conversation{background-position:-138px -1978px}.icon-large.icon-chat{background-position:-184px 0}.icon-large.icon-male{background-position:-184px -29px}.icon-large.icon-female{background-position:-184px -61px}.icon-large.icon-asterisk{background-position:-184px -98px}.icon-large.icon-divide{background-position:-184px -128px}.icon-large.icon-snorkel-diving{background-position:-184px -154px}.icon-large.icon-scuba-diving{background-position:-184px -189px}.icon-large.icon-oxygen-bottle{background-position:-184px -223px}.icon-large.icon-fins{background-position:-184px -260px}.icon-large.icon-fishes{background-position:-184px -297px}.icon-large.icon-boat{background-position:-184px -337px}.icon-large.icon-delete-point{background-position:-184px -371px}.icon-large.icon-qrcode{background-position:-184px -398px}.icon-large.icon-barcode{background-position:-184px -432px}.icon-large.icon-pool{background-position:-184px -466px}.icon-large.icon-buoy{background-position:-184px -500px}.icon-large.icon-spade{background-position:-184px -534px}.icon-large.icon-bank{background-position:-184px -568px}.icon-large.icon-vcard{background-position:-184px -602px}.icon-large.icon-electrical-plug{background-position:-184px -636px}.icon-large.icon-flag{background-position:-184px -671px}.icon-large.icon-credit-card{background-position:-184px -707px}.icon-large.icon-keyboard-wireless{background-position:-184px -736px}.icon-large.icon-keyboard-wired{background-position:-184px -765px}.icon-large.icon-shield{background-position:-184px -800px}.icon-large.icon-ring{background-position:-184px -834px}.icon-large.icon-cake{background-position:-184px -868px}.icon-large.icon-drink{background-position:-184px -902px}.icon-large.icon-beer{background-position:-184px -936px}.icon-large.icon-fast-food{background-position:-184px -970px}.icon-large.icon-cutlery{background-position:-184px -1004px}.icon-large.icon-pizza{background-position:-184px -1038px}.icon-large.icon-birthday-cake{background-position:-184px -1077px}.icon-large.icon-tablet{background-position:-184px -1116px}.icon-large.icon-settings{background-position:-184px -1151px}.icon-large.icon-bullets{background-position:-184px -1185px}.icon-large.icon-cardio{background-position:-184px -1218px}.icon-large.icon-pants{background-position:-184px -1254px}.icon-large.icon-sweater{background-position:-184px -1288px}.icon-large.icon-fabric{background-position:-184px -1322px}.icon-large.icon-leather{background-position:-184px -1354px}.icon-large.icon-scissors{background-position:-184px -1388px}.icon-large.icon-podium{background-position:-184px -1425px}.icon-large.icon-skull{background-position:-184px -1456px}.icon-large.icon-celebration{background-position:-184px -1490px}.icon-large.icon-tea-kettle{background-position:-184px -1525px}.icon-large.icon-french-press{background-position:-184px -1558px}.icon-large.icon-coffe-cup{background-position:-184px -1593px}.icon-large.icon-pot{background-position:-184px -1622px}.icon-large.icon-grater{background-position:-184px -1654px}.icon-large.icon-kettle{background-position:-184px -1688px}.icon-large.icon-hospital{background-position:-184px -1722px}.icon-large.icon-hospital-h{background-position:-184px -1756px}.icon-large.icon-microphone{background-position:-184px -1790px}.icon-large.icon-webcam{background-position:-184px -1824px}.icon-large.icon-temple-christianity-church{background-position:-184px -1858px}.icon-large.icon-temple-islam{background-position:-184px -1893px}.icon-large.icon-temple-hindu{background-position:-184px -1927px}.icon-large.icon-temple-buddhist{background-position:-184px -1961px}.icon-large.icon-electrical-socket-eu{background-position:-230px 0}.icon-large.icon-electrical-socket-us{background-position:-230px -33px}.icon-large.icon-bomb{background-position:-230px -66px}.icon-large.icon-comments,.icon-large.icon-comment{background-position:-230px -102px}.icon-large.icon-flower{background-position:-230px -135px}.icon-large.icon-baseball{background-position:-230px -170px}.icon-large.icon-rugby{background-position:-230px -206px}.icon-large.icon-ax{background-position:-230px -240px}.icon-large.icon-table-tennis{background-position:-230px -275px}.icon-large.icon-bowling{background-position:-230px -309px}.icon-large.icon-tree-conifer{background-position:-230px -343px}.icon-large.icon-tree-deciduous{background-position:-230px -377px}.icon-large.icon-sort{background-position:-230px -412px}.icon-large.icon-filter{background-position:-230px -447px}.icon-large.icon-gamepad{background-position:-230px -481px}.icon-large.icon-playing-dices{background-position:-230px -510px}.icon-large.icon-calculator{background-position:-230px -543px}.icon-large.icon-tie{background-position:-230px -577px}.icon-large.icon-wallet{background-position:-230px -613px}.icon-large.icon-share{background-position:-230px -643px}.icon-large.icon-sampler{background-position:-230px -675px}.icon-large.icon-piano{background-position:-230px -707px}.icon-large.icon-web-browser{background-position:-230px -741px}.icon-large.icon-blog{background-position:-230px -773px}.icon-large.icon-dashboard{background-position:-230px -806px}.icon-large.icon-certificate{background-position:-230px -840px}.icon-large.icon-bell{background-position:-230px -875px}.icon-large.icon-candle{background-position:-230px -909px}.icon-large.icon-pin-classic{background-position:-230px -944px}.icon-large.icon-iphone-shake{background-position:-230px -978px}.icon-large.icon-pin-flag{background-position:-230px -1012px}.icon-large.icon-turtle{background-position:-230px -1044px}.icon-large.icon-rabbit{background-position:-230px -1070px}.icon-large.icon-globe{background-position:-230px -1102px}.icon-large.icon-briefcase{background-position:-230px -1136px}.icon-large.icon-hdd{background-position:-230px -1167px}.icon-large.icon-thumbs-up{background-position:-230px -1198px}.icon-large.icon-thumbs-down{background-position:-230px -1229px}.icon-large.icon-hand-right{background-position:-230px -1260px}.icon-large.icon-hand-left{background-position:-230px -1289px}.icon-large.icon-hand-up{background-position:-230px -1318px}.icon-large.icon-hand-down{background-position:-230px -1354px}.icon-large.icon-fullscreen{background-position:-230px -1391px}.icon-large.icon-shopping-bag{background-position:-230px -1425px}.icon-large.icon-book-open{background-position:-230px -1461px}.icon-large.icon-nameplate{background-position:-230px -1494px}.icon-large.icon-nameplate-alt{background-position:-230px -1525px}.icon-large.icon-vases{background-position:-230px -1557px}.icon-large.icon-announcement,.icon-large.icon-bullhorn{background-position:-230px -1591px}.icon-large.icon-dumbbell{background-position:-230px -1621px}.icon-large.icon-suitcase{background-position:-230px -1647px}.icon-large.icon-file-import{background-position:-230px -1679px}.icon-large.icon-file-export{background-position:-230px -1713px}.icon-large.icon-pinterest{background-position:-230px -1747px}.icon-large.icon-dropbox{background-position:-230px -1781px}.icon-large.icon-jolicloud{background-position:-230px -1815px}.icon-large.icon-yahoo{background-position:-230px -1849px}.icon-large.icon-blogger{background-position:-230px -1883px}.icon-large.icon-picasa{background-position:-230px -1917px}.icon-large.icon-amazon{background-position:-230px -1951px}.icon-large.icon-tumblr{background-position:-230px -1985px}.icon-large.icon-wordpress{background-position:-276px 0}.icon-large.icon-instapaper{background-position:-276px -34px}.icon-large.icon-evernote{background-position:-276px -68px}.icon-large.icon-xing{background-position:-276px -102px}.icon-large.icon-zootool{background-position:-276px -136px}.icon-large.icon-dribbble{background-position:-276px -170px}.icon-large.icon-deviantart{background-position:-276px -204px}.icon-large.icon-read-it-later{background-position:-276px -238px}.icon-large.icon-linked-in{background-position:-276px -272px}.icon-large.icon-forrst{background-position:-276px -306px}.icon-large.icon-pinboard{background-position:-276px -340px}.icon-large.icon-behance{background-position:-276px -374px}.icon-large.icon-github{background-position:-276px -408px}.icon-large.icon-youtube{background-position:-276px -442px}.icon-large.icon-skitch{background-position:-276px -476px}.icon-large.icon-quora{background-position:-276px -510px}.icon-large.icon-google-plus{background-position:-276px -544px}.icon-large.icon-spootify{background-position:-276px -578px}.icon-large.icon-stumbleupon{background-position:-276px -612px}.icon-large.icon-readability{background-position:-276px -646px}.icon-large.icon-facebook{background-position:-276px -680px}.icon-large.icon-twitter-t{background-position:-276px -714px}.icon-large.icon-twitter{background-position:-276px -748px}.icon-large.icon-buzz{background-position:-276px -782px}.icon-large.icon-vimeo{background-position:-276px -816px}.icon-large.icon-flickr{background-position:-276px -850px}.icon-large.icon-last-fm{background-position:-276px -884px}.icon-large.icon-rss{background-position:-276px -918px}.icon-large.icon-skype{background-position:-276px -952px}/*! jQuery UI - v1.10.4 - 2014-01-17 -* http://jqueryui.com -* Includes: jquery.ui.core.css, jquery.ui.accordion.css, jquery.ui.autocomplete.css, jquery.ui.button.css, jquery.ui.datepicker.css, jquery.ui.dialog.css, jquery.ui.menu.css, jquery.ui.progressbar.css, jquery.ui.resizable.css, jquery.ui.selectable.css, jquery.ui.slider.css, jquery.ui.spinner.css, jquery.ui.tabs.css, jquery.ui.tooltip.css, jquery.ui.theme.css -* To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Verdana%2CArial%2Csans-serif&fwDefault=normal&fsDefault=1.1em&cornerRadius=4px&bgColorHeader=cccccc&bgTextureHeader=highlight_soft&bgImgOpacityHeader=75&borderColorHeader=aaaaaa&fcHeader=222222&iconColorHeader=222222&bgColorContent=ffffff&bgTextureContent=flat&bgImgOpacityContent=75&borderColorContent=aaaaaa&fcContent=222222&iconColorContent=222222&bgColorDefault=e6e6e6&bgTextureDefault=glass&bgImgOpacityDefault=75&borderColorDefault=d3d3d3&fcDefault=555555&iconColorDefault=888888&bgColorHover=dadada&bgTextureHover=glass&bgImgOpacityHover=75&borderColorHover=999999&fcHover=212121&iconColorHover=454545&bgColorActive=ffffff&bgTextureActive=glass&bgImgOpacityActive=65&borderColorActive=aaaaaa&fcActive=212121&iconColorActive=454545&bgColorHighlight=fbf9ee&bgTextureHighlight=glass&bgImgOpacityHighlight=55&borderColorHighlight=fcefa1&fcHighlight=363636&iconColorHighlight=2e83ff&bgColorError=fef1ec&bgTextureError=glass&bgImgOpacityError=95&borderColorError=cd0a0a&fcError=cd0a0a&iconColorError=cd0a0a&bgColorOverlay=aaaaaa&bgTextureOverlay=flat&bgImgOpacityOverlay=0&opacityOverlay=30&bgColorShadow=aaaaaa&bgTextureShadow=flat&bgImgOpacityShadow=0&opacityShadow=30&thicknessShadow=8px&offsetTopShadow=-8px&offsetLeftShadow=-8px&cornerRadiusShadow=8px -* Copyright 2014 jQuery Foundation and other contributors; Licensed MIT */.ui-helper-hidden{display:none}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.ui-helper-reset{margin:0;padding:0;border:0;outline:0;line-height:1.3;text-decoration:none;font-size:100%;list-style:none}.ui-helper-clearfix:before,.ui-helper-clearfix:after{content:"";display:table;border-collapse:collapse}.ui-helper-clearfix:after{clear:both}.ui-helper-clearfix{min-height:0}.ui-helper-zfix{width:100%;height:100%;top:0;left:0;position:absolute;opacity:0;filter:Alpha(Opacity=0)}.ui-front{z-index:100}.ui-state-disabled{cursor:default!important}.ui-icon{display:block;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat}.ui-widget-overlay{position:fixed;top:0;left:0;width:100%;height:100%}.ui-accordion .ui-accordion-header{display:block;cursor:pointer;position:relative;margin-top:2px;padding:.5em .5em .5em .7em;min-height:0}.ui-accordion .ui-accordion-icons{padding-left:2.2em}.ui-accordion .ui-accordion-noicons{padding-left:.7em}.ui-accordion .ui-accordion-icons .ui-accordion-icons{padding-left:2.2em}.ui-accordion .ui-accordion-header .ui-accordion-header-icon{position:absolute;left:.5em;top:50%;margin-top:-8px}.ui-accordion .ui-accordion-content{padding:1em 2.2em;border-top:0;overflow:auto}.ui-autocomplete{position:absolute;top:0;left:0;cursor:default}.ui-button{display:inline-block;position:relative;padding:0;line-height:normal;margin-right:.1em;cursor:pointer;vertical-align:middle;text-align:center;overflow:visible}.ui-button,.ui-button:link,.ui-button:visited,.ui-button:hover,.ui-button:active{text-decoration:none}.ui-button-icon-only{width:2.2em}button.ui-button-icon-only{width:2.4em}.ui-button-icons-only{width:3.4em}button.ui-button-icons-only{width:3.7em}.ui-button .ui-button-text{display:block;line-height:normal}.ui-button-text-only .ui-button-text{padding:.4em 1em}.ui-button-icon-only .ui-button-text,.ui-button-icons-only .ui-button-text{padding:.4em;text-indent:-9999999px}.ui-button-text-icon-primary .ui-button-text,.ui-button-text-icons .ui-button-text{padding:.4em 1em .4em 2.1em}.ui-button-text-icon-secondary .ui-button-text,.ui-button-text-icons .ui-button-text{padding:.4em 2.1em .4em 1em}.ui-button-text-icons .ui-button-text{padding-left:2.1em;padding-right:2.1em}input.ui-button{padding:.4em 1em}.ui-button-icon-only .ui-icon,.ui-button-text-icon-primary .ui-icon,.ui-button-text-icon-secondary .ui-icon,.ui-button-text-icons .ui-icon,.ui-button-icons-only .ui-icon{position:absolute;top:50%;margin-top:-8px}.ui-button-icon-only .ui-icon{left:50%;margin-left:-8px}.ui-button-text-icon-primary .ui-button-icon-primary,.ui-button-text-icons .ui-button-icon-primary,.ui-button-icons-only .ui-button-icon-primary{left:.5em}.ui-button-text-icon-secondary .ui-button-icon-secondary,.ui-button-text-icons .ui-button-icon-secondary,.ui-button-icons-only .ui-button-icon-secondary{right:.5em}.ui-buttonset{margin-right:7px}.ui-buttonset .ui-button{margin-left:0;margin-right:-.3em}input.ui-button::-moz-focus-inner,button.ui-button::-moz-focus-inner{border:0;padding:0}.ui-datepicker{width:17em;padding:.2em .2em 0;display:none}.ui-datepicker .ui-datepicker-header{position:relative;padding:.2em 0}.ui-datepicker .ui-datepicker-prev,.ui-datepicker .ui-datepicker-next{position:absolute;top:2px;width:1.8em;height:1.8em}.ui-datepicker .ui-datepicker-prev-hover,.ui-datepicker .ui-datepicker-next-hover{top:1px}.ui-datepicker .ui-datepicker-prev{left:2px}.ui-datepicker .ui-datepicker-next{right:2px}.ui-datepicker .ui-datepicker-prev-hover{left:1px}.ui-datepicker .ui-datepicker-next-hover{right:1px}.ui-datepicker .ui-datepicker-prev span,.ui-datepicker .ui-datepicker-next span{display:block;position:absolute;left:50%;margin-left:-8px;top:50%;margin-top:-8px}.ui-datepicker .ui-datepicker-title{margin:0 2.3em;line-height:1.8em;text-align:center}.ui-datepicker .ui-datepicker-title select{font-size:1em;margin:1px 0}.ui-datepicker select.ui-datepicker-month,.ui-datepicker select.ui-datepicker-year{width:49%}.ui-datepicker table{width:100%;font-size:.9em;border-collapse:collapse;margin:0 0 .4em}.ui-datepicker th{padding:.7em .3em;text-align:center;font-weight:700;border:0}.ui-datepicker td{border:0;padding:1px}.ui-datepicker td span,.ui-datepicker td a{display:block;padding:.2em;text-align:right;text-decoration:none}.ui-datepicker .ui-datepicker-buttonpane{background-image:none;margin:.7em 0 0;padding:0 .2em;border-left:0;border-right:0;border-bottom:0}.ui-datepicker .ui-datepicker-buttonpane button{float:right;margin:.5em .2em .4em;cursor:pointer;padding:.2em .6em .3em;width:auto;overflow:visible}.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current{float:left}.ui-datepicker.ui-datepicker-multi{width:auto}.ui-datepicker-multi .ui-datepicker-group{float:left}.ui-datepicker-multi .ui-datepicker-group table{width:95%;margin:0 auto .4em}.ui-datepicker-multi-2 .ui-datepicker-group{width:50%}.ui-datepicker-multi-3 .ui-datepicker-group{width:33.3%}.ui-datepicker-multi-4 .ui-datepicker-group{width:25%}.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header,.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header{border-left-width:0}.ui-datepicker-multi .ui-datepicker-buttonpane{clear:left}.ui-datepicker-row-break{clear:both;width:100%;font-size:0}.ui-datepicker-rtl{direction:rtl}.ui-datepicker-rtl .ui-datepicker-prev{right:2px;left:auto}.ui-datepicker-rtl .ui-datepicker-next{left:2px;right:auto}.ui-datepicker-rtl .ui-datepicker-prev:hover{right:1px;left:auto}.ui-datepicker-rtl .ui-datepicker-next:hover{left:1px;right:auto}.ui-datepicker-rtl .ui-datepicker-buttonpane{clear:right}.ui-datepicker-rtl .ui-datepicker-buttonpane button{float:left}.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current,.ui-datepicker-rtl .ui-datepicker-group{float:right}.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header,.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header{border-right-width:0;border-left-width:1px}.ui-dialog{overflow:hidden;position:absolute;top:0;left:0;padding:.2em;outline:0}.ui-dialog .ui-dialog-titlebar{padding:.4em 1em;position:relative}.ui-dialog .ui-dialog-title{float:left;margin:.1em 0;white-space:nowrap;width:90%;overflow:hidden;text-overflow:ellipsis}.ui-dialog .ui-dialog-titlebar-close{position:absolute;right:.3em;top:50%;width:20px;margin:-10px 0 0 0;padding:1px;height:20px}.ui-dialog .ui-dialog-content{position:relative;border:0;padding:.5em 1em;background:0 0;overflow:auto}.ui-dialog .ui-dialog-buttonpane{text-align:left;border-width:1px 0 0;background-image:none;margin-top:.5em;padding:.3em 1em .5em .4em}.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset{float:right}.ui-dialog .ui-dialog-buttonpane button{margin:.5em .4em .5em 0;cursor:pointer}.ui-dialog .ui-resizable-se{width:12px;height:12px;right:-5px;bottom:-5px;background-position:16px 16px}.ui-draggable .ui-dialog-titlebar{cursor:move}.ui-menu{list-style:none;padding:2px;margin:0;display:block;outline:0}.ui-menu .ui-menu{margin-top:-3px;position:absolute}.ui-menu .ui-menu-item{margin:0;padding:0;width:100%;list-style-image:url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)}.ui-menu .ui-menu-divider{margin:5px -2px 5px -2px;height:0;font-size:0;line-height:0;border-width:1px 0 0}.ui-menu .ui-menu-item a{text-decoration:none;display:block;padding:2px .4em;line-height:1.5;min-height:0;font-weight:400}.ui-menu .ui-menu-item a.ui-state-focus,.ui-menu .ui-menu-item a.ui-state-active{font-weight:400;margin:-1px}.ui-menu .ui-state-disabled{font-weight:400;margin:.4em 0 .2em;line-height:1.5}.ui-menu .ui-state-disabled a{cursor:default}.ui-menu-icons{position:relative}.ui-menu-icons .ui-menu-item a{position:relative;padding-left:2em}.ui-menu .ui-icon{position:absolute;top:.2em;left:.2em}.ui-menu .ui-menu-icon{position:static;float:right}.ui-progressbar{height:2em;text-align:left;overflow:hidden}.ui-progressbar .ui-progressbar-value{margin:-1px;height:100%}.ui-progressbar .ui-progressbar-overlay{background:url(images/animated-overlay.gif);height:100%;filter:alpha(opacity=25);opacity:.25}.ui-progressbar-indeterminate .ui-progressbar-value{background-image:none}.ui-resizable{position:relative}.ui-resizable-handle{position:absolute;font-size:.1px;display:block}.ui-resizable-disabled .ui-resizable-handle,.ui-resizable-autohide .ui-resizable-handle{display:none}.ui-resizable-n{cursor:n-resize;height:7px;width:100%;top:-5px;left:0}.ui-resizable-s{cursor:s-resize;height:7px;width:100%;bottom:-5px;left:0}.ui-resizable-e{cursor:e-resize;width:7px;right:-5px;top:0;height:100%}.ui-resizable-w{cursor:w-resize;width:7px;left:-5px;top:0;height:100%}.ui-resizable-se{cursor:se-resize;width:12px;height:12px;right:1px;bottom:1px}.ui-resizable-sw{cursor:sw-resize;width:9px;height:9px;left:-5px;bottom:-5px}.ui-resizable-nw{cursor:nw-resize;width:9px;height:9px;left:-5px;top:-5px}.ui-resizable-ne{cursor:ne-resize;width:9px;height:9px;right:-5px;top:-5px}.ui-selectable-helper{position:absolute;z-index:100;border:1px dotted #000}.ui-slider{position:relative;text-align:left}.ui-slider .ui-slider-handle{position:absolute;z-index:2;width:1.2em;height:1.2em;cursor:default}.ui-slider .ui-slider-range{position:absolute;z-index:1;font-size:.7em;display:block;border:0;background-position:0 0}.ui-slider.ui-state-disabled .ui-slider-handle,.ui-slider.ui-state-disabled .ui-slider-range{filter:inherit}.ui-slider-horizontal{height:.8em}.ui-slider-horizontal .ui-slider-handle{top:-.3em;margin-left:-.6em}.ui-slider-horizontal .ui-slider-range{top:0;height:100%}.ui-slider-horizontal .ui-slider-range-min{left:0}.ui-slider-horizontal .ui-slider-range-max{right:0}.ui-slider-vertical{width:.8em;height:100px}.ui-slider-vertical .ui-slider-handle{left:-.3em;margin-left:0;margin-bottom:-.6em}.ui-slider-vertical .ui-slider-range{left:0;width:100%}.ui-slider-vertical .ui-slider-range-min{bottom:0}.ui-slider-vertical .ui-slider-range-max{top:0}.ui-spinner{position:relative;display:inline-block;overflow:hidden;padding:0;vertical-align:middle}.ui-spinner-input{border:0;background:0 0;color:inherit;padding:0;margin:.2em 0;vertical-align:middle;margin-left:.4em;margin-right:22px}.ui-spinner-button{width:16px;height:50%;font-size:.5em;padding:0;margin:0;text-align:center;position:absolute;cursor:default;display:block;overflow:hidden;right:0}.ui-spinner a.ui-spinner-button{border-top:0;border-bottom:0;border-right:0}.ui-spinner .ui-icon{position:absolute;margin-top:-8px;top:50%;left:0}.ui-spinner-up{top:0}.ui-spinner-down{bottom:0}.ui-spinner .ui-icon-triangle-1-s{background-position:-65px -16px}.ui-tabs{position:relative;padding:.2em}.ui-tabs .ui-tabs-nav{margin:0;padding:.2em .2em 0}.ui-tabs .ui-tabs-nav li{list-style:none;float:left;position:relative;top:0;margin:1px .2em 0 0;border-bottom-width:0;padding:0;white-space:nowrap}.ui-tabs .ui-tabs-nav .ui-tabs-anchor{float:left;padding:.5em 1em;text-decoration:none}.ui-tabs .ui-tabs-nav li.ui-tabs-active{margin-bottom:-1px;padding-bottom:1px}.ui-tabs .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor,.ui-tabs .ui-tabs-nav li.ui-state-disabled .ui-tabs-anchor,.ui-tabs .ui-tabs-nav li.ui-tabs-loading .ui-tabs-anchor{cursor:text}.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor{cursor:pointer}.ui-tabs .ui-tabs-panel{display:block;border-width:0;padding:1em 1.4em;background:0 0}.ui-tooltip{padding:8px;position:absolute;z-index:9999;max-width:300px;-webkit-box-shadow:0 0 5px #aaa;box-shadow:0 0 5px #aaa}body .ui-tooltip{border-width:2px}.ui-widget{font-family:Verdana,Arial,sans-serif;font-size:1.1em}.ui-widget .ui-widget{font-size:1em}.ui-widget input,.ui-widget select,.ui-widget textarea,.ui-widget button{font-family:Verdana,Arial,sans-serif;font-size:1em}.ui-widget-content{border:1px solid #aaa;background:#fff url(images/ui-bg_flat_75_ffffff_40x100.png) 50% 50% repeat-x;color:#222}.ui-widget-content a{color:#222}.ui-widget-header{border:1px solid #aaa;background:#ccc url(images/ui-bg_highlight-soft_75_cccccc_1x100.png) 50% 50% repeat-x;color:#222;font-weight:700}.ui-widget-header a{color:#222}.ui-state-default,.ui-widget-content .ui-state-default,.ui-widget-header .ui-state-default{border:1px solid #d3d3d3;background:#e6e6e6 url(images/ui-bg_glass_75_e6e6e6_1x400.png) 50% 50% repeat-x;font-weight:400;color:#555}.ui-state-default a,.ui-state-default a:link,.ui-state-default a:visited{color:#555;text-decoration:none}.ui-state-hover,.ui-widget-content .ui-state-hover,.ui-widget-header .ui-state-hover,.ui-state-focus,.ui-widget-content .ui-state-focus,.ui-widget-header .ui-state-focus{border:1px solid #999;background:#dadada url(images/ui-bg_glass_75_dadada_1x400.png) 50% 50% repeat-x;font-weight:400;color:#212121}.ui-state-hover a,.ui-state-hover a:hover,.ui-state-hover a:link,.ui-state-hover a:visited,.ui-state-focus a,.ui-state-focus a:hover,.ui-state-focus a:link,.ui-state-focus a:visited{color:#212121;text-decoration:none}.ui-state-active,.ui-widget-content .ui-state-active,.ui-widget-header .ui-state-active{border:1px solid #aaa;background:#fff url(images/ui-bg_glass_65_ffffff_1x400.png) 50% 50% repeat-x;font-weight:400;color:#212121}.ui-state-active a,.ui-state-active a:link,.ui-state-active a:visited{color:#212121;text-decoration:none}.ui-state-highlight,.ui-widget-content .ui-state-highlight,.ui-widget-header .ui-state-highlight{border:1px solid #fcefa1;background:#fbf9ee url(images/ui-bg_glass_55_fbf9ee_1x400.png) 50% 50% repeat-x;color:#363636}.ui-state-highlight a,.ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a{color:#363636}.ui-state-error,.ui-widget-content .ui-state-error,.ui-widget-header .ui-state-error{border:1px solid #cd0a0a;background:#fef1ec url(images/ui-bg_glass_95_fef1ec_1x400.png) 50% 50% repeat-x;color:#cd0a0a}.ui-state-error a,.ui-widget-content .ui-state-error a,.ui-widget-header .ui-state-error a{color:#cd0a0a}.ui-state-error-text,.ui-widget-content .ui-state-error-text,.ui-widget-header .ui-state-error-text{color:#cd0a0a}.ui-priority-primary,.ui-widget-content .ui-priority-primary,.ui-widget-header .ui-priority-primary{font-weight:700}.ui-priority-secondary,.ui-widget-content .ui-priority-secondary,.ui-widget-header .ui-priority-secondary{opacity:.7;filter:Alpha(Opacity=70);font-weight:400}.ui-state-disabled,.ui-widget-content .ui-state-disabled,.ui-widget-header .ui-state-disabled{opacity:.35;filter:Alpha(Opacity=35);background-image:none}.ui-state-disabled .ui-icon{filter:Alpha(Opacity=35)}.ui-icon{width:16px;height:16px}.ui-icon,.ui-widget-content .ui-icon{background-image:url(images/ui-icons_222222_256x240.png)}.ui-widget-header .ui-icon{background-image:url(images/ui-icons_222222_256x240.png)}.ui-state-default .ui-icon{background-image:url(images/ui-icons_888888_256x240.png)}.ui-state-hover .ui-icon,.ui-state-focus .ui-icon{background-image:url(images/ui-icons_454545_256x240.png)}.ui-state-active .ui-icon{background-image:url(images/ui-icons_454545_256x240.png)}.ui-state-highlight .ui-icon{background-image:url(images/ui-icons_2e83ff_256x240.png)}.ui-state-error .ui-icon,.ui-state-error-text .ui-icon{background-image:url(images/ui-icons_cd0a0a_256x240.png)}.ui-icon-blank{background-position:16px 16px}.ui-icon-carat-1-n{background-position:0 0}.ui-icon-carat-1-ne{background-position:-16px 0}.ui-icon-carat-1-e{background-position:-32px 0}.ui-icon-carat-1-se{background-position:-48px 0}.ui-icon-carat-1-s{background-position:-64px 0}.ui-icon-carat-1-sw{background-position:-80px 0}.ui-icon-carat-1-w{background-position:-96px 0}.ui-icon-carat-1-nw{background-position:-112px 0}.ui-icon-carat-2-n-s{background-position:-128px 0}.ui-icon-carat-2-e-w{background-position:-144px 0}.ui-icon-triangle-1-n{background-position:0 -16px}.ui-icon-triangle-1-ne{background-position:-16px -16px}.ui-icon-triangle-1-e{background-position:-32px -16px}.ui-icon-triangle-1-se{background-position:-48px -16px}.ui-icon-triangle-1-s{background-position:-64px -16px}.ui-icon-triangle-1-sw{background-position:-80px -16px}.ui-icon-triangle-1-w{background-position:-96px -16px}.ui-icon-triangle-1-nw{background-position:-112px -16px}.ui-icon-triangle-2-n-s{background-position:-128px -16px}.ui-icon-triangle-2-e-w{background-position:-144px -16px}.ui-icon-arrow-1-n{background-position:0 -32px}.ui-icon-arrow-1-ne{background-position:-16px -32px}.ui-icon-arrow-1-e{background-position:-32px -32px}.ui-icon-arrow-1-se{background-position:-48px -32px}.ui-icon-arrow-1-s{background-position:-64px -32px}.ui-icon-arrow-1-sw{background-position:-80px -32px}.ui-icon-arrow-1-w{background-position:-96px -32px}.ui-icon-arrow-1-nw{background-position:-112px -32px}.ui-icon-arrow-2-n-s{background-position:-128px -32px}.ui-icon-arrow-2-ne-sw{background-position:-144px -32px}.ui-icon-arrow-2-e-w{background-position:-160px -32px}.ui-icon-arrow-2-se-nw{background-position:-176px -32px}.ui-icon-arrowstop-1-n{background-position:-192px -32px}.ui-icon-arrowstop-1-e{background-position:-208px -32px}.ui-icon-arrowstop-1-s{background-position:-224px -32px}.ui-icon-arrowstop-1-w{background-position:-240px -32px}.ui-icon-arrowthick-1-n{background-position:0 -48px}.ui-icon-arrowthick-1-ne{background-position:-16px -48px}.ui-icon-arrowthick-1-e{background-position:-32px -48px}.ui-icon-arrowthick-1-se{background-position:-48px -48px}.ui-icon-arrowthick-1-s{background-position:-64px -48px}.ui-icon-arrowthick-1-sw{background-position:-80px -48px}.ui-icon-arrowthick-1-w{background-position:-96px -48px}.ui-icon-arrowthick-1-nw{background-position:-112px -48px}.ui-icon-arrowthick-2-n-s{background-position:-128px -48px}.ui-icon-arrowthick-2-ne-sw{background-position:-144px -48px}.ui-icon-arrowthick-2-e-w{background-position:-160px -48px}.ui-icon-arrowthick-2-se-nw{background-position:-176px -48px}.ui-icon-arrowthickstop-1-n{background-position:-192px -48px}.ui-icon-arrowthickstop-1-e{background-position:-208px -48px}.ui-icon-arrowthickstop-1-s{background-position:-224px -48px}.ui-icon-arrowthickstop-1-w{background-position:-240px -48px}.ui-icon-arrowreturnthick-1-w{background-position:0 -64px}.ui-icon-arrowreturnthick-1-n{background-position:-16px -64px}.ui-icon-arrowreturnthick-1-e{background-position:-32px -64px}.ui-icon-arrowreturnthick-1-s{background-position:-48px -64px}.ui-icon-arrowreturn-1-w{background-position:-64px -64px}.ui-icon-arrowreturn-1-n{background-position:-80px -64px}.ui-icon-arrowreturn-1-e{background-position:-96px -64px}.ui-icon-arrowreturn-1-s{background-position:-112px -64px}.ui-icon-arrowrefresh-1-w{background-position:-128px -64px}.ui-icon-arrowrefresh-1-n{background-position:-144px -64px}.ui-icon-arrowrefresh-1-e{background-position:-160px -64px}.ui-icon-arrowrefresh-1-s{background-position:-176px -64px}.ui-icon-arrow-4{background-position:0 -80px}.ui-icon-arrow-4-diag{background-position:-16px -80px}.ui-icon-extlink{background-position:-32px -80px}.ui-icon-newwin{background-position:-48px -80px}.ui-icon-refresh{background-position:-64px -80px}.ui-icon-shuffle{background-position:-80px -80px}.ui-icon-transfer-e-w{background-position:-96px -80px}.ui-icon-transferthick-e-w{background-position:-112px -80px}.ui-icon-folder-collapsed{background-position:0 -96px}.ui-icon-folder-open{background-position:-16px -96px}.ui-icon-document{background-position:-32px -96px}.ui-icon-document-b{background-position:-48px -96px}.ui-icon-note{background-position:-64px -96px}.ui-icon-mail-closed{background-position:-80px -96px}.ui-icon-mail-open{background-position:-96px -96px}.ui-icon-suitcase{background-position:-112px -96px}.ui-icon-comment{background-position:-128px -96px}.ui-icon-person{background-position:-144px -96px}.ui-icon-print{background-position:-160px -96px}.ui-icon-trash{background-position:-176px -96px}.ui-icon-locked{background-position:-192px -96px}.ui-icon-unlocked{background-position:-208px -96px}.ui-icon-bookmark{background-position:-224px -96px}.ui-icon-tag{background-position:-240px -96px}.ui-icon-home{background-position:0 -112px}.ui-icon-flag{background-position:-16px -112px}.ui-icon-calendar{background-position:-32px -112px}.ui-icon-cart{background-position:-48px -112px}.ui-icon-pencil{background-position:-64px -112px}.ui-icon-clock{background-position:-80px -112px}.ui-icon-disk{background-position:-96px -112px}.ui-icon-calculator{background-position:-112px -112px}.ui-icon-zoomin{background-position:-128px -112px}.ui-icon-zoomout{background-position:-144px -112px}.ui-icon-search{background-position:-160px -112px}.ui-icon-wrench{background-position:-176px -112px}.ui-icon-gear{background-position:-192px -112px}.ui-icon-heart{background-position:-208px -112px}.ui-icon-star{background-position:-224px -112px}.ui-icon-link{background-position:-240px -112px}.ui-icon-cancel{background-position:0 -128px}.ui-icon-plus{background-position:-16px -128px}.ui-icon-plusthick{background-position:-32px -128px}.ui-icon-minus{background-position:-48px -128px}.ui-icon-minusthick{background-position:-64px -128px}.ui-icon-close{background-position:-80px -128px}.ui-icon-closethick{background-position:-96px -128px}.ui-icon-key{background-position:-112px -128px}.ui-icon-lightbulb{background-position:-128px -128px}.ui-icon-scissors{background-position:-144px -128px}.ui-icon-clipboard{background-position:-160px -128px}.ui-icon-copy{background-position:-176px -128px}.ui-icon-contact{background-position:-192px -128px}.ui-icon-image{background-position:-208px -128px}.ui-icon-video{background-position:-224px -128px}.ui-icon-script{background-position:-240px -128px}.ui-icon-alert{background-position:0 -144px}.ui-icon-info{background-position:-16px -144px}.ui-icon-notice{background-position:-32px -144px}.ui-icon-help{background-position:-48px -144px}.ui-icon-check{background-position:-64px -144px}.ui-icon-bullet{background-position:-80px -144px}.ui-icon-radio-on{background-position:-96px -144px}.ui-icon-radio-off{background-position:-112px -144px}.ui-icon-pin-w{background-position:-128px -144px}.ui-icon-pin-s{background-position:-144px -144px}.ui-icon-play{background-position:0 -160px}.ui-icon-pause{background-position:-16px -160px}.ui-icon-seek-next{background-position:-32px -160px}.ui-icon-seek-prev{background-position:-48px -160px}.ui-icon-seek-end{background-position:-64px -160px}.ui-icon-seek-start{background-position:-80px -160px}.ui-icon-seek-first{background-position:-80px -160px}.ui-icon-stop{background-position:-96px -160px}.ui-icon-eject{background-position:-112px -160px}.ui-icon-volume-off{background-position:-128px -160px}.ui-icon-volume-on{background-position:-144px -160px}.ui-icon-power{background-position:0 -176px}.ui-icon-signal-diag{background-position:-16px -176px}.ui-icon-signal{background-position:-32px -176px}.ui-icon-battery-0{background-position:-48px -176px}.ui-icon-battery-1{background-position:-64px -176px}.ui-icon-battery-2{background-position:-80px -176px}.ui-icon-battery-3{background-position:-96px -176px}.ui-icon-circle-plus{background-position:0 -192px}.ui-icon-circle-minus{background-position:-16px -192px}.ui-icon-circle-close{background-position:-32px -192px}.ui-icon-circle-triangle-e{background-position:-48px -192px}.ui-icon-circle-triangle-s{background-position:-64px -192px}.ui-icon-circle-triangle-w{background-position:-80px -192px}.ui-icon-circle-triangle-n{background-position:-96px -192px}.ui-icon-circle-arrow-e{background-position:-112px -192px}.ui-icon-circle-arrow-s{background-position:-128px -192px}.ui-icon-circle-arrow-w{background-position:-144px -192px}.ui-icon-circle-arrow-n{background-position:-160px -192px}.ui-icon-circle-zoomin{background-position:-176px -192px}.ui-icon-circle-zoomout{background-position:-192px -192px}.ui-icon-circle-check{background-position:-208px -192px}.ui-icon-circlesmall-plus{background-position:0 -208px}.ui-icon-circlesmall-minus{background-position:-16px -208px}.ui-icon-circlesmall-close{background-position:-32px -208px}.ui-icon-squaresmall-plus{background-position:-48px -208px}.ui-icon-squaresmall-minus{background-position:-64px -208px}.ui-icon-squaresmall-close{background-position:-80px -208px}.ui-icon-grip-dotted-vertical{background-position:0 -224px}.ui-icon-grip-dotted-horizontal{background-position:-16px -224px}.ui-icon-grip-solid-vertical{background-position:-32px -224px}.ui-icon-grip-solid-horizontal{background-position:-48px -224px}.ui-icon-gripsmall-diagonal-se{background-position:-64px -224px}.ui-icon-grip-diagonal-se{background-position:-80px -224px}.ui-corner-all,.ui-corner-top,.ui-corner-left,.ui-corner-tl{border-top-left-radius:4px}.ui-corner-all,.ui-corner-top,.ui-corner-right,.ui-corner-tr{border-top-right-radius:4px}.ui-corner-all,.ui-corner-bottom,.ui-corner-left,.ui-corner-bl{border-bottom-left-radius:4px}.ui-corner-all,.ui-corner-bottom,.ui-corner-right,.ui-corner-br{border-bottom-right-radius:4px}.ui-widget-overlay{background:#aaa url(images/ui-bg_flat_0_aaaaaa_40x100.png) 50% 50% repeat-x;opacity:.3;filter:Alpha(Opacity=30)}.ui-widget-shadow{margin:-8px 0 0 -8px;padding:8px;background:#aaa url(images/ui-bg_flat_0_aaaaaa_40x100.png) 50% 50% repeat-x;opacity:.3;filter:Alpha(Opacity=30);border-radius:8px}svg.timeslider{width:100%;height:100%;background:#fff;opacity:.8;border:1px solid #fff;border-radius:4px;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}svg.timeslider .axis{fill:none}svg.timeslider .axis .tick{stroke:#646464;shape-rendering:crispEdges}svg.timeslider .axis .tick text{stroke:#000;font:.7em sans-serif}svg.timeslider .axis .tick.minor{stroke:rgba(100,100,100,.25);shape-rendering:crispEdges}svg.timeslider .axis .domain{stroke:#646464;shape-rendering:crispEdges}svg.timeslider .brush .extent{opacity:.9}svg.timeslider .datasets path{stroke-width:3;shape-rendering:crispEdges}div.tooltip{position:absolute;text-align:center;width:auto;height:auto;padding:2px;font:12px sans-serif;background:#fff;border:0;border-radius:8px;pointer-events:none;opacity:1}/*! - * Bootstrap v3.0.3 (http://getbootstrap.com) - * Copyright 2013 Twitter, Inc. - * Licensed under http://www.apache.org/licenses/LICENSE-2.0 - *//*! normalize.css v2.1.3 | MIT License | git.io/normalize */article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}audio,canvas,video{display:inline-block}audio:not([controls]){display:none;height:0}[hidden],template{display:none}html{font-family:sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}a{background:transparent}a:focus{outline:thin dotted}a:active,a:hover{outline:0}h1{margin:.67em 0;font-size:2em}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}hr{height:0;-moz-box-sizing:content-box;box-sizing:content-box}mark{color:#000;background:#ff0}code,kbd,pre,samp{font-family:monospace,serif;font-size:1em}pre{white-space:pre-wrap}q{quotes:"\201C" "\201D" "\2018" "\2019"}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:0}fieldset{padding:.35em .625em .75em;margin:0 2px;border:1px solid silver}legend{padding:0;border:0}button,input,select,textarea{margin:0;font-family:inherit;font-size:100%}button,input{line-height:normal}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{cursor:pointer;-webkit-appearance:button}button[disabled],html input[disabled]{cursor:default}input[type=checkbox],input[type=radio]{padding:0;box-sizing:border-box}input[type=search]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}textarea{overflow:auto;vertical-align:top}table{border-collapse:collapse;border-spacing:0}@media print{*{color:#000!important;text-shadow:none!important;background:transparent!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100%!important}@page{margin:2cm .5cm}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}select{background:#fff!important}.navbar{display:none}.table td,.table th{background-color:#fff!important}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table-bordered th,.table-bordered td{border:1px solid #ddd!important}}*,:before,:after{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:62.5%;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.428571429;color:#333;background-color:#fff}input,button,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#428bca;text-decoration:none}a:hover,a:focus{color:#2a6496;text-decoration:underline}a:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}img{vertical-align:middle}.img-responsive{display:block;height:auto;max-width:100%}.img-rounded{border-radius:6px}.img-thumbnail{display:inline-block;height:auto;max-width:100%;padding:4px;line-height:1.428571429;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:500;line-height:1.1;color:inherit}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small,.h1 small,.h2 small,.h3 small,.h4 small,.h5 small,.h6 small,h1 .small,h2 .small,h3 .small,h4 .small,h5 .small,h6 .small,.h1 .small,.h2 .small,.h3 .small,.h4 .small,.h5 .small,.h6 .small{font-weight:400;line-height:1;color:#999}h1,h2,h3{margin-top:20px;margin-bottom:10px}h1 small,h2 small,h3 small,h1 .small,h2 .small,h3 .small{font-size:65%}h4,h5,h6{margin-top:10px;margin-bottom:10px}h4 small,h5 small,h6 small,h4 .small,h5 .small,h6 .small{font-size:75%}h1,.h1{font-size:36px}h2,.h2{font-size:30px}h3,.h3{font-size:24px}h4,.h4{font-size:18px}h5,.h5{font-size:14px}h6,.h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:200;line-height:1.4}@media(min-width:768px){.lead{font-size:21px}}small,.small{font-size:85%}cite{font-style:normal}.text-muted{color:#999}.text-primary{color:#428bca}.text-primary:hover{color:#3071a9}.text-warning{color:#8a6d3b}.text-warning:hover{color:#66512c}.text-danger{color:#a94442}.text-danger:hover{color:#843534}.text-success{color:#3c763d}.text-success:hover{color:#2b542c}.text-info{color:#31708f}.text-info:hover{color:#245269}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ul,ol{margin-top:0;margin-bottom:10px}ul ul,ol ul,ul ol,ol ol{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline>li{display:inline-block;padding-right:5px;padding-left:5px}.list-inline>li:first-child{padding-left:0}dl{margin-top:0;margin-bottom:20px}dt,dd{line-height:1.428571429}dt{font-weight:700}dd{margin-left:0}@media(min-width:768px){.dl-horizontal dt{float:left;width:160px;overflow:hidden;clear:left;text-align:right;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}.dl-horizontal dd:before,.dl-horizontal dd:after{display:table;content:" "}.dl-horizontal dd:after{clear:both}.dl-horizontal dd:before,.dl-horizontal dd:after{display:table;content:" "}.dl-horizontal dd:after{clear:both}}abbr[title],abbr[data-original-title]{cursor:help;border-bottom:1px dotted #999}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;border-left:5px solid #eee}blockquote p{font-size:17.5px;font-weight:300;line-height:1.25}blockquote p:last-child{margin-bottom:0}blockquote small,blockquote .small{display:block;line-height:1.428571429;color:#999}blockquote small:before,blockquote .small:before{content:'\2014 \00A0'}blockquote.pull-right{padding-right:15px;padding-left:0;border-right:5px solid #eee;border-left:0}blockquote.pull-right p,blockquote.pull-right small,blockquote.pull-right .small{text-align:right}blockquote.pull-right small:before,blockquote.pull-right .small:before{content:''}blockquote.pull-right small:after,blockquote.pull-right .small:after{content:'\00A0 \2014'}blockquote:before,blockquote:after{content:""}address{margin-bottom:20px;font-style:normal;line-height:1.428571429}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;white-space:nowrap;background-color:#f9f2f4;border-radius:4px}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.428571429;color:#333;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.container:before,.container:after{display:table;content:" "}.container:after{clear:both}.container:before,.container:after{display:table;content:" "}.container:after{clear:both}@media(min-width:768px){.container{width:750px}}@media(min-width:992px){.container{width:970px}}@media(min-width:1200px){.container{width:1170px}}.row{margin-right:-15px;margin-left:-15px}.row:before,.row:after{display:table;content:" "}.row:after{clear:both}.row:before,.row:after{display:table;content:" "}.row:after{clear:both}.col-xs-1,.col-sm-1,.col-md-1,.col-lg-1,.col-xs-2,.col-sm-2,.col-md-2,.col-lg-2,.col-xs-3,.col-sm-3,.col-md-3,.col-lg-3,.col-xs-4,.col-sm-4,.col-md-4,.col-lg-4,.col-xs-5,.col-sm-5,.col-md-5,.col-lg-5,.col-xs-6,.col-sm-6,.col-md-6,.col-lg-6,.col-xs-7,.col-sm-7,.col-md-7,.col-lg-7,.col-xs-8,.col-sm-8,.col-md-8,.col-lg-8,.col-xs-9,.col-sm-9,.col-md-9,.col-lg-9,.col-xs-10,.col-sm-10,.col-md-10,.col-lg-10,.col-xs-11,.col-sm-11,.col-md-11,.col-lg-11,.col-xs-12,.col-sm-12,.col-md-12,.col-lg-12{position:relative;min-height:1px;padding-right:15px;padding-left:15px}.col-xs-1,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9,.col-xs-10,.col-xs-11,.col-xs-12{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666666666666%}.col-xs-10{width:83.33333333333334%}.col-xs-9{width:75%}.col-xs-8{width:66.66666666666666%}.col-xs-7{width:58.333333333333336%}.col-xs-6{width:50%}.col-xs-5{width:41.66666666666667%}.col-xs-4{width:33.33333333333333%}.col-xs-3{width:25%}.col-xs-2{width:16.666666666666664%}.col-xs-1{width:8.333333333333332%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666666666666%}.col-xs-pull-10{right:83.33333333333334%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666666666666%}.col-xs-pull-7{right:58.333333333333336%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666666666667%}.col-xs-pull-4{right:33.33333333333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.666666666666664%}.col-xs-pull-1{right:8.333333333333332%}.col-xs-pull-0{right:0}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666666666666%}.col-xs-push-10{left:83.33333333333334%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666666666666%}.col-xs-push-7{left:58.333333333333336%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666666666667%}.col-xs-push-4{left:33.33333333333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.666666666666664%}.col-xs-push-1{left:8.333333333333332%}.col-xs-push-0{left:0}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666666666666%}.col-xs-offset-10{margin-left:83.33333333333334%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666666666666%}.col-xs-offset-7{margin-left:58.333333333333336%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666666666667%}.col-xs-offset-4{margin-left:33.33333333333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.666666666666664%}.col-xs-offset-1{margin-left:8.333333333333332%}.col-xs-offset-0{margin-left:0}@media(min-width:768px){.col-sm-1,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-10,.col-sm-11,.col-sm-12{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666666666666%}.col-sm-10{width:83.33333333333334%}.col-sm-9{width:75%}.col-sm-8{width:66.66666666666666%}.col-sm-7{width:58.333333333333336%}.col-sm-6{width:50%}.col-sm-5{width:41.66666666666667%}.col-sm-4{width:33.33333333333333%}.col-sm-3{width:25%}.col-sm-2{width:16.666666666666664%}.col-sm-1{width:8.333333333333332%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666666666666%}.col-sm-pull-10{right:83.33333333333334%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666666666666%}.col-sm-pull-7{right:58.333333333333336%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666666666667%}.col-sm-pull-4{right:33.33333333333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.666666666666664%}.col-sm-pull-1{right:8.333333333333332%}.col-sm-pull-0{right:0}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666666666666%}.col-sm-push-10{left:83.33333333333334%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666666666666%}.col-sm-push-7{left:58.333333333333336%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666666666667%}.col-sm-push-4{left:33.33333333333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.666666666666664%}.col-sm-push-1{left:8.333333333333332%}.col-sm-push-0{left:0}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666666666666%}.col-sm-offset-10{margin-left:83.33333333333334%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666666666666%}.col-sm-offset-7{margin-left:58.333333333333336%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666666666667%}.col-sm-offset-4{margin-left:33.33333333333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.666666666666664%}.col-sm-offset-1{margin-left:8.333333333333332%}.col-sm-offset-0{margin-left:0}}@media(min-width:992px){.col-md-1,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-10,.col-md-11,.col-md-12{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666666666666%}.col-md-10{width:83.33333333333334%}.col-md-9{width:75%}.col-md-8{width:66.66666666666666%}.col-md-7{width:58.333333333333336%}.col-md-6{width:50%}.col-md-5{width:41.66666666666667%}.col-md-4{width:33.33333333333333%}.col-md-3{width:25%}.col-md-2{width:16.666666666666664%}.col-md-1{width:8.333333333333332%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666666666666%}.col-md-pull-10{right:83.33333333333334%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666666666666%}.col-md-pull-7{right:58.333333333333336%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666666666667%}.col-md-pull-4{right:33.33333333333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.666666666666664%}.col-md-pull-1{right:8.333333333333332%}.col-md-pull-0{right:0}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666666666666%}.col-md-push-10{left:83.33333333333334%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666666666666%}.col-md-push-7{left:58.333333333333336%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666666666667%}.col-md-push-4{left:33.33333333333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.666666666666664%}.col-md-push-1{left:8.333333333333332%}.col-md-push-0{left:0}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666666666666%}.col-md-offset-10{margin-left:83.33333333333334%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666666666666%}.col-md-offset-7{margin-left:58.333333333333336%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666666666667%}.col-md-offset-4{margin-left:33.33333333333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.666666666666664%}.col-md-offset-1{margin-left:8.333333333333332%}.col-md-offset-0{margin-left:0}}@media(min-width:1200px){.col-lg-1,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-10,.col-lg-11,.col-lg-12{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666666666666%}.col-lg-10{width:83.33333333333334%}.col-lg-9{width:75%}.col-lg-8{width:66.66666666666666%}.col-lg-7{width:58.333333333333336%}.col-lg-6{width:50%}.col-lg-5{width:41.66666666666667%}.col-lg-4{width:33.33333333333333%}.col-lg-3{width:25%}.col-lg-2{width:16.666666666666664%}.col-lg-1{width:8.333333333333332%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666666666666%}.col-lg-pull-10{right:83.33333333333334%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666666666666%}.col-lg-pull-7{right:58.333333333333336%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666666666667%}.col-lg-pull-4{right:33.33333333333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.666666666666664%}.col-lg-pull-1{right:8.333333333333332%}.col-lg-pull-0{right:0}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666666666666%}.col-lg-push-10{left:83.33333333333334%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666666666666%}.col-lg-push-7{left:58.333333333333336%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666666666667%}.col-lg-push-4{left:33.33333333333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.666666666666664%}.col-lg-push-1{left:8.333333333333332%}.col-lg-push-0{left:0}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666666666666%}.col-lg-offset-10{margin-left:83.33333333333334%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666666666666%}.col-lg-offset-7{margin-left:58.333333333333336%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666666666667%}.col-lg-offset-4{margin-left:33.33333333333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.666666666666664%}.col-lg-offset-1{margin-left:8.333333333333332%}.col-lg-offset-0{margin-left:0}}table{max-width:100%;background-color:transparent}th{text-align:left}.table{width:100%;margin-bottom:20px}.table>thead>tr>th,.table>tbody>tr>th,.table>tfoot>tr>th,.table>thead>tr>td,.table>tbody>tr>td,.table>tfoot>tr>td{padding:8px;line-height:1.428571429;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>th,.table>caption+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>td,.table>thead:first-child>tr:first-child>td{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>thead>tr>th,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>tbody>tr>td,.table-condensed>tfoot>tr>td{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>tbody>tr>td,.table-bordered>tfoot>tr>td{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>thead>tr>td{border-bottom-width:2px}.table-striped>tbody>tr:nth-child(odd)>td,.table-striped>tbody>tr:nth-child(odd)>th{background-color:#f9f9f9}.table-hover>tbody>tr:hover>td,.table-hover>tbody>tr:hover>th{background-color:#f5f5f5}table col[class*=col-]{position:static;display:table-column;float:none}table td[class*=col-],table th[class*=col-]{display:table-cell;float:none}.table>thead>tr>.active,.table>tbody>tr>.active,.table>tfoot>tr>.active,.table>thead>.active>td,.table>tbody>.active>td,.table>tfoot>.active>td,.table>thead>.active>th,.table>tbody>.active>th,.table>tfoot>.active>th{background-color:#f5f5f5}.table-hover>tbody>tr>.active:hover,.table-hover>tbody>.active:hover>td,.table-hover>tbody>.active:hover>th{background-color:#e8e8e8}.table>thead>tr>.success,.table>tbody>tr>.success,.table>tfoot>tr>.success,.table>thead>.success>td,.table>tbody>.success>td,.table>tfoot>.success>td,.table>thead>.success>th,.table>tbody>.success>th,.table>tfoot>.success>th{background-color:#dff0d8}.table-hover>tbody>tr>.success:hover,.table-hover>tbody>.success:hover>td,.table-hover>tbody>.success:hover>th{background-color:#d0e9c6}.table>thead>tr>.danger,.table>tbody>tr>.danger,.table>tfoot>tr>.danger,.table>thead>.danger>td,.table>tbody>.danger>td,.table>tfoot>.danger>td,.table>thead>.danger>th,.table>tbody>.danger>th,.table>tfoot>.danger>th{background-color:#f2dede}.table-hover>tbody>tr>.danger:hover,.table-hover>tbody>.danger:hover>td,.table-hover>tbody>.danger:hover>th{background-color:#ebcccc}.table>thead>tr>.warning,.table>tbody>tr>.warning,.table>tfoot>tr>.warning,.table>thead>.warning>td,.table>tbody>.warning>td,.table>tfoot>.warning>td,.table>thead>.warning>th,.table>tbody>.warning>th,.table>tfoot>.warning>th{background-color:#fcf8e3}.table-hover>tbody>tr>.warning:hover,.table-hover>tbody>.warning:hover>td,.table-hover>tbody>.warning:hover>th{background-color:#faf2cc}@media(max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-x:scroll;overflow-y:hidden;border:1px solid #ddd;-ms-overflow-style:-ms-autohiding-scrollbar;-webkit-overflow-scrolling:touch}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>thead>tr>th,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tfoot>tr>td{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>thead>tr>th:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.table-responsive>.table-bordered>thead>tr>th:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>th,.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>td{border-bottom:0}}fieldset{padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;margin-bottom:5px;font-weight:700}input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=radio],input[type=checkbox]{margin:4px 0 0;margin-top:1px \9;line-height:normal}input[type=file]{display:block}select[multiple],select[size]{height:auto}select optgroup{font-family:inherit;font-size:inherit;font-style:inherit}input[type=file]:focus,input[type=radio]:focus,input[type=checkbox]:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}input[type=number]::-webkit-outer-spin-button,input[type=number]::-webkit-inner-spin-button{height:auto}output{display:block;padding-top:7px;font-size:14px;line-height:1.428571429;color:#555;vertical-align:middle}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.428571429;color:#555;vertical-align:middle;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-webkit-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.form-control:-moz-placeholder{color:#999}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{cursor:not-allowed;background-color:#eee}textarea.form-control{height:auto}.form-group{margin-bottom:15px}.radio,.checkbox{display:block;min-height:20px;padding-left:20px;margin-top:10px;margin-bottom:10px;vertical-align:middle}.radio label,.checkbox label{display:inline;margin-bottom:0;font-weight:400;cursor:pointer}.radio input[type=radio],.radio-inline input[type=radio],.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox]{float:left;margin-left:-20px}.radio+.radio,.checkbox+.checkbox{margin-top:-5px}.radio-inline,.checkbox-inline{display:inline-block;padding-left:20px;margin-bottom:0;font-weight:400;vertical-align:middle;cursor:pointer}.radio-inline+.radio-inline,.checkbox-inline+.checkbox-inline{margin-top:0;margin-left:10px}input[type=radio][disabled],input[type=checkbox][disabled],.radio[disabled],.radio-inline[disabled],.checkbox[disabled],.checkbox-inline[disabled],fieldset[disabled] input[type=radio],fieldset[disabled] input[type=checkbox],fieldset[disabled] .radio,fieldset[disabled] .radio-inline,fieldset[disabled] .checkbox,fieldset[disabled] .checkbox-inline{cursor:not-allowed}.input-sm{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm{height:30px;line-height:30px}textarea.input-sm{height:auto}.input-lg{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}select.input-lg{height:46px;line-height:46px}textarea.input-lg{height:auto}.has-warning .help-block,.has-warning .control-label,.has-warning .radio,.has-warning .checkbox,.has-warning .radio-inline,.has-warning .checkbox-inline{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;background-color:#fcf8e3;border-color:#8a6d3b}.has-error .help-block,.has-error .control-label,.has-error .radio,.has-error .checkbox,.has-error .radio-inline,.has-error .checkbox-inline{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;background-color:#f2dede;border-color:#a94442}.has-success .help-block,.has-success .control-label,.has-success .radio,.has-success .checkbox,.has-success .radio-inline,.has-success .checkbox-inline{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;background-color:#dff0d8;border-color:#3c763d}.form-control-static{margin-bottom:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media(min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block}.form-inline select.form-control{width:auto}.form-inline .radio,.form-inline .checkbox{display:inline-block;padding-left:0;margin-top:0;margin-bottom:0}.form-inline .radio input[type=radio],.form-inline .checkbox input[type=checkbox]{float:none;margin-left:0}}.form-horizontal .control-label,.form-horizontal .radio,.form-horizontal .checkbox,.form-horizontal .radio-inline,.form-horizontal .checkbox-inline{padding-top:7px;margin-top:0;margin-bottom:0}.form-horizontal .radio,.form-horizontal .checkbox{min-height:27px}.form-horizontal .form-group{margin-right:-15px;margin-left:-15px}.form-horizontal .form-group:before,.form-horizontal .form-group:after{display:table;content:" "}.form-horizontal .form-group:after{clear:both}.form-horizontal .form-group:before,.form-horizontal .form-group:after{display:table;content:" "}.form-horizontal .form-group:after{clear:both}.form-horizontal .form-control-static{padding-top:7px}@media(min-width:768px){.form-horizontal .control-label{text-align:right}}.btn{display:inline-block;padding:6px 12px;margin-bottom:0;font-size:14px;font-weight:400;line-height:1.428571429;text-align:center;white-space:nowrap;vertical-align:middle;cursor:pointer;background-image:none;border:1px solid transparent;border-radius:4px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;-o-user-select:none;user-select:none}.btn:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn:hover,.btn:focus{color:#333;text-decoration:none}.btn:active,.btn.active{background-image:none;outline:0;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{pointer-events:none;cursor:not-allowed;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default:hover,.btn-default:focus,.btn-default:active,.btn-default.active,.open .dropdown-toggle.btn-default{color:#333;background-color:#ebebeb;border-color:#adadad}.btn-default:active,.btn-default.active,.open .dropdown-toggle.btn-default{background-image:none}.btn-default.disabled,.btn-default[disabled],fieldset[disabled] .btn-default,.btn-default.disabled:hover,.btn-default[disabled]:hover,fieldset[disabled] .btn-default:hover,.btn-default.disabled:focus,.btn-default[disabled]:focus,fieldset[disabled] .btn-default:focus,.btn-default.disabled:active,.btn-default[disabled]:active,fieldset[disabled] .btn-default:active,.btn-default.disabled.active,.btn-default[disabled].active,fieldset[disabled] .btn-default.active{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#fff}.btn-primary{color:#fff;background-color:#428bca;border-color:#357ebd}.btn-primary:hover,.btn-primary:focus,.btn-primary:active,.btn-primary.active,.open .dropdown-toggle.btn-primary{color:#fff;background-color:#3276b1;border-color:#285e8e}.btn-primary:active,.btn-primary.active,.open .dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled,.btn-primary[disabled],fieldset[disabled] .btn-primary,.btn-primary.disabled:hover,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary:hover,.btn-primary.disabled:focus,.btn-primary[disabled]:focus,fieldset[disabled] .btn-primary:focus,.btn-primary.disabled:active,.btn-primary[disabled]:active,fieldset[disabled] .btn-primary:active,.btn-primary.disabled.active,.btn-primary[disabled].active,fieldset[disabled] .btn-primary.active{background-color:#428bca;border-color:#357ebd}.btn-primary .badge{color:#428bca;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning:hover,.btn-warning:focus,.btn-warning:active,.btn-warning.active,.open .dropdown-toggle.btn-warning{color:#fff;background-color:#ed9c28;border-color:#d58512}.btn-warning:active,.btn-warning.active,.open .dropdown-toggle.btn-warning{background-image:none}.btn-warning.disabled,.btn-warning[disabled],fieldset[disabled] .btn-warning,.btn-warning.disabled:hover,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning:hover,.btn-warning.disabled:focus,.btn-warning[disabled]:focus,fieldset[disabled] .btn-warning:focus,.btn-warning.disabled:active,.btn-warning[disabled]:active,fieldset[disabled] .btn-warning:active,.btn-warning.disabled.active,.btn-warning[disabled].active,fieldset[disabled] .btn-warning.active{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger:hover,.btn-danger:focus,.btn-danger:active,.btn-danger.active,.open .dropdown-toggle.btn-danger{color:#fff;background-color:#d2322d;border-color:#ac2925}.btn-danger:active,.btn-danger.active,.open .dropdown-toggle.btn-danger{background-image:none}.btn-danger.disabled,.btn-danger[disabled],fieldset[disabled] .btn-danger,.btn-danger.disabled:hover,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger:hover,.btn-danger.disabled:focus,.btn-danger[disabled]:focus,fieldset[disabled] .btn-danger:focus,.btn-danger.disabled:active,.btn-danger[disabled]:active,fieldset[disabled] .btn-danger:active,.btn-danger.disabled.active,.btn-danger[disabled].active,fieldset[disabled] .btn-danger.active{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success:hover,.btn-success:focus,.btn-success:active,.btn-success.active,.open .dropdown-toggle.btn-success{color:#fff;background-color:#47a447;border-color:#398439}.btn-success:active,.btn-success.active,.open .dropdown-toggle.btn-success{background-image:none}.btn-success.disabled,.btn-success[disabled],fieldset[disabled] .btn-success,.btn-success.disabled:hover,.btn-success[disabled]:hover,fieldset[disabled] .btn-success:hover,.btn-success.disabled:focus,.btn-success[disabled]:focus,fieldset[disabled] .btn-success:focus,.btn-success.disabled:active,.btn-success[disabled]:active,fieldset[disabled] .btn-success:active,.btn-success.disabled.active,.btn-success[disabled].active,fieldset[disabled] .btn-success.active{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info:hover,.btn-info:focus,.btn-info:active,.btn-info.active,.open .dropdown-toggle.btn-info{color:#fff;background-color:#39b3d7;border-color:#269abc}.btn-info:active,.btn-info.active,.open .dropdown-toggle.btn-info{background-image:none}.btn-info.disabled,.btn-info[disabled],fieldset[disabled] .btn-info,.btn-info.disabled:hover,.btn-info[disabled]:hover,fieldset[disabled] .btn-info:hover,.btn-info.disabled:focus,.btn-info[disabled]:focus,fieldset[disabled] .btn-info:focus,.btn-info.disabled:active,.btn-info[disabled]:active,fieldset[disabled] .btn-info:active,.btn-info.disabled.active,.btn-info[disabled].active,fieldset[disabled] .btn-info.active{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-link{font-weight:400;color:#428bca;cursor:pointer;border-radius:0}.btn-link,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:hover,.btn-link:focus,.btn-link:active{border-color:transparent}.btn-link:hover,.btn-link:focus{color:#2a6496;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,fieldset[disabled] .btn-link:hover,.btn-link[disabled]:focus,fieldset[disabled] .btn-link:focus{color:#999;text-decoration:none}.btn-lg{padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}.btn-sm{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-xs{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%;padding-right:0;padding-left:0}.btn-block+.btn-block{margin-top:5px}input[type=submit].btn-block,input[type=reset].btn-block,input[type=button].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition:height .35s ease;transition:height .35s ease}@font-face{font-family:'Glyphicons Halflings';src:url(../fonts/glyphicons-halflings-regular.eot);src:url(../fonts/glyphicons-halflings-regular.eot?#iefix) format('embedded-opentype'),url(../fonts/glyphicons-halflings-regular.woff) format('woff'),url(../fonts/glyphicons-halflings-regular.ttf) format('truetype'),url(../fonts/glyphicons-halflings-regular.svg#glyphicons-halflingsregular) format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';-webkit-font-smoothing:antialiased;font-style:normal;font-weight:400;line-height:1;-moz-osx-font-smoothing:grayscale}.glyphicon:empty{width:1em}.glyphicon-asterisk:before{content:"\2a"}.glyphicon-plus:before{content:"\2b"}.glyphicon-euro:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px solid;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;font-size:14px;list-style:none;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);box-shadow:0 6px 12px rgba(0,0,0,.175);background-clip:padding-box}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.428571429;color:#333;white-space:nowrap}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{color:#262626;text-decoration:none;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{color:#fff;text-decoration:none;background-color:#428bca;outline:0}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{color:#999}.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{text-decoration:none;cursor:not-allowed;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.428571429;color:#999}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{border-top:0;border-bottom:4px solid;content:""}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:1px}@media(min-width:768px){.navbar-right .dropdown-menu{right:0;left:auto}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;float:left}.btn-group>.btn:hover,.btn-group-vertical>.btn:hover,.btn-group>.btn:focus,.btn-group-vertical>.btn:focus,.btn-group>.btn:active,.btn-group-vertical>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn.active{z-index:2}.btn-group>.btn:focus,.btn-group-vertical>.btn:focus{outline:0}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar:before,.btn-toolbar:after{display:table;content:" "}.btn-toolbar:after{clear:both}.btn-toolbar:before,.btn-toolbar:after{display:table;content:" "}.btn-toolbar:after{clear:both}.btn-toolbar .btn-group{float:left}.btn-toolbar>.btn+.btn,.btn-toolbar>.btn-group+.btn,.btn-toolbar>.btn+.btn-group,.btn-toolbar>.btn-group+.btn-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child>.btn:last-child,.btn-group>.btn-group:first-child>.dropdown-toggle{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:last-child>.btn:first-child{border-bottom-left-radius:0;border-top-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group-xs>.btn{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-group-sm>.btn{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-group-lg>.btn{padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}.btn-group>.btn+.dropdown-toggle{padding-right:8px;padding-left:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-right:12px;padding-left:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group:before,.btn-group-vertical>.btn-group:after{display:table;content:" "}.btn-group-vertical>.btn-group:after{clear:both}.btn-group-vertical>.btn-group:before,.btn-group-vertical>.btn-group:after{display:table;content:" "}.btn-group-vertical>.btn-group:after{clear:both}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-right-radius:0;border-bottom-left-radius:4px;border-top-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child>.btn:last-child,.btn-group-vertical>.btn-group:first-child>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child>.btn:first-child{border-top-right-radius:0;border-top-left-radius:0}.btn-group-justified{display:table;width:100%;border-collapse:separate;table-layout:fixed}.btn-group-justified>.btn,.btn-group-justified>.btn-group{display:table-cell;float:none;width:1%}.btn-group-justified>.btn-group .btn{width:100%}[data-toggle=buttons]>.btn>input[type=radio],[data-toggle=buttons]>.btn>input[type=checkbox]{display:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*=col-]{float:none;padding-right:0;padding-left:0}.input-group .form-control{width:100%;margin-bottom:0}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn{height:auto}.input-group-addon,.input-group-btn,.input-group .form-control{display:table-cell}.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child),.input-group .form-control:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:400;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type=radio],.input-group-addon input[type=checkbox]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:last-child>.btn,.input-group-btn:last-child>.dropdown-toggle,.input-group-btn:first-child>.btn:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;white-space:nowrap}.input-group-btn:first-child>.btn{margin-right:-1px}.input-group-btn:last-child>.btn{margin-left:-1px}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-4px}.input-group-btn>.btn:hover,.input-group-btn>.btn:active{z-index:2}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav:before,.nav:after{display:table;content:" "}.nav:after{clear:both}.nav:before,.nav:after{display:table;content:" "}.nav:after{clear:both}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:hover,.nav>li>a:focus{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#999}.nav>li.disabled>a:hover,.nav>li.disabled>a:focus{color:#999;text-decoration:none;cursor:not-allowed;background-color:transparent}.nav .open>a,.nav .open>a:hover,.nav .open>a:focus{background-color:#eee;border-color:#428bca}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.428571429;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media(min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border:1px solid #ddd}@media(min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:hover,.nav-pills>li.active>a:focus{color:#fff;background-color:#428bca}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media(min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border:1px solid #ddd}@media(min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-right-radius:0;border-top-left-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}.navbar:before,.navbar:after{display:table;content:" "}.navbar:after{clear:both}.navbar:before,.navbar:after{display:table;content:" "}.navbar:after{clear:both}@media(min-width:768px){.navbar{border-radius:4px}}.navbar-header:before,.navbar-header:after{display:table;content:" "}.navbar-header:after{clear:both}.navbar-header:before,.navbar-header:after{display:table;content:" "}.navbar-header:after{clear:both}@media(min-width:768px){.navbar-header{float:left}}.navbar-collapse{max-height:340px;padding-right:15px;padding-left:15px;overflow-x:visible;border-top:1px solid transparent;box-shadow:inset 0 1px 0 rgba(255,255,255,.1);-webkit-overflow-scrolling:touch}.navbar-collapse:before,.navbar-collapse:after{display:table;content:" "}.navbar-collapse:after{clear:both}.navbar-collapse:before,.navbar-collapse:after{display:table;content:" "}.navbar-collapse:after{clear:both}.navbar-collapse.in{overflow-y:auto}@media(min-width:768px){.navbar-collapse{width:auto;border-top:0;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{padding-right:0;padding-left:0}}.container>.navbar-header,.container>.navbar-collapse{margin-right:-15px;margin-left:-15px}@media(min-width:768px){.container>.navbar-header,.container>.navbar-collapse{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media(min-width:768px){.navbar-static-top{border-radius:0}}.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030}@media(min-width:768px){.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;padding:15px;font-size:18px;line-height:20px}.navbar-brand:hover,.navbar-brand:focus{text-decoration:none}@media(min-width:768px){.navbar>.container .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;padding:9px 10px;margin-top:8px;margin-right:15px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media(min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media(max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;box-shadow:none}.navbar-nav .open .dropdown-menu>li>a,.navbar-nav .open .dropdown-menu .dropdown-header{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:hover,.navbar-nav .open .dropdown-menu>li>a:focus{background-image:none}}@media(min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}.navbar-nav.navbar-right:last-child{margin-right:-15px}}@media(min-width:768px){.navbar-left{float:left!important}.navbar-right{float:right!important}}.navbar-form{padding:10px 15px;margin-top:8px;margin-right:-15px;margin-bottom:8px;margin-left:-15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1)}@media(min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block}.navbar-form select.form-control{width:auto}.navbar-form .radio,.navbar-form .checkbox{display:inline-block;padding-left:0;margin-top:0;margin-bottom:0}.navbar-form .radio input[type=radio],.navbar-form .checkbox input[type=checkbox]{float:none;margin-left:0}}@media(max-width:767px){.navbar-form .form-group{margin-bottom:5px}}@media(min-width:768px){.navbar-form{width:auto;padding-top:0;padding-bottom:0;margin-right:0;margin-left:0;border:0;-webkit-box-shadow:none;box-shadow:none}.navbar-form.navbar-right:last-child{margin-right:-15px}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-right-radius:0;border-top-left-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-nav.pull-right>li>.dropdown-menu,.navbar-nav>li>.dropdown-menu.pull-right{right:0;left:auto}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media(min-width:768px){.navbar-text{float:left;margin-right:15px;margin-left:15px}.navbar-text.navbar-right:last-child{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:hover,.navbar-default .navbar-brand:focus{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#777}.navbar-default .navbar-nav>li>a:hover,.navbar-default .navbar-nav>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:hover,.navbar-default .navbar-nav>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:hover,.navbar-default .navbar-nav>.disabled>a:focus{color:#ccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:hover,.navbar-default .navbar-toggle:focus{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#ccc}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:hover,.navbar-default .navbar-nav>.open>a:focus{color:#555;background-color:#e7e7e7}@media(max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#ccc;background-color:transparent}}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#999}.navbar-inverse .navbar-brand:hover,.navbar-inverse .navbar-brand:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#999}.navbar-inverse .navbar-nav>li>a{color:#999}.navbar-inverse .navbar-nav>li>a:hover,.navbar-inverse .navbar-nav>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:hover,.navbar-inverse .navbar-nav>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:hover,.navbar-inverse .navbar-nav>.disabled>a:focus{color:#444;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:hover,.navbar-inverse .navbar-toggle:focus{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:hover,.navbar-inverse .navbar-nav>.open>a:focus{color:#fff;background-color:#080808}@media(max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#999}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#444;background-color:transparent}}.navbar-inverse .navbar-link{color:#999}.navbar-inverse .navbar-link:hover{color:#fff}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{padding:0 5px;color:#ccc;content:"/\00a0"}.breadcrumb>.active{color:#999}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;margin-left:-1px;line-height:1.428571429;text-decoration:none;background-color:#fff;border:1px solid #ddd}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-bottom-left-radius:4px;border-top-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-top-right-radius:4px;border-bottom-right-radius:4px}.pagination>li>a:hover,.pagination>li>span:hover,.pagination>li>a:focus,.pagination>li>span:focus{background-color:#eee}.pagination>.active>a,.pagination>.active>span,.pagination>.active>a:hover,.pagination>.active>span:hover,.pagination>.active>a:focus,.pagination>.active>span:focus{z-index:2;color:#fff;cursor:default;background-color:#428bca;border-color:#428bca}.pagination>.disabled>span,.pagination>.disabled>span:hover,.pagination>.disabled>span:focus,.pagination>.disabled>a,.pagination>.disabled>a:hover,.pagination>.disabled>a:focus{color:#999;cursor:not-allowed;background-color:#fff;border-color:#ddd}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-bottom-left-radius:6px;border-top-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-top-right-radius:6px;border-bottom-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-bottom-left-radius:3px;border-top-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-top-right-radius:3px;border-bottom-right-radius:3px}.pager{padding-left:0;margin:20px 0;text-align:center;list-style:none}.pager:before,.pager:after{display:table;content:" "}.pager:after{clear:both}.pager:before,.pager:after{display:table;content:" "}.pager:after{clear:both}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:hover,.pager li>a:focus{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:hover,.pager .disabled>a:focus,.pager .disabled>span{color:#999;cursor:not-allowed;background-color:#fff}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}.label[href]:hover,.label[href]:focus{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#999}.label-default[href]:hover,.label-default[href]:focus{background-color:gray}.label-primary{background-color:#428bca}.label-primary[href]:hover,.label-primary[href]:focus{background-color:#3071a9}.label-success{background-color:#5cb85c}.label-success[href]:hover,.label-success[href]:focus{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:hover,.label-info[href]:focus{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:hover,.label-warning[href]:focus{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:hover,.label-danger[href]:focus{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;background-color:#999;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}a.badge:hover,a.badge:focus{color:#fff;text-decoration:none;cursor:pointer}a.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#428bca;background-color:#fff}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding:30px;margin-bottom:30px;font-size:21px;font-weight:200;line-height:2.1428571435;color:inherit;background-color:#eee}.jumbotron h1,.jumbotron .h1{line-height:1;color:inherit}.jumbotron p{line-height:1.4}.container .jumbotron{border-radius:6px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron{padding-right:60px;padding-left:60px}.jumbotron h1,.jumbotron .h1{font-size:63px}}.thumbnail{display:block;padding:4px;margin-bottom:20px;line-height:1.428571429;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.thumbnail>img,.thumbnail a>img{display:block;height:auto;max-width:100%;margin-right:auto;margin-left:auto}a.thumbnail:hover,a.thumbnail:focus,a.thumbnail.active{border-color:#428bca}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:700}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable{padding-right:35px}.alert-dismissable .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.progress-bar{float:left;width:0;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#428bca;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);-webkit-transition:width .6s ease;transition:width .6s ease}.progress-striped .progress-bar{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:40px 40px}.progress.active .progress-bar{-webkit-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.media,.media-body{overflow:hidden;zoom:1}.media,.media .media{margin-top:15px}.media:first-child{margin-top:0}.media-object{display:block}.media-heading{margin:0 0 5px}.media>.pull-left{margin-right:10px}.media>.pull-right{margin-left:10px}.media-list{padding-left:0;list-style:none}.list-group{padding-left:0;margin-bottom:20px}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-right-radius:4px;border-top-left-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}a.list-group-item{color:#555}a.list-group-item .list-group-item-heading{color:#333}a.list-group-item:hover,a.list-group-item:focus{text-decoration:none;background-color:#f5f5f5}a.list-group-item.active,a.list-group-item.active:hover,a.list-group-item.active:focus{z-index:2;color:#fff;background-color:#428bca;border-color:#428bca}a.list-group-item.active .list-group-item-heading,a.list-group-item.active:hover .list-group-item-heading,a.list-group-item.active:focus .list-group-item-heading{color:inherit}a.list-group-item.active .list-group-item-text,a.list-group-item.active:hover .list-group-item-text,a.list-group-item.active:focus .list-group-item-text{color:#e1edf7}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05);box-shadow:0 1px 1px rgba(0,0,0,.05)}.panel-body{padding:15px}.panel-body:before,.panel-body:after{display:table;content:" "}.panel-body:after{clear:both}.panel-body:before,.panel-body:after{display:table;content:" "}.panel-body:after{clear:both}.panel>.list-group{margin-bottom:0}.panel>.list-group .list-group-item{border-width:1px 0}.panel>.list-group .list-group-item:first-child{border-top-right-radius:0;border-top-left-radius:0}.panel>.list-group .list-group-item:last-child{border-bottom:0}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.panel>.table,.panel>.table-responsive>.table{margin-bottom:0}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive{border-top:1px solid #ddd}.panel>.table>tbody:first-child th,.panel>.table>tbody:first-child td{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.panel>.table-bordered>thead>tr:last-child>th,.panel>.table-responsive>.table-bordered>thead>tr:last-child>th,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th,.panel>.table-bordered>thead>tr:last-child>td,.panel>.table-responsive>.table-bordered>thead>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td{border-bottom:0}.panel>.table-responsive{margin-bottom:0;border:0}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-right-radius:3px;border-top-left-radius:3px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel-group .panel{margin-bottom:0;overflow:hidden;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse .panel-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse .panel-body{border-top-color:#ddd}.panel-default>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#428bca}.panel-primary>.panel-heading{color:#fff;background-color:#428bca;border-color:#428bca}.panel-primary>.panel-heading+.panel-collapse .panel-body{border-top-color:#428bca}.panel-primary>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#428bca}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse .panel-body{border-top-color:#d6e9c6}.panel-success>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#d6e9c6}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse .panel-body{border-top-color:#faebcc}.panel-warning>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse .panel-body{border-top-color:#ebccd1}.panel-danger>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#ebccd1}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse .panel-body{border-top-color:#bce8f1}.panel-info>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#bce8f1}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:21px;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.2;filter:alpha(opacity=20)}.close:hover,.close:focus{color:#000;text-decoration:none;cursor:pointer;opacity:.5;filter:alpha(opacity=50)}button.close{padding:0;cursor:pointer;background:transparent;border:0;-webkit-appearance:none}.modal-open{overflow:hidden}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;display:none;overflow:auto;overflow-y:scroll}.modal.fade .modal-dialog{-webkit-transform:translate(0,-25%);-ms-transform:translate(0,-25%);transform:translate(0,-25%);-webkit-transition:-webkit-transform .3s ease-out;-moz-transition:-moz-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out}.modal.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);transform:translate(0,0)}.modal-dialog{position:relative;z-index:1050;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;border:1px solid #999;border:1px solid rgba(0,0,0,.2);border-radius:6px;outline:0;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5);background-clip:padding-box}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1030;background-color:#000}.modal-backdrop.fade{opacity:0;filter:alpha(opacity=0)}.modal-backdrop.in{opacity:.5;filter:alpha(opacity=50)}.modal-header{min-height:16.428571429px;padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.428571429}.modal-body{position:relative;padding:20px}.modal-footer{padding:19px 20px 20px;margin-top:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer:before,.modal-footer:after{display:table;content:" "}.modal-footer:after{clear:both}.modal-footer:before,.modal-footer:after{display:table;content:" "}.modal-footer:after{clear:both}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}@media screen and (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,.5);box-shadow:0 5px 15px rgba(0,0,0,.5)}}.tooltip{position:absolute;z-index:1030;display:block;font-size:12px;line-height:1.4;opacity:0;filter:alpha(opacity=0);visibility:visible}.tooltip.in{opacity:.9;filter:alpha(opacity=90)}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;text-decoration:none;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-top-color:#000;border-width:5px 5px 0}.tooltip.top-left .tooltip-arrow{bottom:0;left:5px;border-top-color:#000;border-width:5px 5px 0}.tooltip.top-right .tooltip-arrow{right:5px;bottom:0;border-top-color:#000;border-width:5px 5px 0}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-right-color:#000;border-width:5px 5px 5px 0}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-left-color:#000;border-width:5px 0 5px 5px}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-bottom-color:#000;border-width:0 5px 5px}.tooltip.bottom-left .tooltip-arrow{top:0;left:5px;border-bottom-color:#000;border-width:0 5px 5px}.tooltip.bottom-right .tooltip-arrow{top:0;right:5px;border-bottom-color:#000;border-width:0 5px 5px}.popover{position:absolute;top:0;left:0;z-index:1010;display:none;max-width:276px;padding:1px;text-align:left;white-space:normal;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2);background-clip:padding-box}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{padding:8px 14px;margin:0;font-size:14px;font-weight:400;line-height:18px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.popover .arrow,.popover .arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover .arrow{border-width:11px}.popover .arrow:after{border-width:10px;content:""}.popover.top .arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999;border-top-color:rgba(0,0,0,.25);border-bottom-width:0}.popover.top .arrow:after{bottom:1px;margin-left:-10px;border-top-color:#fff;border-bottom-width:0;content:" "}.popover.right .arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999;border-right-color:rgba(0,0,0,.25);border-left-width:0}.popover.right .arrow:after{bottom:-10px;left:1px;border-right-color:#fff;border-left-width:0;content:" "}.popover.bottom .arrow{top:-11px;left:50%;margin-left:-11px;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,.25);border-top-width:0}.popover.bottom .arrow:after{top:1px;margin-left:-10px;border-bottom-color:#fff;border-top-width:0;content:" "}.popover.left .arrow{top:50%;right:-11px;margin-top:-11px;border-left-color:#999;border-left-color:rgba(0,0,0,.25);border-right-width:0}.popover.left .arrow:after{right:1px;bottom:-10px;border-left-color:#fff;border-right-width:0;content:" "}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>img,.carousel-inner>.item>a>img{display:block;height:auto;max-width:100%;line-height:1}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;bottom:0;left:0;width:15%;font-size:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6);opacity:.5;filter:alpha(opacity=50)}.carousel-control.left{background-image:-webkit-linear-gradient(left,color-stop(rgba(0,0,0,.5) 0),color-stop(rgba(0,0,0,.0001) 100%));background-image:linear-gradient(to right,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1)}.carousel-control.right{right:0;left:auto;background-image:-webkit-linear-gradient(left,color-stop(rgba(0,0,0,.0001) 0),color-stop(rgba(0,0,0,.5) 100%));background-image:linear-gradient(to right,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1)}.carousel-control:hover,.carousel-control:focus{color:#fff;text-decoration:none;outline:0;opacity:.9;filter:alpha(opacity=90)}.carousel-control .icon-prev,.carousel-control .icon-next,.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right{position:absolute;top:50%;z-index:5;display:inline-block}.carousel-control .icon-prev,.carousel-control .glyphicon-chevron-left{left:50%}.carousel-control .icon-next,.carousel-control .glyphicon-chevron-right{right:50%}.carousel-control .icon-prev,.carousel-control .icon-next{width:20px;height:20px;margin-top:-10px;margin-left:-10px;font-family:serif}.carousel-control .icon-prev:before{content:'\2039'}.carousel-control .icon-next:before{content:'\203a'}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;padding-left:0;margin-left:-30%;text-align:center;list-style:none}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;cursor:pointer;background-color:#000 \9;background-color:rgba(0,0,0,0);border:1px solid #fff;border-radius:10px}.carousel-indicators .active{width:12px;height:12px;margin:0;background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicons-chevron-left,.carousel-control .glyphicons-chevron-right,.carousel-control .icon-prev,.carousel-control .icon-next{width:30px;height:30px;margin-top:-15px;margin-left:-15px;font-size:30px}.carousel-caption{right:20%;left:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.clearfix:before,.clearfix:after{display:table;content:" "}.clearfix:after{clear:both}.center-block{display:block;margin-right:auto;margin-left:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none!important;visibility:hidden!important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-xs,tr.visible-xs,th.visible-xs,td.visible-xs{display:none!important}@media(max-width:767px){.visible-xs{display:block!important}table.visible-xs{display:table}tr.visible-xs{display:table-row!important}th.visible-xs,td.visible-xs{display:table-cell!important}}@media(min-width:768px) and (max-width:991px){.visible-xs.visible-sm{display:block!important}table.visible-xs.visible-sm{display:table}tr.visible-xs.visible-sm{display:table-row!important}th.visible-xs.visible-sm,td.visible-xs.visible-sm{display:table-cell!important}}@media(min-width:992px) and (max-width:1199px){.visible-xs.visible-md{display:block!important}table.visible-xs.visible-md{display:table}tr.visible-xs.visible-md{display:table-row!important}th.visible-xs.visible-md,td.visible-xs.visible-md{display:table-cell!important}}@media(min-width:1200px){.visible-xs.visible-lg{display:block!important}table.visible-xs.visible-lg{display:table}tr.visible-xs.visible-lg{display:table-row!important}th.visible-xs.visible-lg,td.visible-xs.visible-lg{display:table-cell!important}}.visible-sm,tr.visible-sm,th.visible-sm,td.visible-sm{display:none!important}@media(max-width:767px){.visible-sm.visible-xs{display:block!important}table.visible-sm.visible-xs{display:table}tr.visible-sm.visible-xs{display:table-row!important}th.visible-sm.visible-xs,td.visible-sm.visible-xs{display:table-cell!important}}@media(min-width:768px) and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table}tr.visible-sm{display:table-row!important}th.visible-sm,td.visible-sm{display:table-cell!important}}@media(min-width:992px) and (max-width:1199px){.visible-sm.visible-md{display:block!important}table.visible-sm.visible-md{display:table}tr.visible-sm.visible-md{display:table-row!important}th.visible-sm.visible-md,td.visible-sm.visible-md{display:table-cell!important}}@media(min-width:1200px){.visible-sm.visible-lg{display:block!important}table.visible-sm.visible-lg{display:table}tr.visible-sm.visible-lg{display:table-row!important}th.visible-sm.visible-lg,td.visible-sm.visible-lg{display:table-cell!important}}.visible-md,tr.visible-md,th.visible-md,td.visible-md{display:none!important}@media(max-width:767px){.visible-md.visible-xs{display:block!important}table.visible-md.visible-xs{display:table}tr.visible-md.visible-xs{display:table-row!important}th.visible-md.visible-xs,td.visible-md.visible-xs{display:table-cell!important}}@media(min-width:768px) and (max-width:991px){.visible-md.visible-sm{display:block!important}table.visible-md.visible-sm{display:table}tr.visible-md.visible-sm{display:table-row!important}th.visible-md.visible-sm,td.visible-md.visible-sm{display:table-cell!important}}@media(min-width:992px) and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table}tr.visible-md{display:table-row!important}th.visible-md,td.visible-md{display:table-cell!important}}@media(min-width:1200px){.visible-md.visible-lg{display:block!important}table.visible-md.visible-lg{display:table}tr.visible-md.visible-lg{display:table-row!important}th.visible-md.visible-lg,td.visible-md.visible-lg{display:table-cell!important}}.visible-lg,tr.visible-lg,th.visible-lg,td.visible-lg{display:none!important}@media(max-width:767px){.visible-lg.visible-xs{display:block!important}table.visible-lg.visible-xs{display:table}tr.visible-lg.visible-xs{display:table-row!important}th.visible-lg.visible-xs,td.visible-lg.visible-xs{display:table-cell!important}}@media(min-width:768px) and (max-width:991px){.visible-lg.visible-sm{display:block!important}table.visible-lg.visible-sm{display:table}tr.visible-lg.visible-sm{display:table-row!important}th.visible-lg.visible-sm,td.visible-lg.visible-sm{display:table-cell!important}}@media(min-width:992px) and (max-width:1199px){.visible-lg.visible-md{display:block!important}table.visible-lg.visible-md{display:table}tr.visible-lg.visible-md{display:table-row!important}th.visible-lg.visible-md,td.visible-lg.visible-md{display:table-cell!important}}@media(min-width:1200px){.visible-lg{display:block!important}table.visible-lg{display:table}tr.visible-lg{display:table-row!important}th.visible-lg,td.visible-lg{display:table-cell!important}}.hidden-xs{display:block!important}table.hidden-xs{display:table}tr.hidden-xs{display:table-row!important}th.hidden-xs,td.hidden-xs{display:table-cell!important}@media(max-width:767px){.hidden-xs,tr.hidden-xs,th.hidden-xs,td.hidden-xs{display:none!important}}@media(min-width:768px) and (max-width:991px){.hidden-xs.hidden-sm,tr.hidden-xs.hidden-sm,th.hidden-xs.hidden-sm,td.hidden-xs.hidden-sm{display:none!important}}@media(min-width:992px) and (max-width:1199px){.hidden-xs.hidden-md,tr.hidden-xs.hidden-md,th.hidden-xs.hidden-md,td.hidden-xs.hidden-md{display:none!important}}@media(min-width:1200px){.hidden-xs.hidden-lg,tr.hidden-xs.hidden-lg,th.hidden-xs.hidden-lg,td.hidden-xs.hidden-lg{display:none!important}}.hidden-sm{display:block!important}table.hidden-sm{display:table}tr.hidden-sm{display:table-row!important}th.hidden-sm,td.hidden-sm{display:table-cell!important}@media(max-width:767px){.hidden-sm.hidden-xs,tr.hidden-sm.hidden-xs,th.hidden-sm.hidden-xs,td.hidden-sm.hidden-xs{display:none!important}}@media(min-width:768px) and (max-width:991px){.hidden-sm,tr.hidden-sm,th.hidden-sm,td.hidden-sm{display:none!important}}@media(min-width:992px) and (max-width:1199px){.hidden-sm.hidden-md,tr.hidden-sm.hidden-md,th.hidden-sm.hidden-md,td.hidden-sm.hidden-md{display:none!important}}@media(min-width:1200px){.hidden-sm.hidden-lg,tr.hidden-sm.hidden-lg,th.hidden-sm.hidden-lg,td.hidden-sm.hidden-lg{display:none!important}}.hidden-md{display:block!important}table.hidden-md{display:table}tr.hidden-md{display:table-row!important}th.hidden-md,td.hidden-md{display:table-cell!important}@media(max-width:767px){.hidden-md.hidden-xs,tr.hidden-md.hidden-xs,th.hidden-md.hidden-xs,td.hidden-md.hidden-xs{display:none!important}}@media(min-width:768px) and (max-width:991px){.hidden-md.hidden-sm,tr.hidden-md.hidden-sm,th.hidden-md.hidden-sm,td.hidden-md.hidden-sm{display:none!important}}@media(min-width:992px) and (max-width:1199px){.hidden-md,tr.hidden-md,th.hidden-md,td.hidden-md{display:none!important}}@media(min-width:1200px){.hidden-md.hidden-lg,tr.hidden-md.hidden-lg,th.hidden-md.hidden-lg,td.hidden-md.hidden-lg{display:none!important}}.hidden-lg{display:block!important}table.hidden-lg{display:table}tr.hidden-lg{display:table-row!important}th.hidden-lg,td.hidden-lg{display:table-cell!important}@media(max-width:767px){.hidden-lg.hidden-xs,tr.hidden-lg.hidden-xs,th.hidden-lg.hidden-xs,td.hidden-lg.hidden-xs{display:none!important}}@media(min-width:768px) and (max-width:991px){.hidden-lg.hidden-sm,tr.hidden-lg.hidden-sm,th.hidden-lg.hidden-sm,td.hidden-lg.hidden-sm{display:none!important}}@media(min-width:992px) and (max-width:1199px){.hidden-lg.hidden-md,tr.hidden-lg.hidden-md,th.hidden-lg.hidden-md,td.hidden-lg.hidden-md{display:none!important}}@media(min-width:1200px){.hidden-lg,tr.hidden-lg,th.hidden-lg,td.hidden-lg{display:none!important}}.visible-print,tr.visible-print,th.visible-print,td.visible-print{display:none!important}@media print{.visible-print{display:block!important}table.visible-print{display:table}tr.visible-print{display:table-row!important}th.visible-print,td.visible-print{display:table-cell!important}.hidden-print,tr.hidden-print,th.hidden-print,td.hidden-print{display:none!important}}/*! - * Font Awesome 4.0.3 by @davegandy - http://fontawesome.io - @fontawesome - * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) - */@font-face{font-family:FontAwesome;src:url(../fonts/fontawesome-webfont.eot?v=4.0.3);src:url(../fonts/fontawesome-webfont.eot?#iefix&v=4.0.3) format('embedded-opentype'),url(../fonts/fontawesome-webfont.woff?v=4.0.3) format('woff'),url(../fonts/fontawesome-webfont.ttf?v=4.0.3) format('truetype'),url(../fonts/fontawesome-webfont.svg?v=4.0.3#fontawesomeregular) format('svg');font-weight:400;font-style:normal}.fa{display:inline-block;font-family:FontAwesome;font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.3333333333333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.2857142857142858em;text-align:center}.fa-ul{padding-left:0;margin-left:2.142857142857143em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.142857142857143em;width:2.142857142857143em;top:.14285714285714285em;text-align:center}.fa-li.fa-lg{left:-1.8571428571428572em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:spin 2s infinite linear;-moz-animation:spin 2s infinite linear;-o-animation:spin 2s infinite linear;animation:spin 2s infinite linear}@-moz-keyframes spin{0%{-moz-transform:rotate(0deg)}100%{-moz-transform:rotate(359deg)}}@-webkit-keyframes spin{0%{-webkit-transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg)}}@-o-keyframes spin{0%{-o-transform:rotate(0deg)}100%{-o-transform:rotate(359deg)}}@-ms-keyframes spin{0%{-ms-transform:rotate(0deg)}100%{-ms-transform:rotate(359deg)}}@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(359deg)}}.fa-rotate-90{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=1);-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2);-webkit-transform:rotate(180deg);-moz-transform:rotate(180deg);-ms-transform:rotate(180deg);-o-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=3);-webkit-transform:rotate(270deg);-moz-transform:rotate(270deg);-ms-transform:rotate(270deg);-o-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1);-webkit-transform:scale(-1,1);-moz-transform:scale(-1,1);-ms-transform:scale(-1,1);-o-transform:scale(-1,1);transform:scale(-1,1)}.fa-flip-vertical{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1);-webkit-transform:scale(1,-1);-moz-transform:scale(1,-1);-ms-transform:scale(1,-1);-o-transform:scale(1,-1);transform:scale(1,-1)}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-asc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-desc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-reply-all:before{content:"\f122"}.fa-mail-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"} \ No newline at end of file diff -Nru eoxserver-0.4.0beta2/eoxserver/webclient/static/templates/BulletLayer.hbs eoxserver-0.3.2/eoxserver/webclient/static/templates/BulletLayer.hbs --- eoxserver-0.4.0beta2/eoxserver/webclient/static/templates/BulletLayer.hbs 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/webclient/static/templates/BulletLayer.hbs 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ - diff -Nru eoxserver-0.4.0beta2/eoxserver/webclient/static/templates/CheckBoxLayer.hbs eoxserver-0.3.2/eoxserver/webclient/static/templates/CheckBoxLayer.hbs --- eoxserver-0.4.0beta2/eoxserver/webclient/static/templates/CheckBoxLayer.hbs 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/webclient/static/templates/CheckBoxLayer.hbs 1970-01-01 00:00:00.000000000 +0000 @@ -1,11 +0,0 @@ -
    - - - - {{name}} - - - - - -
    diff -Nru eoxserver-0.4.0beta2/eoxserver/webclient/static/templates/CheckBoxOverlayLayer.hbs eoxserver-0.3.2/eoxserver/webclient/static/templates/CheckBoxOverlayLayer.hbs --- eoxserver-0.4.0beta2/eoxserver/webclient/static/templates/CheckBoxOverlayLayer.hbs 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/webclient/static/templates/CheckBoxOverlayLayer.hbs 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ - {{name}} \ No newline at end of file diff -Nru eoxserver-0.4.0beta2/eoxserver/webclient/static/templates/CoverageDownloadPost.hbs eoxserver-0.3.2/eoxserver/webclient/static/templates/CoverageDownloadPost.hbs --- eoxserver-0.4.0beta2/eoxserver/webclient/static/templates/CoverageDownloadPost.hbs 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/webclient/static/templates/CoverageDownloadPost.hbs 1970-01-01 00:00:00.000000000 +0000 @@ -1,3 +0,0 @@ -
    - -
    \ No newline at end of file diff -Nru eoxserver-0.4.0beta2/eoxserver/webclient/static/templates/CoverageInfo.hbs eoxserver-0.3.2/eoxserver/webclient/static/templates/CoverageInfo.hbs --- eoxserver-0.4.0beta2/eoxserver/webclient/static/templates/CoverageInfo.hbs 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/webclient/static/templates/CoverageInfo.hbs 1970-01-01 00:00:00.000000000 +0000 @@ -1,5 +0,0 @@ - - - - -
    Time Period {{timePeriod}}
    \ No newline at end of file diff -Nru eoxserver-0.4.0beta2/eoxserver/webclient/static/templates/Download.hbs eoxserver-0.3.2/eoxserver/webclient/static/templates/Download.hbs --- eoxserver-0.4.0beta2/eoxserver/webclient/static/templates/Download.hbs 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/webclient/static/templates/Download.hbs 1970-01-01 00:00:00.000000000 +0000 @@ -1,33 +0,0 @@ -
    - -

    Download Manager

    -
    -
    - diff -Nru eoxserver-0.4.0beta2/eoxserver/webclient/static/templates/Info.hbs eoxserver-0.3.2/eoxserver/webclient/static/templates/Info.hbs --- eoxserver-0.4.0beta2/eoxserver/webclient/static/templates/Info.hbs 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/webclient/static/templates/Info.hbs 1970-01-01 00:00:00.000000000 +0000 @@ -1,20 +0,0 @@ - diff -Nru eoxserver-0.4.0beta2/eoxserver/webclient/static/templates/LayerControl.hbs eoxserver-0.3.2/eoxserver/webclient/static/templates/LayerControl.hbs --- eoxserver-0.4.0beta2/eoxserver/webclient/static/templates/LayerControl.hbs 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/webclient/static/templates/LayerControl.hbs 1970-01-01 00:00:00.000000000 +0000 @@ -1,9 +0,0 @@ -
    - -

    Layers

    -
    -
    -
    -
    -
    -
    diff -Nru eoxserver-0.4.0beta2/eoxserver/webclient/static/templates/NavBar.hbs eoxserver-0.3.2/eoxserver/webclient/static/templates/NavBar.hbs --- eoxserver-0.4.0beta2/eoxserver/webclient/static/templates/NavBar.hbs 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/webclient/static/templates/NavBar.hbs 1970-01-01 00:00:00.000000000 +0000 @@ -1,10 +0,0 @@ - diff -Nru eoxserver-0.4.0beta2/eoxserver/webclient/static/templates/NavBarItem.hbs eoxserver-0.3.2/eoxserver/webclient/static/templates/NavBarItem.hbs --- eoxserver-0.4.0beta2/eoxserver/webclient/static/templates/NavBarItem.hbs 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/webclient/static/templates/NavBarItem.hbs 1970-01-01 00:00:00.000000000 +0000 @@ -1,4 +0,0 @@ - - {{name}} - - diff -Nru eoxserver-0.4.0beta2/eoxserver/webclient/static/templates/SelectCoverageListItem.hbs eoxserver-0.3.2/eoxserver/webclient/static/templates/SelectCoverageListItem.hbs --- eoxserver-0.4.0beta2/eoxserver/webclient/static/templates/SelectCoverageListItem.hbs 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/webclient/static/templates/SelectCoverageListItem.hbs 1970-01-01 00:00:00.000000000 +0000 @@ -1,3 +0,0 @@ -
    - -
    \ No newline at end of file diff -Nru eoxserver-0.4.0beta2/eoxserver/webclient/static/templates/SelectionManager.hbs eoxserver-0.3.2/eoxserver/webclient/static/templates/SelectionManager.hbs --- eoxserver-0.4.0beta2/eoxserver/webclient/static/templates/SelectionManager.hbs 2014-12-18 15:25:53.000000000 +0000 +++ eoxserver-0.3.2/eoxserver/webclient/static/templates/SelectionManager.hbs 1970-01-01 00:00:00.000000000 +0000 @@ -1,16 +0,0 @@ -
    - -

    Selection Manager

    -
    -
    -