(function() {
    'use strict';

    /**
     * @ngdoc overview
     * @name index
     * @description
     # SPARQL Faceter - Client-Side Faceted Search Using SPARQL
     *
     * The module provides a set of directives that work as facets,
     * and a service that synchronizes them.
     *
     * There are four different built-in facet types:
     *
     * - {@link seco.facetedSearch.directive:secoBasicFacet `secoBasicFacet`} - A basic select box facet with text filtering
     * - {@link seco.facetedSearch.directive:secoHierarchyFacet `secoHierarchyFacet`} - A basic facet with hierarchy support.
     * - {@link seco.facetedSearch.directive:secoTextFacet `secoTextFacet`} - A free-text facet.
     * - {@link seco.facetedSearch.directive:secoJenaTextFacet `secoJenaTextFacet`} - A free-text facet that uses Jena text search.
     * - {@link seco.facetedSearch.directive:secoTimespanFacet `secoTimespanFacet`} - Date range facet.
     *
     * Custom facets can be implemented reasonably easily.
     *
     ## How it works
     * Facets are implemented as a directives.
     * Each facet listens on its scope for changes in other facets,
     * and emits its state when its value changes.
     * The facets are responsible for maintaining their own state.
     * In the following, "constraint" means a triple pattern that narrows the
     * resultset down based on a facet value.
     *
     * {@link seco.facetedSearch.FacetHandler `FacetHandler`} mediates the facet changes
     * by listening to the facets' change events, and broadcasting the resulting
     * constraints to all facets in the scope.
     *
     * The facets are configured using the `options` attribute of the directive.
     * Configuration options that are common to all facets are:
     *
     * - **facetId** - `{string}` - A friendly id for the facet.
     *   Should be unique in the set of facets, and should be usable as a SPARQL variable.
     * - **predicate** - `{string}` - The predicate or property path that defines the facet values.
     * - **name** - `{string}` - The title of the facet. Will be displayed to end users.
     * - **enabled** `{boolean}` - Whether or not the facet is enabled by default.
     * - **priority** `{number}` - When the {@link seco.facetedSearch.FacetHandler `FacetHandler`}
     *   broadcasts the constraints, it sorts them based on each facet's priority value (if it
     *   is defined), in ascending order. This can be used for better query caching, or to
     *   optimize the order of constraints. For most facets, this value is undefined by default.
     *   {@link seco.facetedSearch.directive:secoJenaTextFacet `secoJenaTextFacet`} has a default
     *   priority of 10 (so it will always be the first constraint by default).
     *
     * For other options, see the facets' individual documentation.
     *
     * To make use of the constraints generated by the facets, a controller should
     * listen for the update event `sf-facets-changed` (and possibly for the initial
     * `sf-initial-constraints`). The argument sent with the event is an objectfollowing
     * fields:
     *
     * - **constraint** - `{Array}` - The constraints as generated by all the facets,
     *   plus any initial constraints (see {@link seco.facetedSearch.FacetHandler `FacetHandler`}).
     * - **facets** - `{Object}` - A collection of facet states where the key is the
     * `facetId` of the facet, and value is the state as emitted by that facet.
     *   Presumably, when using the built-in facets, you would not be interested
     *   in these values.
     *
     * @example
     * Setup in the controller:
     *
     * <pre>
     * var vm = this;
     *
     * // Define facets
     * vm.facets = {
     *     // Text facet
     *     name: {
     *         name: 'Name',
     *         facetId: 'name',
     *         predicate: '<http://www.w3.org/2004/02/skos/core#prefLabel>',
     *         enabled: true
     *     },
     *     // Date facet
     *     deathDate: {
     *         name: 'Time of Death',
     *         facetId: 'death',
     *         startPredicate: '<http://ldf.fi/schema/narc-menehtyneet1939-45/kuolinaika>',
     *         endPredicate: '<http://ldf.fi/schema/narc-menehtyneet1939-45/kuolinaika>',
     *         min: '1939-10-01',
     *         max: '1989-12-31',
     *         enabled: true
     *     },
     *     // Basic facet
     *     profession: {
     *         name: 'Ammatti',
     *         facetId: 'occupation',
     *         predicate: '<http://ldf.fi/schema/narc-menehtyneet1939-45/ammatti>',
     *         enabled: true
     *     },
     *     // Basic facet with property path
     *     source: {
     *         name: 'Source',
     *         facetId: 'source',
     *         predicate: '^<http://www.cidoc-crm.org/cidoc-crm/P70i_is_documented_in>/<http://purl.org/dc/elements/1.1/source>',
     *         enabled: true
     *     },
     *     // Basic facet with labels in another service.
     *     birthMunicipality: {
     *         name: 'Birth Municipality',
     *         services: ['<http://ldf.fi/pnr/sparql>'],
     *         facetId: 'birthplace',
     *         predicate: '<http://ldf.fi/schema/narc-menehtyneet1939-45/synnyinkunta>',
     *         enabled: false
     *     },
     *     // Hierarchical facet
     *     rank: {
     *         name: 'Rank',
     *         facetId: 'rank',
     *         predicate: '<http://ldf.fi/schema/narc-menehtyneet1939-45/sotilasarvo>',
     *         hierarchy: '<http://purl.org/dc/terms/isPartOf>*|(<http://rdf.muninn-project.org/ontologies/organization#equalTo>/<http://purl.org/dc/terms/isPartOf>*)',
     *         enabled: true,
     *         classes: [
     *             '<http://ldf.fi/warsa/actors/ranks/Upseeri>',
     *             '<http://ldf.fi/warsa/actors/ranks/Aliupseeri>',
     *             '<http://ldf.fi/warsa/actors/ranks/Miehistoe>',
     *             '<http://ldf.fi/warsa/actors/ranks/Jaeaekaeriarvo>',
     *             '<http://ldf.fi/warsa/actors/ranks/Virkahenkiloestoe>',
     *             '<http://ldf.fi/warsa/actors/ranks/Lottahenkiloestoe>',
     *             '<http://ldf.fi/warsa/actors/ranks/Muu>'
     *         ]
     *     }
     * };
     *
     * // Define common options
     * vm.options = {
     *     scope: $scope,
     *     endpointUrl: 'http://ldf.fi/warsa/sparql',
     *     rdfClass: '<http://ldf.fi/schema/narc-menehtyneet1939-45/DeathRecord>',
     *     constraint: '?id skos:prefLabel ?name .',
     *     preferredLang : 'fi'
     * };
     *
     * // Define a function that handles updates.
     * // 'dataService' is some service that fetches results based on the facet selections.
     * function updateResults(event, facetState) {
     *     dataService.getResults(facetState.constraints).then(function(results) {
     *         vm.results = results;
     *     }
     * }
     *
     * // Listen for the update event
     * $scope.$on('sf-facet-constraints', updateResults);
     *
     * // Listen for initial state
     * var initListener = $scope.$on('sf-initial-constraints', function(event, state) {
     *     updateResults(event, state);
     *     // Only listen once for the init event
     *     initListener();
     * });
     *
     * // Initialize the facet handler:
     * vm.handler = new FacetHandler(vm.options);
     * </pre>
     *
     * Setup the facets in the template:
     *
     * <pre>
     * <seco-text-facet
     *   data-options="vm.facets.name">
     * </seco-text-facet>
     * <seco-timespan-facet
     *   data-options="vm.facets.deathDate">
     * </seco-timespan-facet>
     * <seco-basic-facet
     *   data-options="vm.facets.source">
     * </seco-basic-facet>
     * <seco-basic-facet
     *   data-options="vm.facets.profession">
     * </seco-basic-facet>
     * <seco-basic-facet
     *   data-options="vm.facets.birthMunicipality">
     * </seco-basic-facet>
     * <seco-basic-facet
     *   data-options="vm.facets.principalAbode">
     * </seco-basic-facet>
     * <seco-hierarchy-facet
     *   data-options="vm.facets.rank">
     * </seco-hierarchy-facet>
     * </pre>
     */

    /**
     * @ngdoc overview
     * @name seco.facetedSearch
     * @description
     # SPARQL Faceter - Client-Side Faceted Search Using SPARQL
     * Main module.
     */
    angular.module('seco.facetedSearch', [
        'sparql', 'ui.bootstrap', 'angularSpinner', 'checklist-model'
    ])
    .constant('_', _) // eslint-disable-line no-undef
    .constant('PREFIXES',
        ' PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> ' +
        ' PREFIX skos: <http://www.w3.org/2004/02/skos/core#> ' +
        ' PREFIX xsd: <http://www.w3.org/2001/XMLSchema#> '
    )
    .constant('EVENT_REQUEST_CONSTRAINTS', 'sf-request-constraints')
    .constant('EVENT_INITIAL_CONSTRAINTS', 'sf-initial-constraints')
    .constant('EVENT_FACET_CHANGED', 'sf-facet-changed')
    .constant('EVENT_FACET_CONSTRAINTS', 'sf-facet-constraints')
    .constant('NO_SELECTION_STRING', '-- No Selection --');
})();


(function() {

    'use strict';

    /* eslint-disable angular/no-service-method */
    angular.module('seco.facetedSearch')

    /**
    * @ngdoc service
    * @name seco.facetedSearch.facetUrlStateHandlerService
    * @description
    * # facetUrlStateHandlerService
    * Service for updating the URL parameters based on facet selections,
    * and retrieving the facet values from URL parameters.
    *
    * Intended for bookmarking the current facet selections.
    * {@link seco.facetedSearch.FacetHandler `FacetHandler`} can take
    * the return value of `getFacetValuesFromUrlParams` as the
    * `initialState` configuration value.
    *
    * Updating the URL parameters can be done, e.g., when the facet states
    * have been received while listening for facet updates.
    * See {@link seco.facetedSearch.FacetHandler `FacetHandler`} for details
    * regarding events.
    */
    .service('facetUrlStateHandlerService', facetUrlStateHandlerService);

    /* @ngInject */
    function facetUrlStateHandlerService($location, _) {

        this.updateUrlParams = updateUrlParams;
        this.getFacetValuesFromUrlParams = getFacetValuesFromUrlParams;

        /**
        * @ngdoc method
        * @methodOf seco.facetedSearch.facetUrlStateHandlerService
        * @name seco.facetedSearch.facetUrlStateHandlerService#updateUrlParams
        * @description
        * Update the URL parameters based on the given facet state.
        * @param {Object} facets The facet states as broadcast by
        *   {@link seco.facetedSearch.FacetHandler `FacetHandler`}.
        */
        function updateUrlParams(facets) {
            facets = facets.facets || facets;
            var params = {};
            _(facets).forOwn(function(val, id) {
                if (val && val.value && !(_.isObject(val.value) && _.isEmpty(val.value))) {
                    params[id] = { value: val.value, constraint: val.constraint };
                }
            });
            if (_.isEmpty(params)) {
                params = null;
            } else {
                params = angular.toJson(params);
            }
            $location.search('facets', params);
        }

        /**
        * @ngdoc method
        * @methodOf seco.facetedSearch.facetUrlStateHandlerService
        * @name seco.facetedSearch.facetUrlStateHandlerService#getFacetValuesFromUrlParams
        * @description
        * Get the facet states from the URL parameters.
        * @return {Object} The facet states.
        */
        function getFacetValuesFromUrlParams() {
            var res = {};

            var params = ($location.search() || {}).facets;
            if (!params) {
                return res;
            }
            try {
                params = angular.fromJson(params);
            }
            catch(e) {
                $location.search('facets', null);
                return res;
            }
            _.forOwn(params, function(val, id) {
                res[id] = val;
            });
            return res;
        }
    }
})();

(function() {
    'use strict';

    /**
    * @ngdoc object
    * @name seco.facetedSearch.FacetResultHandler
    */
    angular.module('seco.facetedSearch')
    .constant('DEFAULT_PAGES_PER_QUERY', 1)
    .constant('DEFAULT_RESULTS_PER_PAGE', 10)

    /*
    * Result handler service.
    */
    .factory('FacetResultHandler', FacetResultHandler);

    /* @ngInject */
    function FacetResultHandler(_, DEFAULT_PAGES_PER_QUERY, DEFAULT_RESULTS_PER_PAGE,
            PREFIXES, AdvancedSparqlService, objectMapperService, QueryBuilderService) {

        return ResultHandler;

        /**
        * @name seco.facetedSearch.FacetResultHandler
        * @ngdoc function
        * @constructor
        * @description
        * Service for retrieving SPARQL results based on facet selections.
        *
        * @param {string} endpointUrl The URL of the SPARQL endpoint.
        * @param {Object} resultOptions Configuration object.
        *   The object has the following properties:
        *
        *   - **queryTemplate** - `{string}` - The result query with a `<RESULT_SET>`
        *     placeholder for the facet selections.
        *     The variable `?id` should be used for the result resources.
        *     The query should not restrict the results in any way (outside of the
        *     facet selections), or the results will not reflect the facets.
        *     The best way to insure this is to wrap each result value in an
        *     `OPTIONAL` block.
        *     For example:
        *      <pre>
        *      SELECT * WHERE {
        *          <RESULT_SET>
        *          OPTIONAL {
        *              ?id rdfs:label ?name .
        *              FILTER(langMatches(lang(?name), "en"))
        *          }
        *      }
        *      </pre>
        *   - **[prefixes]** - `{string}` - Any prefixes used in the `queryTemplate`.
        *     Required if the query uses any other prefixes than `rdf`, `rdfs`, or `skos`.
        *   - **[paging]** - `{boolean}` - If truthy, results will be paged.
        *     Default is `true`.
        *   - **[resultsPerPage]** - `{number}` - The number of results per page.
        *     Default is 10.
        *   - **[pagesPerQuery]** - `{number}` - The number of pages to retrieve per query.
        *     Default is 1.
        *   - **[mapper]** - `{Object}` - Mapper service for the results.
        *     The default is
        *     {@link http://semanticcomputing.github.io/angular-paging-sparql-service/#/api/sparql.objectMapperService `objectMapperService`}.
        *     See the {@link https://github.com/SemanticComputing/angular-paging-sparql-service angular-paging-sparql-service} package
        *     for more information.
        *
        */
        function ResultHandler(endpointUrl, resultOptions) {
            // Default options
            var options = {
                resultsPerPage: DEFAULT_RESULTS_PER_PAGE,
                pagesPerQuery: DEFAULT_PAGES_PER_QUERY,
                mapper: objectMapperService,
                prefixes: PREFIXES,
                paging: true
            };
            options = angular.extend(options, resultOptions);

            /* Public API */

            this.getResults = getResults;

            /* Implementation */

            var qryBuilder = new QueryBuilderService(options.prefixes);

            var endpoint = new AdvancedSparqlService(endpointUrl, options.mapper);

            /**
            * @ngdoc method
            * @methodOf seco.facetedSearch.FacetResultHandler
            * @name seco.facetedSearch.FacetResultHandler#getResults
            * @description
            * Get results based on the facet selections and the query template.
            * Use paging if defined in the options.
            * @param {Object} facetSelections The facet states as broadcast by
            *   {@link seco.facetedSearch.FacetHandler `FacetHandler`}
            * @param {string} [orderBy] SPARQL order comparators to use in sorting
            *   the results. Any variables used here have to be present in the
            *   `<RESULT_SET>` part of the query. I.e. they need to be included
            *   in the `constraint` parameter of
            *   {@link seco.facetedSearch.FacetHandler `FacetHandler`}.
            *   The default is to sort by `?id`.
            */
            function getResults(facetSelections, orderBy) {
                var constraints = facetSelections.constraint.join(' ');
                var qry = qryBuilder.buildQuery(options.queryTemplate, constraints, orderBy);

                if (options.paging) {
                    return endpoint.getObjects(qry.query, options.resultsPerPage, qry.resultSetQuery,
                            options.pagesPerQuery);
                } else {
                    return endpoint.getObjects(qry.query);
                }
            }
        }
    }
})();

(function() {
    'use strict';

    /*
    * Service for transforming SPARQL result triples into facet objects.
    *
    * Author Erkki Heino.
    */
    angular.module('seco.facetedSearch')

    .factory('facetMapperService', facetMapperService);

    /* ngInject */
    function facetMapperService(_, objectMapperService) {
        FacetMapper.prototype.makeObject = makeObject;

        var proto = Object.getPrototypeOf(objectMapperService);
        FacetMapper.prototype = angular.extend({}, proto, FacetMapper.prototype);

        return new FacetMapper();

        function FacetMapper() {
            this.objectClass = Object;
        }

        function makeObject(obj) {
            var o = new this.objectClass();

            o.value = parseValue(obj.value);
            o.text = _.get(obj, 'facet_text.value');
            o.count = obj.cnt ? parseInt(obj.cnt.value) : undefined;

            return o;
        }

        function parseValue(value) {
            if (!value) {
                return undefined;
            }
            if (value.type === 'uri') {
                return '<' + value.value + '>';
            }
            if (_.includes(value.type, 'literal') && value.datatype === 'http://www.w3.org/2001/XMLSchema#integer') {
                return value.value;
            }
            if (_.includes(value.type, 'literal') && value.datatype) {
                return '"' + value.value + '"^^<' + value.datatype + '>';
            }
            return '"' + value.value + '"' + (value['xml:lang'] ? '@' + value['xml:lang'] : '');
        }

    }
})();

(function() {
    'use strict';

    /**
    * @ngdoc object
    * @name seco.facetedSearch.FacetHandler
    */
    angular.module('seco.facetedSearch')
    .factory('FacetHandler', FacetHandler);

    /* ngInject */
    function FacetHandler(_, EVENT_FACET_CONSTRAINTS, EVENT_FACET_CHANGED, EVENT_REQUEST_CONSTRAINTS,
            EVENT_INITIAL_CONSTRAINTS) {

        return FacetHandler;

        /**
        * @ngdoc function
        * @name seco.facetedSearch.FacetHandler
        * @constructor
        * @description
        * Service for mediating the communication between facets.
        *
        * When created, the FacetHandler object broadcasts a `sf-initial-constraints` event
        * on the given scope, with the following structure:
        *
        * - **constraint** - `{Array}` - The constraints as generated by all the facets,
        *   plus any initial constraints defined in the init configuration.
        * - **facets** - `{Object}` - A collection of facet states where the key is the
        *   `facetId` of the facet, and value is the state as emitted by that facet.
        * - **config** - `{Object}` - The configuration given when instantiating
        *   the FacetHandler.
        *
        * The FacetHandler object listens for two events:
        *
        * - **`sf-request-constraints`** - In response will broadcast a
        *   `sf-initial-constraints` event
        *   with the same object as above as the argument.
        * - **`sf-facets-changed`** - In response will broadcast a
        *   `sf-facet-constraints` event on the scope with the same object as
        *   above, except omitting `config`.
        *
        * The event names are injectable as constants:
        * - `sf-initial-constraints`: `EVENT_INITIAL_CONSTRAINTS`
        * - `sf-facet-constraints`: `EVENT_FACET_CONSTRAINTS`
        * - `sf-request-constraints`: `EVENT_REQUEST_CONSTRAINTS`
        * - `sf-facet-changed`: `EVENT_FACET_CHANGED`
        *
        * @param {Object} config Configuration object broadcast to all facets at init.
        *   The object has the following properties:
        *
        *   - **scope** - `{Object}` - The facets' parent scope. This is used for
        *   broadcasting and listening for events.
        *   - **[endpointUrl]** - `{string}` - The SPARQL endpoint URL. Optional,
        *   as it is just sent to all listening facets at init, and can also
        *   be given to facets individually.
        *   - **[constraint]** - `{string}` - A SPARQL triple pattern that selects
        *   all the resources that are being faceted.
        *   - **[rdfClass]** - `{string}` - The `rdf:type` of the resources being
        *   faceted. A shorthand for `constraint: '?id a <class> .'`.
        *   - **[preferredLang]** - `{string}` - The language tag that is preferred
        *   when getting labels for facet values, in case the value is a resource.
        *   The default is 'en'.
        *   Currently only one language can be given.
        *   This argument can also be given directly to the individual facets.
        *   - **[initialState]** - `{Object}` - The initial state of the facets.
        *   Used when loading the state from URL parameters, for example.
        *   See {@link seco.facetedSearch.facetUrlStateHandlerService `facetUrlStateHandlerService`}.
        *
        *   The object is sent to facets as is, and so any extra fields will be included,
        *   although the above fields are the only ones used by FacetHandler and the
        *   built-in facets.
        */
        function FacetHandler(config) {
            var self = this;

            init();

            function init() {
                self.state = { facets: {} };

                var defaultConfig = {
                    preferredLang: 'en'
                };

                self.config = angular.extend({}, defaultConfig, config);

                self.changeListener = self.config.scope.$on(EVENT_FACET_CHANGED, update);
                self.initListener = self.config.scope.$on(EVENT_REQUEST_CONSTRAINTS, broadCastInitial);

                self.removeListeners = removeListeners;

                self.state.facets = self.config.initialState || {};
                self.state.default = getInitialConstraints(self.config);
                broadCastInitial();
            }

            // Update state, and broadcast it to listening facets.
            function update(event, constraint) {
                event.stopPropagation();
                if (self.state.facets[constraint.id] !== constraint) {
                    self.state.facets[constraint.id] = constraint;
                    broadCastConstraints(EVENT_FACET_CONSTRAINTS);
                }
            }

            function broadCastInitial(event) {
                if (event) {
                    event.stopPropagation();
                }
                var data = {
                    config: self.config
                };
                broadCastConstraints(EVENT_INITIAL_CONSTRAINTS, data);
            }

            function broadCastConstraints(eventType, data) {
                data = data || {};

                var constraint = getConstraint();
                constraint.push(self.state.default);

                data.facets = self.state.facets;
                data.constraint = constraint;

                self.config.scope.$broadcast(eventType, data);
            }

            function getConstraint() {
                return _(self.state.facets).values().sortBy('priority').map('constraint').compact().value();
            }

            // Combine the possible RDF class and constraint definitions in the config.
            function getInitialConstraints(config) {
                var state = config.rdfClass ? ' ?id a ' + config.rdfClass + ' . ' : '';
                state = state + (config.constraint || '');
                return state;
            }

            /**
            * @ngdoc method
            * @methodOf seco.facetedSearch.FacetHandler
            * @name seco.facetedSearch.FacetHandler#removeListeners
            * @description
            * Remove event listeners from the scope.
            * Probably only needed if you, for some reason, need to
            * use `$rootScope` as the communication scope.
            */
            function removeListeners() {
                self.initListener();
                self.changeListener();
            }
        }
    }
})();

(function() {
    'use strict';

    angular.module('seco.facetedSearch')
    .filter('textWithSelection', function(_) {
        return function(values, text, selection) {
            if (!text) {
                return values;
            }
            var selectedValues;
            if (_.isArray(selection)) {
                selectedValues = _.map(selection, 'value');
            } else {
                selectedValues = selection ? [selection.value] : [];
            }

            var hasNoSelection = _.some(selectedValues, angular.isUndefined);
            if (!hasNoSelection) {
                selectedValues.push(undefined);
            }

            return _.filter(values, function(val) {
                return _.includes(val.text.toLowerCase(), text.toLowerCase()) ||
                    _.includes(selectedValues, val.value);
            });
        };
    });
})();

(function() {
    'use strict';

    angular.module('seco.facetedSearch')
    .controller('AbstractFacetController', AbstractFacetController);

    /* @ngInject */
    function AbstractFacetController($scope, $log, _, EVENT_FACET_CONSTRAINTS,
            EVENT_FACET_CHANGED, EVENT_REQUEST_CONSTRAINTS, EVENT_INITIAL_CONSTRAINTS,
            FacetImpl) {

        var vm = this;

        vm.isLoading = isLoading;
        vm.changed = changed;

        vm.disableFacet = disableFacet;
        vm.enableFacet = enableFacet;

        vm.getFacetSize = getFacetSize;

        vm.listener = function() { };

        vm.getSpinnerKey = getSpinnerKey;

        // Wait until the options attribute has been set.
        var watcher = $scope.$watch('options', function(val) {
            if (val) {
                init(FacetImpl);
                watcher();
            }
        });

        function init(Facet) {
            var initListener = $scope.$on(EVENT_INITIAL_CONSTRAINTS, function(event, cons) {
                var opts = _.cloneDeep($scope.options);
                opts = angular.extend({}, cons.config, opts);
                opts.initial = cons.facets;
                vm.facet = new Facet(opts);
                if (vm.facet.isEnabled()) {
                    vm.previousVal = _.cloneDeep(vm.facet.getSelectedValue());
                    listen();
                    update(cons);
                }
                // Unregister initListener
                initListener();
            });
            $scope.$emit(EVENT_REQUEST_CONSTRAINTS);
        }

        var spinnerKey;
        function getSpinnerKey() {
            if (!spinnerKey) {
                spinnerKey = _.uniqueId('spinner');
            }
            return spinnerKey;
        }

        function listen() {
            vm.listener = $scope.$on(EVENT_FACET_CONSTRAINTS, function(event, cons) {
                update(cons);
            });
        }

        function update(constraints) {
            vm.isLoadingFacet = true;
            return vm.facet.update(constraints).then(handleUpdateSuccess, handleError);
        }

        function isLoading() {
            return vm.isLoadingFacet || !vm.facet || vm.facet.isLoading();
        }

        function emitChange(forced) {
            var val = vm.facet.getSelectedValue();
            if (!forced && _.isEqual(vm.previousVal, val)) {
                vm.isLoadingFacet = false;
                return;
            }
            vm.previousVal = _.cloneDeep(val);
            var args = {
                id: vm.facet.facetId,
                constraint: vm.facet.getConstraint(),
                value: val
            };
            $scope.$emit(EVENT_FACET_CHANGED, args);
        }

        function changed() {
            vm.isLoadingFacet = true;
            emitChange();
        }

        function enableFacet() {
            listen();
            vm.isLoadingFacet = true;
            vm.facet.enable();
            emitChange(true);
        }

        function disableFacet() {
            vm.listener();
            vm.facet.disable();
            var forced = vm.facet.getSelectedValue() ? true : false;
            emitChange(forced);
        }

        function handleUpdateSuccess() {
            vm.error = undefined;
            vm.isLoadingFacet = false;
        }

        function handleError(error) {
            if (!vm.facet.hasError()) {
                $log.info(error);
                // The facet has recovered from the error.
                // This happens when an update has been cancelled
                // due to changes in facet selections.
                return;
            }
            $log.error(error);
            vm.isLoadingFacet = false;
            if (error) {
                vm.error = error;
            } else {
                vm.error = 'Error';
            }
        }

        function getFacetSize(facetStates) {
            if (facetStates) {
                return Math.min(facetStates.length + 2, 10).toString();
            }
            return '10';
        }
    }
})();


/*
* Facet for selecting a simple value.
*/
(function() {
    'use strict';

    angular.module('seco.facetedSearch')
    .factory('BasicFacet', BasicFacet);

    /* ngInject */
    function BasicFacet($q, _, AdvancedSparqlService, facetMapperService, NO_SELECTION_STRING, PREFIXES) {

        BasicFacetConstructor.prototype.update = update;
        BasicFacetConstructor.prototype.getState = getState;
        BasicFacetConstructor.prototype.fetchState = fetchState;
        BasicFacetConstructor.prototype.fetchFacetTextFromServices = fetchFacetTextFromServices;
        BasicFacetConstructor.prototype.finalizeFacetValues = finalizeFacetValues;
        BasicFacetConstructor.prototype.getConstraint = getConstraint;
        BasicFacetConstructor.prototype.getTriplePattern = getTriplePattern;
        BasicFacetConstructor.prototype.getSpecifier = getSpecifier;
        BasicFacetConstructor.prototype.buildQueryTemplate = buildQueryTemplate;
        BasicFacetConstructor.prototype.buildQuery = buildQuery;
        BasicFacetConstructor.prototype.buildSelections = buildSelections;
        BasicFacetConstructor.prototype.buildLabelPart = buildLabelPart;
        BasicFacetConstructor.prototype.removeOwnConstraint = removeOwnConstraint;
        BasicFacetConstructor.prototype.getOtherSelections = getOtherSelections;
        BasicFacetConstructor.prototype.getDeselectUnionTemplate = getDeselectUnionTemplate;
        BasicFacetConstructor.prototype.disable = disable;
        BasicFacetConstructor.prototype.enable = enable;
        BasicFacetConstructor.prototype.isLoading = isLoading;
        BasicFacetConstructor.prototype.isEnabled = isEnabled;
        BasicFacetConstructor.prototype.hasError = hasError;
        BasicFacetConstructor.prototype.getSelectedValue = getSelectedValue;

        return BasicFacetConstructor;

        function BasicFacetConstructor(options) {

            /* Implementation */

            this.previousConstraints;
            this.state = {};

            var labelPart =
            ' OPTIONAL {' +
            '  ?value skos:prefLabel ?lbl . ' +
            '  FILTER(langMatches(lang(?lbl), "<PREF_LANG>")) .' +
            ' }' +
            ' OPTIONAL {' +
            '  ?value rdfs:label ?lbl . ' +
            '  FILTER(langMatches(lang(?lbl), "<PREF_LANG>")) .' +
            ' }';

            var serviceQueryTemplate = PREFIXES +
            ' SELECT DISTINCT ?facet_text ?value {' +
            '  VALUES ?value { <VALUES> } ' +
            '  ?value skos:prefLabel|rdfs:label [] . ' +
            '  <LABEL_PART>' +
            '  BIND(?lbl AS ?facet_text)' +
            '  FILTER(?facet_text != "")' +
            ' }';

            var queryTemplate = PREFIXES +
            ' SELECT DISTINCT ?cnt ?facet_text ?value WHERE {' +
            ' { ' +
            '  { ' +
            '   SELECT DISTINCT (count(DISTINCT ?id) as ?cnt) { ' +
            '    <OTHER_SELECTIONS> ' +
            '   } ' +
            '  } ' +
            '  BIND("<NO_SELECTION_STRING>" AS ?facet_text) ' +
            ' } UNION ' +
            '  {' +
            '   SELECT DISTINCT ?cnt ?value ?facet_text { ' +
            '    {' +
            '     SELECT DISTINCT (count(DISTINCT ?id) as ?cnt) ?value {' +
            '      <SELECTIONS> ' +
            '     } GROUP BY ?value ' +
            '    } ' +
            '    FILTER(BOUND(?value)) ' +
            '    <LABEL_PART> ' +
            '    BIND(COALESCE(?lbl, IF(!ISURI(?value), ?value, "")) AS ?facet_text)' +
            '   } ' +
            '  }' +
            ' } ';

            var defaultConfig = {
                preferredLang: 'fi',
                queryTemplate: queryTemplate,
                serviceQueryTemplate: serviceQueryTemplate,
                labelPart: labelPart,
                noSelectionString: NO_SELECTION_STRING,
                usePost: true
            };

            this.config = angular.extend({}, defaultConfig, options);

            this.name = this.config.name;
            this.facetId = this.config.facetId;
            this.predicate = this.config.predicate;
            this.specifier = this.config.specifier;
            if (this.config.enabled) {
                this.enable();
            } else {
                this.disable();
            }

            var endpointConfig = {
                endpointUrl: this.config.endpointUrl,
                usePost: this.config.usePost
            };
            this.endpoint = new AdvancedSparqlService(endpointConfig, facetMapperService);

            // Initial value
            var constVal = _.get(options, 'initial.' + this.facetId);

            if (constVal && constVal.value) {
                this._isEnabled = true;
                this.selectedValue = { value: constVal.value };
            }

            this.labelPart = this.buildLabelPart();
            this.queryTemplate = this.buildQueryTemplate(this.config.queryTemplate);
            this.serviceQueryTemplate = this.buildQueryTemplate(this.config.serviceQueryTemplate);
        }

        function update(constraints) {
            var self = this;
            if (!self.isEnabled()) {
                return $q.when();
            }

            var otherCons = this.getOtherSelections(constraints.constraint);
            if (self.otherCons === otherCons) {
                return $q.when(self.state);
            }
            self.otherCons = otherCons;

            self._isBusy = true;

            return self.fetchState(constraints).then(function(state) {
                if (!_.isEqual(otherCons, self.otherCons)) {
                    return $q.reject('Facet state changed');
                }
                self.state = state;
                self._isBusy = false;

                return state;
            });
        }

        function getState() {
            return this.state;
        }

        // Build a query with the facet selection and use it to get the facet state.
        function fetchState(constraints) {
            var self = this;

            var query = self.buildQuery(constraints.constraint);

            return self.endpoint.getObjectsNoGrouping(query).then(function(results) {
                if (self.config.services) {
                    return self.fetchFacetTextFromServices(results);
                }
                self._error = false;
                return results;
            }).then(function(res) {
                res = self.finalizeFacetValues(res);
                self._error = false;
                return res;
            }).catch(function(error) {
                self._isBusy = false;
                self._error = true;
                return $q.reject(error);
            });
        }

        function finalizeFacetValues(results) {
            results.forEach(function(r) {
                if (!r.text) {
                    r.text = r.value.replace(/^.+\/(.+?)>$/, '$1');
                }
            });
            return [_.head(results)].concat(_.sortBy(_.tail(results), 'text'));
        }

        function fetchFacetTextFromServices(results) {
            var self = this;
            var emptyLabels = _.filter(results, function(r) { return !r.text; });
            var values = _.map(emptyLabels, function(r) { return r.value; });
            var promises = _.map(self.config.services, function(s) {
                var endpointConfig = {
                    endpointUrl: s.replace(/[<>]/g, ''),
                    usePost: self.config.usePost
                };
                var endpoint = new AdvancedSparqlService(endpointConfig, facetMapperService);
                var qry = self.serviceQueryTemplate
                    .replace(/<VALUES>/g, values.join(' '));
                return endpoint.getObjectsNoGrouping(qry);
            });
            return $q.all(promises).then(function(res) {
                var all = _.flatten(res);
                all.forEach(function(objWithText) {
                    _.find(results, ['value', objWithText.value]).text = objWithText.text;
                });
                return results;
            });
        }

        function hasError() {
            return this._error;
        }

        function getTriplePattern() {
            return '?id ' + this.predicate + ' ?value . ';
        }

        function getSpecifier() {
            return this.specifier ? this.specifier : '';
        }

        function getConstraint() {
            if (!this.getSelectedValue()) {
                return;
            }
            if (this.getSelectedValue()) {
                return ' ?id ' + this.predicate + ' ' + this.getSelectedValue() + ' . ';
            }
        }

        function getDeselectUnionTemplate() {
            return this.deselectUnionTemplate;
        }

        // Build the facet query
        function buildQuery(constraints) {
            constraints = constraints || [];
            var otherConstraints = this.removeOwnConstraint(constraints);
            var query = this.queryTemplate
                .replace(/<OTHER_SELECTIONS>/g, otherConstraints.join(' '))
                .replace(/<SELECTIONS>/g, this.buildSelections(otherConstraints));

            return query;
        }

        function buildSelections(constraints) {
            constraints = constraints.join(' ') +
                ' ' + this.getTriplePattern() +
                ' ' + this.getSpecifier();
            return constraints;
        }

        function removeOwnConstraint(constraints) {
            var ownConstraint = this.getConstraint();
            return _.reject(constraints, function(v) { return v === ownConstraint; });
        }

        function getOtherSelections(constraints) {
            return this.removeOwnConstraint(constraints).join(' ');
        }

        function buildLabelPart() {
            var self = this;
            var res = '';
            var langs = _.castArray(self.config.preferredLang).concat(['']);
            langs.forEach(function(lang) {
                res += self.config.labelPart.replace(/<PREF_LANG>/g, lang);
            });
            return res;
        }

        // Replace placeholders in the query template using the configuration.
        function buildQueryTemplate(template) {
            var templateSubs = [
                {
                    placeHolder: /<LABEL_PART>/g,
                    value: this.labelPart
                },
                {
                    placeHolder: /<NO_SELECTION_STRING>/g,
                    value: this.config.noSelectionString
                },
                {
                    placeHolder: /\s+/g,
                    value: ' '
                }
            ];

            templateSubs.forEach(function(s) {
                template = template.replace(s.placeHolder, s.value);
            });
            return template;
        }

        function getSelectedValue() {
            var val;
            if (this.selectedValue) {
                val = this.selectedValue.value;
            }
            return val;
        }

        function isEnabled() {
            return this._isEnabled;
        }

        function enable() {
            this._isEnabled = true;
        }

        function disable() {
            this.selectedValue = undefined;
            this._isEnabled = false;
        }

        function isLoading() {
            return this._isBusy;
        }
    }
})();

(function() {
    'use strict';

    angular.module('seco.facetedSearch')
    .controller('BasicFacetController', BasicFacetController);

    /* ngInject */
    function BasicFacetController($scope, $controller, BasicFacet) {
        var args = { $scope: $scope, FacetImpl: BasicFacet };
        return $controller('AbstractFacetController', args);
    }
})();

(function() {
    'use strict';

    /**
    * @ngdoc directive
    * @name seco.facetedSearch.directive:secoBasicFacet
    * @restrict 'E'
    * @element ANY
    * @description
    * A basic select box facet with text filtering.
    *
    * @param {Object} options The configuration object with the following structure:
    * - **facetId** - `{string}` - A friendly id for the facet.
    *   Should be unique in the set of facets, and should be usable as a SPARQL variable.
    * - **name** - `{string}` - The title of the facet. Will be displayed to end users.
    * - **predicate** - `{string}` - The property (path) that defines the facet values.
    * - **[specifier]** `{string}` - Restriction on the values as a SPARQL triple pattern.
    *   Helpful if multiple facets need to be generated from the same predicate,
    *   or not all values defined by the given predicate should be selectable.
    *   `?value` is the variable to which the facet selection is bound.
    *   For example, if `predicate` has been defined as
    *   `<http://purl.org/dc/terms/subject>` (subject),
    *   and there are different kinds of subjects for the resource, and you want
    *   to select people (`<http://xmlns.com/foaf/0.1/Person>`) only, you would
    *   define `specifier` as `'?value a <http://xmlns.com/foaf/0.1/Person> .'`.
    *   This would generate the following triple patterns:
    *       ?id <http://purl.org/dc/terms/subject> ?value .
    *       ?value a <http://xmlns.com/foaf/0.1/Person> .
    * - **[enabled]** `{boolean}` - Whether or not the facet is enabled by default.
    *   If undefined, the facet will be disabled by default.
    * - **[endpointUrl]** `{string}` - The URL of the SPARQL endpoint.
    *   Optional, as it can also be given globally in
    *   {@link seco.facetedSearch.FacetHandler `FacetHandler`} config.
    * - **[services]** `{Array}` - In case labels for the facet values are (partially)
    *   found in another SPARQL endpoint, those endpoints can be given as a list of URIs.
    *   A separate query is made to each additional service to retrieve the labels.
    * - **[preferredLang]** - `{string|Array}` - The language tag that is preferred
    *   when getting labels for facet values, in case the value is a resource.
    *   The default is 'en'.
    *   Can also be a list of languages, in which case the languages are tried
    *   in order.
    *   If a label is not found in the given languages, a label without a
    *   language tag is used. If a label is still not found,
    *   the end part of the resource URI is used.
    *   Supported label properties are `skos:prefLabel`, and `rdfs:label`.
    * - **[priority]** - `{number}` - Priority for constraint sorting.
    *   Undefined by default.
    */
    angular.module('seco.facetedSearch')
    .directive('secoBasicFacet', basicFacet);

    function basicFacet() {
        return {
            restrict: 'E',
            scope: {
                options: '='
            },
            controller: 'BasicFacetController',
            controllerAs: 'vm',
            templateUrl: 'src/facets/basic/facets.basic-facet.directive.html'
        };
    }
})();

(function() {
    'use strict';

    angular.module('seco.facetedSearch')
    .factory('TextFacet', TextFacet);

    /* ngInject */
    function TextFacet(_) {

        TextFacetConstructor.prototype.getConstraint = getConstraint;
        TextFacetConstructor.prototype.getPreferredLang = getPreferredLang;
        TextFacetConstructor.prototype.disable = disable;
        TextFacetConstructor.prototype.enable = enable;
        TextFacetConstructor.prototype.clear = clear;
        TextFacetConstructor.prototype.isEnabled = isEnabled;
        TextFacetConstructor.prototype.getSelectedValue = getSelectedValue;

        return TextFacetConstructor;

        function TextFacetConstructor(options) {

            /* Implementation */

            var defaultConfig = {
                preferredLang: 'fi'
            };

            this.config = angular.extend({}, defaultConfig, options);

            this.name = this.config.name;
            this.facetId = this.config.facetId;
            this.predicate = this.config.predicate;
            if (this.config.enabled) {
                this.enable();
            } else {
                this.disable();
            }

            // Initial value
            var initial = _.get(options, 'initial.' + this.facetId);
            if (initial && initial.value) {
                this._isEnabled = true;
                this.selectedValue = initial.value;
            }
        }

        function getConstraint() {
            var value = this.getSelectedValue();
            if (!value) {
                return;
            }
            var result = this.useJenaText ? ' ?id text:query "' + value + '*" . ' : '';
            var textVar = '?' + this.facetId;
            result = result + ' ?id ' + this.predicate + ' ' + textVar + ' . ';
            var words = value.replace(/[?,._*'\\/-]/g, ' ');

            words.split(' ').forEach(function(word) {
                result = result + ' FILTER(CONTAINS(LCASE(' + textVar + '), "' +
                        word.toLowerCase() + '")) ';
            });

            return result;
        }

        function getPreferredLang() {
            return this.config.preferredLang;
        }

        function getSelectedValue() {
            return this.selectedValue;
        }

        function clear() {
            this.selectedValue = undefined;
        }

        function isEnabled() {
            return this._isEnabled;
        }

        function enable() {
            this._isEnabled = true;
        }

        function disable() {
            this.selectedValue = undefined;
            this._isEnabled = false;
        }
    }
})();

(function() {
    'use strict';

    angular.module('seco.facetedSearch')
    .controller('TextFacetController', TextFacetController);

    /* ngInject */
    function TextFacetController($scope, _, EVENT_FACET_CHANGED,
            EVENT_REQUEST_CONSTRAINTS, EVENT_INITIAL_CONSTRAINTS, TextFacet) {
        var vm = this;

        vm.changed = changed;
        vm.clear = clear;
        vm.enableFacet = enableFacet;
        vm.disableFacet = disableFacet;
        vm.isFacetEnabled = isFacetEnabled;

        // Wait until the options attribute has been set.
        var watcher = $scope.$watch('options', function(val) {
            if (val) {
                init();
                watcher();
            }
        });

        function init() {
            var initListener = $scope.$on(EVENT_INITIAL_CONSTRAINTS, function(event, cons) {
                var opts = _.cloneDeep($scope.options);
                opts.initial = cons.facets;
                vm.facet = new TextFacet(opts);
                // Unregister initListener
                initListener();
            });
            $scope.$emit(EVENT_REQUEST_CONSTRAINTS);
        }

        function emitChange() {
            var val = vm.facet.getSelectedValue();
            var args = {
                id: vm.facet.facetId,
                constraint: vm.facet.getConstraint(),
                value: val
            };
            $scope.$emit(EVENT_FACET_CHANGED, args);
        }

        function changed() {
            emitChange();
        }

        function clear() {
            vm.facet.clear();
            emitChange();
        }

        function enableFacet() {
            vm.facet.enable();
        }

        function disableFacet() {
            vm.facet.disable();
            emitChange();
        }

        function isFacetEnabled() {
            if (!vm.facet) {
                return false;
            }
            return vm.facet.isEnabled();
        }

    }
})();

(function() {
    'use strict';

    /**
    * @ngdoc directive
    * @name seco.facetedSearch.directive:secoTextFacet
    * @restrict 'E'
    * @element ANY
    * @description
    * A free-text search facet.
    *
    * Does not make any SPARQL queries, just generates SPARQL triple patterns
    * out of the typed text for other facets to use.
    *
    * @param {Object} options The configuration object with the following structure:
    * - **facetId** - `{string}` - A friendly id for the facet.
    *   Should be unique in the set of facets, and should be usable as a SPARQL variable.
    * - **predicate** - `{string}` - The predicate or property path that defines the facet values.
    * - **name** - `{string}` - The title of the facet. Will be displayed to end users.
    * - **[enabled]** `{boolean}` - Whether or not the facet is enabled by default.
    *   If undefined, the facet will be disabled by default.
    * - **[priority]** - `{number}` - Priority for constraint sorting.
    *   Undefined by default.
    */
    angular.module('seco.facetedSearch')
    .directive('secoTextFacet', textFacet);

    function textFacet() {
        return {
            restrict: 'E',
            scope: {
                options: '='
            },
            controller: 'TextFacetController',
            controllerAs: 'vm',
            templateUrl: 'src/facets/text/facets.text-facet.directive.html'
        };
    }
})();

(function() {
    'use strict';

    angular.module('seco.facetedSearch')
    .value('textQueryPredicate', '<http://jena.apache.org/text#query>')
    .factory('JenaTextFacet', JenaTextFacet);

    /* ngInject */
    function JenaTextFacet(_, TextFacet, textQueryPredicate) {

        JenaTextFacet.prototype = Object.create(TextFacet.prototype);
        JenaTextFacet.prototype.getConstraint = getConstraint;
        JenaTextFacet.prototype.sanitize = sanitize;

        return JenaTextFacet;

        function JenaTextFacet(options) {
            TextFacet.call(this, options);
            this.config.priority = this.config.priority || 0;
        }

        function getConstraint() {
            var value = this.getSelectedValue();
            if (!value) {
                return undefined;
            }

            value = this.sanitize(value);

            var args = [];
            if (this.config.predicate) {
                args.push(this.config.predicate);
            }

            args.push('"' + value + '"');

            if (this.config.limit) {
                args.push(this.config.limit);
            }

            var obj = '(' + args.join(' ') + ')';

            var result = '(?id ?score) ' + textQueryPredicate + ' ' + obj + ' .';

            if (this.config.graph) {
                result = 'GRAPH ' + this.config.graph + ' { ' + result + ' }';
            }

            return result || undefined;
        }

        function sanitize(query) {
            query = query
                .replace(/[\\()]/g, '') // backslashes, and parentheses
                .replace(/~{2,}/g, '~') // double ~
                .replace(/^~/g, '') // ~ as first token
                .replace(/(\b~*(AND|OR|NOT)\s*~*)+$/g, '') // AND, OR, NOT last
                .replace(/^((AND|OR|NOT)\b\s*~*)+/g, ''); // AND, OR, NOT first

            var quoteRepl;
            if ((query.match(/"/g) || []).length % 2) {
                // Unbalanced quotes, remove them
                quoteRepl = '';
            } else {
                // Balanced quotes, escape them
                quoteRepl = '\\"';
            }
            query = query.replace(/"/g, quoteRepl).trim();

            return query;
        }
    }
})();

(function() {
    'use strict';

    angular.module('seco.facetedSearch')
    .controller('JenaTextFacetController', JenaTextFacetController);

    /* ngInject */
    function JenaTextFacetController($controller, $scope, JenaTextFacet) {
        var args = { $scope: $scope, TextFacet: JenaTextFacet };
        return $controller('TextFacetController', args);
    }
})();

(function() {
    'use strict';

    /**
    * @ngdoc directive
    * @name seco.facetedSearch.directive:secoJenaTextFacet
    * @restrict 'E'
    * @element ANY
    * @description
    * A free-text search facet using Jena text search.
    *
    * This facet can only be used if the SPARQL endpoint supports
    * [Jena text query](https://jena.apache.org/documentation/query/text-query.html).
    *
    * The facet does not make any SPARQL queries, just generates SPARQL triple patterns
    * out of the typed text for other facets to use.
    *
    * The produced constraint looks like the following (where `predicate`, and `limit`
    * are based on the configuration options, and left out if undefined):
    * <pre>
    * (?id ?score) <http://jena.apache.org/text#query> (predicate "search terms" limit) .
    * </pre>
    *
    * If the `graph` option is defined, the constraint is wrapped accordingly:
    * <pre>
    * GRAPH graph {
    *   (?id ?score) <http://jena.apache.org/text#query> (predicate "search terms" limit) .
    * }
    * </pre>
    *
    * The score is captured as variable `?score`, and can thus be used to sort results.
    *
    * The search terms are sanitized in order to avoid syntax errors from the backend.
    * In case there is an even number of quotes (`"`) they are escaped in the search terms.
    * If there is an odd number of quotes, they are removed.
    * Backslashes, and parentheses are removed, as well as `AND`, `OR`, and `NOT`,
    * if they are the first or last tokens in the query.
    * "`~`" is removed if it's the first character in the query.
    * Consecutive "`~`" are removed. I.e. "`~~`" is changed to "`~`".
    * Otherwise the search terms are not modified, so e.g. `AND`, `OR`, `*`, and `~`
    * can be used in the query.
    *
    * The sanitization is not reflected to the user, so if a search triggers while
    * they are writing, and the last word they wrote is, e.g., "`AND`",
    * the user will see all that they wrote, but the produced SPARQL constraint
    * will not include the `AND`. The user can continue writing, and the next time
    * the search triggers, if the query is valid, the constraint will include it.
    *
    * @param {Object} options The configuration object with the following structure:
    * - **facetId** - `{string}` - A friendly id for the facet.
    *   Should be unique in the set of facets, and should be usable as a SPARQL variable.
    * - **name** - `{string}` - The title of the facet. Will be displayed to end users.
    * - **[enabled]** `{boolean}` - Whether or not the facet is enabled by default.
    *   If undefined, the facet will be disabled by default.
    * - **[priority]** - `{number}` - Priority for constraint sorting. Default is 0.
    * - **[predicate]** - `{string}` - The property to use in the search.
    *   See [Jena text query documentation](https://jena.apache.org/documentation/query/text-query.html#query-with-sparql).
    * - **[limit]** - `{number}` - Limit for the text search results.
    *   See [Jena text query documentation](https://jena.apache.org/documentation/query/text-query.html#query-with-sparql).
    * - **[graph]** - `{string}` - The URI of the graph to use for the text search.
    *
    */
    angular.module('seco.facetedSearch')
    .directive('secoJenaTextFacet', jenaTextFacet);

    function jenaTextFacet() {
        return {
            restrict: 'E',
            scope: {
                options: '='
            },
            controller: 'JenaTextFacetController',
            controllerAs: 'vm',
            templateUrl: 'src/facets/text/facets.text-facet.directive.html'
        };
    }
})();

(function() {
    'use strict';

    angular.module('seco.facetedSearch')

    .factory('timespanMapperService', timespanMapperService);

    /* ngInject */
    function timespanMapperService(_, objectMapperService) {
        TimespanMapper.prototype.makeObject = makeObject;
        TimespanMapper.prototype.parseValue = parseValue;

        var proto = Object.getPrototypeOf(objectMapperService);
        TimespanMapper.prototype = angular.extend({}, proto, TimespanMapper.prototype);

        return new TimespanMapper();

        function TimespanMapper() {
            this.objectClass = Object;
        }

        function makeObject(obj) {
            var o = new this.objectClass();

            o.min = parseValue(_.get(obj, 'min.value'));
            o.max = parseValue(_.get(obj, 'max.value'));

            return o;
        }

        function parseValue(value) {
            if (!value) {
                return undefined;
            }
            var d = _(value.substring(0, 10).split('-')).map(function(d) { return parseInt(d, 10); }).value();
            return new Date(d[0], d[1]-1, d[2]);
        }
    }
})();


/*
* Facet for selecting a date range
*/
(function() {
    'use strict';

    angular.module('seco.facetedSearch')
    .factory('TimespanFacet', TimespanFacet);

    /* ngInject */
    function TimespanFacet($q, _, AdvancedSparqlService, timespanMapperService, BasicFacet,
            PREFIXES) {
        TimespanFacetConstructor.prototype = Object.create(BasicFacet.prototype);

        TimespanFacetConstructor.prototype.getSelectedValue = getSelectedValue;
        TimespanFacetConstructor.prototype.getConstraint = getConstraint;
        TimespanFacetConstructor.prototype.buildQueryTemplate = buildQueryTemplate;
        TimespanFacetConstructor.prototype.buildQuery = buildQuery;
        TimespanFacetConstructor.prototype.fetchState = fetchState;
        TimespanFacetConstructor.prototype.update = update;
        TimespanFacetConstructor.prototype.disable = disable;
        TimespanFacetConstructor.prototype.enable = enable;
        TimespanFacetConstructor.prototype.getOtherSelections = getOtherSelections;
        TimespanFacetConstructor.prototype.initState = initState;
        TimespanFacetConstructor.prototype.getMinDate = getMinDate;
        TimespanFacetConstructor.prototype.getMaxDate = getMaxDate;
        TimespanFacetConstructor.prototype.getSelectedStartDate = getSelectedStartDate;
        TimespanFacetConstructor.prototype.getSelectedEndDate = getSelectedEndDate;
        TimespanFacetConstructor.prototype.updateState = updateState;

        return TimespanFacetConstructor;

        function TimespanFacetConstructor(options) {
            var simpleTemplate = PREFIXES +
            ' SELECT (min(xsd:date(?value)) AS ?min) (max(xsd:date(?value)) AS ?max) { ' +
            '   <SELECTIONS> ' +
            '   ?id <START_PROPERTY> ?value . ' +
            ' } ';

            var separateTemplate = PREFIXES +
            ' SELECT ?min ?max { ' +
            '   { ' +
            '     SELECT (min(xsd:date(?start)) AS ?min) { ' +
            '       <SELECTIONS> ' +
            '       ?id <START_PROPERTY> ?start . ' +
            '     } ' +
            '   } ' +
            '   { ' +
            '     SELECT (max(xsd:date(?end)) AS ?max) { ' +
            '       <SELECTIONS> ' +
            '       ?id <END_PROPERTY> ?end . ' +
            '     } ' +
            '   } ' +
            ' } ';

            var defaultConfig = {};

            this.config = angular.extend({}, defaultConfig, options);

            this.name = this.config.name;
            this.facetId = this.config.facetId;
            this.startPredicate = this.config.startPredicate;
            this.endPredicate = this.config.endPredicate;

            if (angular.isString(this.config.min)) {
                this.minDate = timespanMapperService.parseValue(this.config.min);
            } else {
                this.minDate = this.config.min;
            }
            if (angular.isString(this.config.max)) {
                this.maxDate = timespanMapperService.parseValue(this.config.max);
            } else {
                this.maxDate = this.config.max;
            }

            this.initState();

            if (this.config.enabled) {
                this.enable();
            } else {
                this.disable();
            }

            this.endpoint = new AdvancedSparqlService(this.config.endpointUrl,
                timespanMapperService);

            this.queryTemplate = this.buildQueryTemplate(
                this.startPredicate === this.endPredicate ? simpleTemplate : separateTemplate);

            this.varSuffix = this.facetId;

            this.selectedValue = {};

            // Initial value
            var initial = _.get(options, 'initial.' + this.facetId);
            if (initial && initial.value) {
                this._isEnabled = true;
                this.selectedValue = {};
                if (initial.value.start) {
                    this.selectedValue.start = timespanMapperService.parseValue(initial.value.start);
                }
                if (initial.value.end) {
                    this.selectedValue.end = timespanMapperService.parseValue(initial.value.end);
                }
            }
        }

        function initState() {
            if (!this.state) {
                this.state = {};
            }

            this.state.start = {
                minDate: this.getMinDate(),
                maxDate: this.getMaxDate(),
                initDate: this.getMinDate(),
                startingDay: this.config.startingDay || 1
            };

            this.state.end = {
                minDate: this.getMinDate(),
                maxDate: this.getMaxDate(),
                initDate: this.getMaxDate(),
                startingDay: this.config.startingDay || 1
            };
        }

        function update(constraints) {
            var self = this;
            if (!self.isEnabled()) {
                return $q.when();
            }

            var otherCons = this.getOtherSelections(constraints.constraint);
            if (self.otherCons === otherCons) {
                // Only this facet's selection has changed
                self.updateState({ min: self.getMinDate(), max: self.getMaxDate() });
                return $q.when(self.state);
            }
            self.otherCons = otherCons;

            self._isBusy = true;

            return self.fetchState(constraints).then(function(state) {
                if (!_.isEqual(self.otherCons, otherCons)) {
                    return $q.reject('Facet state changed');
                }
                self.state = state;
                self._isBusy = false;

                return state;
            });
        }


        function getMinDate() {
            return _.clone(this.minDate);
        }

        function getMaxDate() {
            return _.clone(this.maxDate);
        }

        function enable() {
            BasicFacet.prototype.enable.call(this);
        }

        function disable() {
            BasicFacet.prototype.disable.call(this);
            this.initState();
        }

        function buildQueryTemplate(template) {
            return template
                .replace(/<START_PROPERTY>/g, this.startPredicate)
                .replace(/<END_PROPERTY>/g, this.endPredicate)
                .replace(/\s+/g, ' ');
        }

        function buildQuery(constraints) {
            constraints = constraints || [];
            var query = this.queryTemplate
                .replace(/<SELECTIONS>/g, this.getOtherSelections(constraints));
            return query;
        }

        function getOtherSelections(constraints) {
            var ownConstraint = this.getConstraint();

            var selections = _.reject(constraints, function(v) { return v === ownConstraint; });
            return selections.join(' ');
        }

        // Build a query with the facet selection and use it to get the facet state.
        function fetchState(constraints) {
            var self = this;

            var query = self.buildQuery(constraints.constraint);

            return self.endpoint.getObjectsNoGrouping(query).then(function(results) {
                var state = _.first(results);

                self.updateState(state);

                self._error = false;

                return self.state;
            }).catch(function(error) {
                self._isBusy = false;
                self._error = true;
                return $q.reject(error);
            });
        }

        function updateState(minMax) {
            var self = this;

            var minDate = self.getMinDate();
            if (!minMax.min || minMax.min < minDate) {
                minMax.min = minDate;
            }

            var maxDate = self.getMaxDate();
            if (!minMax.max || minMax.max > maxDate) {
                minMax.max = maxDate;
            }

            var selectedStart = self.getSelectedStartDate();
            self.state.start.initDate = selectedStart || minMax.min;
            self.state.start.minDate = minMax.min;
            self.state.start.maxDate = minMax.max;

            var selectedEnd = self.getSelectedEndDate();
            self.state.end.initDate = selectedEnd || minMax.max;
            self.state.end.minDate = minMax.min;
            self.state.end.maxDate = minMax.max;

            if (selectedEnd < self.state.start.maxDate) {
                self.state.start.maxDate = selectedEnd;
            }

            if (selectedStart > self.state.end.minDate) {
                self.state.end.minDate = selectedStart;
            }

            return self.state;
        }

        function getSelectedStartDate() {
            return _.clone((this.selectedValue || {}).start);
        }

        function getSelectedEndDate() {
            return _.clone((this.selectedValue || {}).end);
        }

        function getSelectedValue() {
            if (!this.selectedValue) {
                return;
            }
            var selectedValue = {};
            if (this.selectedValue.start) {
                selectedValue.start = getISOStringFromDate(this.selectedValue.start);
            }
            if (this.selectedValue.end) {
                selectedValue.end = getISOStringFromDate(this.selectedValue.end);
            }
            return selectedValue;
        }

        function getConstraint() {
            var result =
            ' <START_FILTER> ' +
            ' <END_FILTER> ';

            var value = this.getSelectedValue() || {};

            var start = value.start;
            var end = value.end;

            if (!(start || end)) {
                return '';
            }

            var startFilter =
            ' ?id <START_PROPERTY> <VAR> . ' +
            ' FILTER(<http://www.w3.org/2001/XMLSchema#date>(<VAR>) >= "<START_VALUE>"^^<http://www.w3.org/2001/XMLSchema#date>) ';

            var endFilter =
            ' ?id <END_PROPERTY> <VAR> . ' +
            ' FILTER(<http://www.w3.org/2001/XMLSchema#date>(<VAR>) <= "<END_VALUE>"^^<http://www.w3.org/2001/XMLSchema#date>) ';

            var startVar = '?start_' + this.varSuffix;
            var endVar = '?end_' + this.varSuffix;

            if (this.startPredicate === this.endPredicate) {
                endVar = startVar;
            }

            startFilter = startFilter.replace(/<VAR>/g, startVar);
            endFilter = endFilter.replace(/<VAR>/g, endVar);

            if (start) {
                result = result
                    .replace('<START_FILTER>',
                        startFilter.replace('<START_PROPERTY>',
                            this.startPredicate))
                    .replace('<START_VALUE>', start);
            } else {
                result = result.replace('<START_FILTER>', '');
            }
            if (end) {
                result = result
                    .replace('<END_FILTER>',
                        endFilter.replace('<END_PROPERTY>',
                            this.endPredicate))
                    .replace('<END_VALUE>', end);
            } else {
                result = result.replace('<END_FILTER>', '');
            }
            return result;
        }

        function getISOStringFromDate(d) {
            var mm = (d.getMonth() + 1).toString();
            var dd = d.getDate().toString();
            mm = mm.length === 2 ? mm : '0' + mm;
            dd = dd.length === 2 ? dd : '0' + dd;

            return [d.getFullYear(), mm, dd].join('-');
        }
    }
})();

(function() {
    'use strict';

    angular.module('seco.facetedSearch')
    .controller('TimespanFacetController', TimespanFacetController);

    /* ngInject */
    function TimespanFacetController($scope, $controller, TimespanFacet) {
        var args = { $scope: $scope, FacetImpl: TimespanFacet };
        return $controller('AbstractFacetController', args);
    }
})();

(function() {
    'use strict';

    /**
    * @ngdoc directive
    * @name seco.facetedSearch.directive:secoTimespanFacet
    * @restrict 'E'
    * @element ANY
    * @description
    * A facet for selecting date ranges.
    *
    * Restricts the selectable dates by getting the minimum and maximum dates
    * based on the underlying data, and facet selections.
    *
    * Currently only supports values of the type <http://www.w3.org/2001/XMLSchema#date>,
    * and there is no support for timezones. Any timezones in the values retrieved
    * will be discarded.
    *
    * @param {Object} options The configuration object with the following structure:
    * - **facetId** - `{string}` - A friendly id for the facet.
    *   Should be unique in the set of facets, and should be usable as a SPARQL variable.
    * - **name** - `{string}` - The title of the facet. Will be displayed to end users.
    * - **startPredicate** - `{string}` - The predicate or property path that defines
    *   the start date of the date range.
    * - **endPredicate** - `{string}` - The predicate or property path that defines
    *   the end date of the date range.
    * - **[min]** - `{string|Date}` - The earliest selectable date. If string, should
    *   be in ISO format. Giving a Date object that has a predefined timezone other
    *   than the user's may lead to timezone issues.
    * - **[max]** - `{string|Date}` - The earliest selectable date. If string, should
    *   be in ISO format. Giving a Date object that has a predefined timezone other
    *   than the user's may lead to timezone issues.
    * - **[enabled]** `{boolean}` - Whether or not the facet is enabled by default.
    *   If undefined, the facet will be disabled by default.
    * - **[priority]** - `{number}` - Priority for constraint sorting.
    *   Undefined by default.
    */
    angular.module('seco.facetedSearch')
    .directive('secoTimespanFacet', timespanFacet);

    function timespanFacet() {
        return {
            restrict: 'E',
            scope: {
                options: '='
            },
            controller: 'TimespanFacetController',
            controllerAs: 'vm',
            templateUrl: 'src/facets/timespan/facets.timespan-facet.directive.html'
        };
    }
})();


/*
* Facet for selecting a date range
*/
(function() {
    'use strict';

    angular.module('seco.facetedSearch')
    .factory('CheckboxFacet', CheckboxFacet);

    /* ngInject */
    function CheckboxFacet($q, _, AdvancedSparqlService, facetMapperService, BasicFacet,
            PREFIXES) {
        CheckboxFacet.prototype = Object.create(BasicFacet.prototype);

        CheckboxFacet.prototype.getConstraint = getConstraint;
        CheckboxFacet.prototype.buildQueryTemplate = buildQueryTemplate;
        CheckboxFacet.prototype.buildQuery = buildQuery;
        CheckboxFacet.prototype.fetchState = fetchState;

        return CheckboxFacet;

        function CheckboxFacet(options) {

            var queryTemplate = PREFIXES +
            ' SELECT DISTINCT ?value ?facet_text ?cnt WHERE { ' +
            '  <PREDICATE_UNION> ' +
            ' } ';

            var predTemplate =
            ' { ' +
            '  SELECT DISTINCT (COUNT(DISTINCT(?id)) AS ?cnt) ("<ID>" AS ?value)' +
            '     ("<LABEL>" AS ?facet_text) { ' +
            '   <SELECTIONS> ' +
            '   BIND("<ID>" AS ?val) ' +
            '   <PREDICATE> ' +
            '  } GROUP BY ?val ' +
            ' } ';

            var defaultConfig = {
                usePost: true
            };

            this.config = angular.extend({}, defaultConfig, options);

            this.name = this.config.name;
            this.facetId = this.config.facetId;
            this.state = {};

            if (this.config.enabled) {
                this.enable();
            } else {
                this.disable();
            }

            var endpointConfig = {
                endpointUrl: this.config.endpointUrl,
                usePost: this.config.usePost
            };

            this.endpoint = new AdvancedSparqlService(endpointConfig, facetMapperService);

            this.queryTemplate = this.buildQueryTemplate(queryTemplate, predTemplate);

            this.selectedValue = {};

            // Initial value
            var initial = _.get(options, 'initial[' + this.facetId + '].value');
            if (initial) {
                this._isEnabled = true;
                this.selectedValue = { value: initial };
            }
        }

        function buildQueryTemplate(template, predTemplate) {
            var unions = '';
            this.config.choices.forEach(function(pred) {
                var union = predTemplate
                    .replace(/<ID>/g, pred.id)
                    .replace(/<PREDICATE>/g, pred.pattern)
                    .replace(/<LABEL>/g, pred.label);
                if (unions) {
                    union = ' UNION ' + union;
                }
                unions += union;
            });

            return template
                .replace(/<PREDICATE_UNION>/g, unions)
                .replace(/\s+/g, ' ');
        }

        function buildQuery(constraints) {
            constraints = constraints || [];
            var query = this.queryTemplate
                .replace(/<SELECTIONS>/g, this.getOtherSelections(constraints));
            return query;
        }

        // Build a query with the facet selection and use it to get the facet state.
        function fetchState(constraints) {
            var self = this;

            var query = self.buildQuery(constraints.constraint);

            return self.endpoint.getObjectsNoGrouping(query).then(function(results) {
                self._error = false;
                return results;
            }).catch(function(error) {
                self._isBusy = false;
                self._error = true;
                return $q.reject(error);
            });
        }

        function getConstraint() {
            var self = this;
            var selections = _.compact(self.getSelectedValue());
            if (!(selections.length)) {
                return;
            }
            var res = '';
            selections.forEach(function(val) {
                var cons = _.get(_.find(self.config.choices, ['id', val.replace(/"/g, '')]), 'pattern');
                if (res) {
                    cons = ' UNION { ' + cons + ' } ';
                } else if (selections.length > 1) {
                    cons = ' { ' + cons + ' } ';
                }
                res += cons;
            });

            return res;
        }
    }
})();

(function() {
    'use strict';

    angular.module('seco.facetedSearch')
    .controller('CheckboxFacetController', CheckboxFacetController);

    /* ngInject */
    function CheckboxFacetController($scope, $controller, CheckboxFacet) {
        var args = { $scope: $scope, FacetImpl: CheckboxFacet };
        return $controller('AbstractFacetController', args);
    }
})();

(function() {
    'use strict';

    /**
    * @ngdoc directive
    * @name seco.facetedSearch.directive:secoCheckboxFacet
    * @restrict 'E'
    * @element ANY
    * @description
    * A facet for a checkbox selector based on a triple pattern.
    *
    * If multiple checkboxes are selected, the resulting SPARQL constraint
    * will be a union of the selections.
    *
    * @param {Object} options The configuration object with the following structure:
    * - **facetId** - `{string}` - A friendly id for the facet.
    *   Should be unique in the set of facets, and should be usable as a SPARQL variable.
    * - **name** - `{string}` - The title of the facet. Will be displayed to end users.
    * - **choices** - `{Array}` - A list of choices and their definitions.
    *   Each element in the list should be an object:
    *   `{ id: 'uniqueIdForThisChoice', pattern: '[SPARQL pattern]', label: 'choice label' }`.
    *   `[SPARQL pattern]` is any SPARQL pattern where `?id` is the variable bound to the
    *   result resource. Example:
    *       {
    *         id: 'hobby',
    *         pattern: '?id <http://schema.org/hobby> [] .',
    *         label: 'Hobby'
    *       }
    *   This would create a checkbox which would restrict the results to those
    *   resources that have a value for the property `<http://schema.org/hobby>`.
    * - **[enabled]** `{boolean}` - Whether or not the facet is enabled by default.
    *   If undefined, the facet will be disabled by default.
    * - **[priority]** - `{number}` - Priority for constraint sorting.
    *   Undefined by default.
    */
    angular.module('seco.facetedSearch')
    .directive('secoCheckboxFacet', checkboxFacet);

    function checkboxFacet() {
        return {
            restrict: 'E',
            scope: {
                options: '='
            },
            controller: 'CheckboxFacetController',
            controllerAs: 'vm',
            templateUrl: 'src/facets/checkbox/facets.checkbox-facet.directive.html'
        };
    }
})();

/*
* Facet for selecting a simple value.
*/
(function() {
    'use strict';

    angular.module('seco.facetedSearch')

    .factory('HierarchyFacet', HierarchyFacet);

    /* ngInject */
    function HierarchyFacet($q, _, BasicFacet, PREFIXES) {

        HierarchyFacetConstructor.prototype = Object.create(BasicFacet.prototype);

        HierarchyFacetConstructor.prototype.getSelectedValue = getSelectedValue;
        HierarchyFacetConstructor.prototype.getConstraint = getConstraint;
        HierarchyFacetConstructor.prototype.buildQueryTemplate = buildQueryTemplate;
        HierarchyFacetConstructor.prototype.buildQuery = buildQuery;
        HierarchyFacetConstructor.prototype.getHierarchyClasses = getHierarchyClasses;
        HierarchyFacetConstructor.prototype.fetchState = fetchState;

        return HierarchyFacetConstructor;

        function HierarchyFacetConstructor(options) {

            var queryTemplate = PREFIXES +
            ' SELECT DISTINCT ?cnt ?facet_text ?value WHERE {' +
            ' { ' +
            '  { ' +
            '   SELECT DISTINCT (count(DISTINCT ?id) as ?cnt) { ' +
            '    <OTHER_SELECTIONS> ' +
            '   } ' +
            '  } ' +
            '  BIND("<NO_SELECTION_STRING>" AS ?facet_text) ' +
            ' } UNION ' +
            '  {' +
            '   SELECT DISTINCT ?cnt ?value ?facet_text {' +
            '    {' +
            '     SELECT DISTINCT (count(DISTINCT ?id) as ?cnt) ?value ?class {' +
            '      VALUES ?class { ' +
            '       <HIERARCHY_CLASSES> ' +
            '      } ' +
            '      ?value <PROPERTY> ?class . ' +
            '      ?h <PROPERTY> ?value . ' +
            '      ?id <ID> ?h .' +
            '      <OTHER_SELECTIONS> ' +
            '     } GROUP BY ?class ?value ' +
            '    } ' +
            '    FILTER(BOUND(?value))' +
            '    <LABEL_PART> ' +
            '    BIND(COALESCE(?lbl, STR(?value)) as ?label)' +
            '    BIND(IF(?value = ?class, ?label, CONCAT("-- ", ?label)) as ?facet_text)' +
            '    BIND(IF(?value = ?class, 0, 1) as ?order)' +
            '   } ORDER BY ?class ?order ?facet_text' +
            '  } ' +
            ' } ';

            options.queryTemplate = options.queryTemplate || queryTemplate;

            BasicFacet.call(this, options);

            this.selectedValue;

            // Initial value
            var constVal = _.get(options, 'initial.' + this.facetId);
            if (constVal && constVal.value) {
                this._isEnabled = true;
                this.selectedValue = { value: constVal.value };
            }

            var triplePatternTemplate =
            ' VALUES ?<CLASS_VAR> { ' +
            '  <HIERARCHY_CLASSES> ' +
            ' } ' +
            ' ?<H_VAR> <PROPERTY> ?<CLASS_VAR> . ' +
            ' ?<V_VAR> <PROPERTY> ?<H_VAR> . ' +
            ' ?id <ID> ?<V_VAR> .';

            this.triplePatternTemplate = this.buildQueryTemplate(triplePatternTemplate);
        }

        function buildQueryTemplate(template) {
            var templateSubs = [
                {
                    placeHolder: /<ID>/g,
                    value: this.predicate
                },
                {
                    placeHolder: /<PROPERTY>/g,
                    value: this.config.hierarchy
                },
                {
                    placeHolder: /<LABEL_PART>/g,
                    value: this.labelPart
                },
                {
                    placeHolder: /<NO_SELECTION_STRING>/g,
                    value: this.config.noSelectionString
                },
                {
                    placeHolder: /<H_VAR>/g,
                    value: 'seco_h_' + this.facetId
                },
                {
                    placeHolder: /<V_VAR>/g,
                    value: 'seco_v_' + this.facetId
                },
                {
                    placeHolder: /<CLASS_VAR>/g,
                    value: 'seco_class_' + this.facetId
                },
                {
                    placeHolder: /\s+/g,
                    value: ' '
                }
            ];

            templateSubs.forEach(function(s) {
                template = template.replace(s.placeHolder, s.value);
            });
            return template;
        }

        function getHierarchyClasses() {
            return this.config.classes || [];
        }

        function getConstraint() {
            if (!this.getSelectedValue()) {
                return;
            }
            var res = this.triplePatternTemplate
                .replace(/<HIERARCHY_CLASSES>/g, this.getSelectedValue());

            return res;
        }

        function getSelectedValue() {
            var val;
            if (this.selectedValue) {
                val = this.selectedValue.value;
            }
            return val;
        }

        function fetchState(constraints) {
            var self = this;

            var query = self.buildQuery(constraints.constraint);

            return self.endpoint.getObjectsNoGrouping(query).then(function(results) {
                self._error = false;
                return results;
            }).catch(function(error) {
                self._isBusy = false;
                self._error = true;
                return $q.reject(error);
            });
        }

        // Build the facet query
        function buildQuery(constraints) {
            constraints = constraints || [];
            var query = this.queryTemplate
                .replace(/<OTHER_SELECTIONS>/g, this.getOtherSelections(constraints))
                .replace(/<HIERARCHY_CLASSES>/g,
                    this.getSelectedValue() || this.getHierarchyClasses().join(' '));

            return query;
        }
    }
})();

(function() {
    'use strict';

    angular.module('seco.facetedSearch')
    .controller('HierarchyFacetController', HierarchyFacetController);

    /* ngInject */
    function HierarchyFacetController($scope, $controller, HierarchyFacet) {
        var args = { $scope: $scope, FacetImpl: HierarchyFacet };
        return $controller('AbstractFacetController', args);
    }
})();

(function() {
    'use strict';

    /**
    * @ngdoc directive
    * @name seco.facetedSearch.directive:secoHierarchyFacet
    * @restrict 'E'
    * @element ANY
    * @description
    * A select box facet for hierarchical values.
    *
    * @param {Object} options The configuration object with the following structure:
    * - **facetId** - `{string}` - A friendly id for the facet.
    *   Should be unique in the set of facets, and should be usable as a SPARQL variable.
    * - **predicate** - `{string}` - The predicate or property path that defines the facet values.
    * - **hierarchy** - `{string}` - The predicate or property path that defines the hierarchy of values.
    * - **classes** - `{Array}` - The top level resources (URIs) of the hierarchy.
    * - **name** - `{string}` - The title of the facet. Will be displayed to end users.
    * - **[enabled]** `{boolean}` - Whether or not the facet is enabled by default.
    *   If undefined, the facet will be disabled by default.
    * - **[endpointUrl]** `{string}` - The URL of the SPARQL endpoint.
    *   Optional, as it can also be given globally in
    *   {@link seco.facetedSearch.FacetHandler `FacetHandler`} config.
    * - **[preferredLang]** - `{string|Array}` - The language tag that is preferred
    *   when getting labels for facet values, in case the value is a resource.
    *   The default is 'en'.
    *   Can also be a list of languages, in which case the languages are tried
    *   in order.
    *   If a label is not found in the given languages, a label without a
    *   language tag is used. If a label is still not found,
    *   the end part of the resource URI is used.
    *   Supported label properties are `skos:prefLabel`, and `rdfs:label`.
    * - **[priority]** - `{number}` - Priority for constraint sorting.
    *   Undefined by default.
    */
    angular.module('seco.facetedSearch')
    .directive('secoHierarchyFacet', hierarchyFacet);

    function hierarchyFacet() {
        return {
            restrict: 'E',
            scope: {
                options: '='
            },
            controller: 'HierarchyFacetController',
            controllerAs: 'vm',
            templateUrl: 'src/facets/basic/facets.basic-facet.directive.html'
        };
    }
})();

angular.module('seco.facetedSearch').run(['$templateCache', function($templateCache) {
  'use strict';

  $templateCache.put('src/facets/basic/facets.basic-facet.directive.html',
    "<div class=\"facet-wrapper\">\n" +
    "  <div class=\"facet\" ng-if=vm.facet.isEnabled()>\n" +
    "    <div class=\"well well-sm\">\n" +
    "      <div class=\"row\">\n" +
    "        <div class=\"col-xs-12 text-left\">\n" +
    "          <div class=\"alert alert-danger\" ng-if=\"vm.error\">{{ vm.error|limitTo:100 }}</div>\n" +
    "          <span spinner-key=\"vm.getSpinnerKey()\" spinner-start-active=\"true\"\n" +
    "            us-spinner=\"{radius:30, width:8, length: 40}\" ng-if=\"vm.isLoading()\"></span>\n" +
    "          <h5 class=\"facet-name pull-left\">{{ vm.facet.name }}</h5>\n" +
    "          <button\n" +
    "            ng-disabled=\"vm.isLoading()\"\n" +
    "            ng-click=\"vm.disableFacet()\"\n" +
    "            class=\"btn btn-danger btn-xs pull-right glyphicon glyphicon-remove\">\n" +
    "          </button>\n" +
    "          <div class=\"facet-input-container\">\n" +
    "            <input\n" +
    "            ng-disabled=\"vm.isLoading()\"\n" +
    "            type=\"text\"\n" +
    "            class=\"form-control\"\n" +
    "            ng-model=\"textFilter\" />\n" +
    "            <select\n" +
    "              ng-change=\"vm.changed()\"\n" +
    "              ng-disabled=\"vm.isLoading()\"\n" +
    "              ng-attr-size=\"{{ vm.getFacetSize(vm.facet.getState()) }}\"\n" +
    "              id=\"{{ ::vm.facet.name + '_select' }}\"\n" +
    "              class=\"selector form-control\"\n" +
    "              ng-options=\"value as (value.text + ' (' + value.count + ')') for value in vm.facet.getState() | textWithSelection:textFilter:vm.facet.selectedValue track by value.value\"\n" +
    "              ng-model=\"vm.facet.selectedValue\">\n" +
    "            </select>\n" +
    "          </div>\n" +
    "        </div>\n" +
    "      </div>\n" +
    "    </div>\n" +
    "  </div>\n" +
    "  <div class=\"facet\" ng-if=!vm.facet.isEnabled()>\n" +
    "    <div class=\"well well-sm\">\n" +
    "      <div class=\"row\">\n" +
    "        <div class=\"col-xs-12\">\n" +
    "          <div class=\"row vertical-align\">\n" +
    "            <div class=\"col-xs-10 text-left\">\n" +
    "              <h5 class=\"facet-name\">{{ vm.facet.name }}</h5>\n" +
    "            </div>\n" +
    "            <div class=\"facet-enable-btn-container col-xs-2 text-right\">\n" +
    "              <button\n" +
    "                ng-disabled=\"vm.isLoading()\"\n" +
    "                ng-click=\"vm.enableFacet()\"\n" +
    "                class=\"facet-enable-btn btn btn-default btn-xs pull-right glyphicon glyphicon-plus\">\n" +
    "              </button>\n" +
    "            </div>\n" +
    "          </div>\n" +
    "        </div>\n" +
    "      </div>\n" +
    "    </div>\n" +
    "  </div>\n" +
    "</div>\n"
  );


  $templateCache.put('src/facets/text/facets.text-facet.directive.html',
    "<div class=\"facet-wrapper\">\n" +
    "  <span us-spinner=\"{radius:30, width:8, length: 40}\" ng-if=\"vm.isLoading()\"></span>\n" +
    "  <div class=\"facet\" ng-if=vm.isFacetEnabled()>\n" +
    "    <div class=\"well well-sm\">\n" +
    "      <div class=\"row\">\n" +
    "        <div class=\"col-xs-12 text-left\">\n" +
    "          <h5 class=\"facet-name pull-left\">{{ vm.facet.name }}</h5>\n" +
    "          <button\n" +
    "            ng-disabled=\"vm.isLoading()\"\n" +
    "            ng-click=\"vm.disableFacet()\"\n" +
    "            class=\"btn btn-danger btn-xs pull-right glyphicon glyphicon-remove\">\n" +
    "          </button>\n" +
    "        </div>\n" +
    "      </div>\n" +
    "      <div class=\"row\">\n" +
    "        <div class=\"col-xs-12 text-left\">\n" +
    "          <div class=\"facet-input-container\">\n" +
    "            <p class=\"input-group\">\n" +
    "            <input type=\"text\" class=\"form-control\"\n" +
    "            ng-change=\"vm.changed()\"\n" +
    "            ng-disabled=\"vm.isLoading()\"\n" +
    "            ng-model=\"vm.facet.selectedValue\"\n" +
    "            ng-model-options=\"{ debounce: 1000 }\">\n" +
    "            </input>\n" +
    "            <span class=\"input-group-btn\">\n" +
    "              <button type=\"button\" class=\"btn btn-default\"\n" +
    "                ng-disabled=\"vm.isDisabled()\"\n" +
    "                ng-click=\"vm.clear()\">\n" +
    "                <i class=\"glyphicon glyphicon-remove\"></i>\n" +
    "              </button>\n" +
    "            </span>\n" +
    "            </p>\n" +
    "          </div>\n" +
    "        </div>\n" +
    "      </div>\n" +
    "    </div>\n" +
    "  </div>\n" +
    "  <div class=\"facet\" ng-if=!vm.isFacetEnabled()>\n" +
    "    <div class=\"well well-sm\">\n" +
    "      <div class=\"row\">\n" +
    "        <div class=\"col-xs-12\">\n" +
    "          <div class=\"row vertical-align\">\n" +
    "            <div class=\"col-xs-10 text-left\">\n" +
    "              <h5 class=\"facet-name\">{{ vm.facet.name }}</h5>\n" +
    "            </div>\n" +
    "            <div class=\"facet-enable-btn-container col-xs-2 text-right\">\n" +
    "              <button\n" +
    "                ng-disabled=\"vm.isLoading()\"\n" +
    "                ng-click=\"vm.enableFacet(id)\"\n" +
    "                class=\"facet-enable-btn btn btn-default btn-xs pull-right glyphicon glyphicon-plus\">\n" +
    "              </button>\n" +
    "            </div>\n" +
    "          </div>\n" +
    "        </div>\n" +
    "      </div>\n" +
    "    </div>\n" +
    "  </div>\n" +
    "</div>\n"
  );


  $templateCache.put('src/facets/timespan/facets.timespan-facet.directive.html',
    "<div class=\"facet-wrapper\">\n" +
    "  <div class=\"facet\" ng-if=vm.facet.isEnabled()>\n" +
    "    <div class=\"well well-sm\">\n" +
    "      <div class=\"row\">\n" +
    "        <div class=\"col-xs-12 text-left\">\n" +
    "          <span spinner-key=\"vm.getSpinnerKey()\" spinner-start-active=\"true\"\n" +
    "            us-spinner=\"{radius:15, width:4, length: 20}\" ng-if=\"vm.isLoading()\"></span>\n" +
    "          <h5 class=\"facet-name pull-left\">{{ vm.facet.name }}</h5>\n" +
    "          <button\n" +
    "            ng-disabled=\"vm.isLoading()\"\n" +
    "            ng-click=\"vm.disableFacet()\"\n" +
    "            class=\"btn btn-danger btn-xs pull-right glyphicon glyphicon-remove\">\n" +
    "          </button>\n" +
    "        </div>\n" +
    "      </div>\n" +
    "      <div class=\"row no-gutter\">\n" +
    "        <div class=\"col-md-6 facet-date-left\">\n" +
    "          <span class=\"input-group\">\n" +
    "            <span class=\"input-group-btn\">\n" +
    "              <button type=\"button\" class=\"btn btn-default\"\n" +
    "                ng-disabled=\"vm.isLoading()\"\n" +
    "                ng-click=\"startDate.opened = !startDate.opened\">\n" +
    "                <i class=\"glyphicon glyphicon-calendar\"></i>\n" +
    "              </button>\n" +
    "            </span>\n" +
    "            <input type=\"text\" class=\"form-control\"\n" +
    "            uib-datepicker-popup=\"\"\n" +
    "            ng-readonly=\"true\"\n" +
    "            ng-change=\"vm.changed()\"\n" +
    "            ng-model=\"vm.facet.selectedValue.start\"\n" +
    "            is-open=\"startDate.opened\"\n" +
    "            show-button-bar=\"false\"\n" +
    "            datepicker-options=\"vm.facet.state.start\"\n" +
    "            close-text=\"Close\" />\n" +
    "          </span>\n" +
    "        </div>\n" +
    "        <div class=\"col-md-6 facet-date-right\">\n" +
    "          <span class=\"input-group\">\n" +
    "            <span class=\"input-group-btn\">\n" +
    "              <button type=\"button\" class=\"btn btn-default\"\n" +
    "                ng-disabled=\"vm.isLoading()\"\n" +
    "                ng-click=\"endDate.opened = !endDate.opened\">\n" +
    "                <i class=\"glyphicon glyphicon-calendar\"></i>\n" +
    "              </button>\n" +
    "            </span>\n" +
    "            <input type=\"text\" class=\"form-control\"\n" +
    "            uib-datepicker-popup=\"\"\n" +
    "            ng-readonly=\"true\"\n" +
    "            ng-change=\"vm.changed()\"\n" +
    "            ng-model=\"vm.facet.selectedValue.end\"\n" +
    "            is-open=\"endDate.opened\"\n" +
    "            show-button-bar=\"false\"\n" +
    "            datepicker-options=\"vm.facet.state.end\"\n" +
    "            close-text=\"Close\" />\n" +
    "          </span>\n" +
    "        </div>\n" +
    "      </div>\n" +
    "    </div>\n" +
    "  </div>\n" +
    "  <div class=\"facet\" ng-if=!vm.facet.isEnabled()>\n" +
    "    <div class=\"well well-sm\">\n" +
    "      <div class=\"row\">\n" +
    "        <div class=\"col-xs-12\">\n" +
    "          <div class=\"row vertical-align\">\n" +
    "            <div class=\"col-xs-10 text-left\">\n" +
    "              <h5 class=\"facet-name\">{{ vm.facet.name }}</h5>\n" +
    "            </div>\n" +
    "            <div class=\"facet-enable-btn-container col-xs-2 text-right\">\n" +
    "              <button\n" +
    "                ng-disabled=\"vm.isLoading()\"\n" +
    "                ng-click=\"vm.enableFacet()\"\n" +
    "                class=\"facet-enable-btn btn btn-default btn-xs pull-right glyphicon glyphicon-plus\">\n" +
    "              </button>\n" +
    "            </div>\n" +
    "          </div>\n" +
    "        </div>\n" +
    "      </div>\n" +
    "    </div>\n" +
    "  </div>\n" +
    "</div>\n"
  );


  $templateCache.put('src/facets/checkbox/facets.checkbox-facet.directive.html',
    "<div class=\"facet-wrapper\">\n" +
    "  <div class=\"facet\" ng-if=vm.facet.isEnabled()>\n" +
    "    <div class=\"well well-sm\">\n" +
    "      <div class=\"row\">\n" +
    "        <div class=\"col-xs-12 text-left\">\n" +
    "          <div class=\"alert alert-danger\" ng-if=\"vm.error\">{{ vm.error|limitTo:100 }}</div>\n" +
    "          <span spinner-key=\"vm.getSpinnerKey()\" spinner-start-active=\"true\"\n" +
    "            us-spinner=\"{radius:30, width:8, length: 40}\" ng-if=\"vm.isLoading()\"></span>\n" +
    "          <h5 class=\"facet-name pull-left\">{{ vm.facet.name }}</h5>\n" +
    "          <button\n" +
    "            ng-disabled=\"vm.isLoading()\"\n" +
    "            ng-click=\"vm.disableFacet()\"\n" +
    "            class=\"btn btn-danger btn-xs pull-right glyphicon glyphicon-remove\">\n" +
    "          </button>\n" +
    "        </div>\n" +
    "      </div>\n" +
    "      <div class=\"row\">\n" +
    "        <div class=\"col-xs-12 text-left\">\n" +
    "          <div class=\"facet-input-container\">\n" +
    "            <div class=\"checkbox\" ng-repeat=\"choice in vm.facet.getState()\">\n" +
    "            <label>\n" +
    "              <input type=\"checkbox\"\n" +
    "              checklist-change=\"vm.changed()\"\n" +
    "              ng-disabled=\"(vm.isLoading() || !choice.count)\"\n" +
    "              class=\"selector checkbox\"\n" +
    "              checklist-model=\"vm.facet.selectedValue.value\"\n" +
    "              checklist-value=\"choice.value\" />\n" +
    "              {{ choice.text }} ({{ choice.count }})\n" +
    "            </label>\n" +
    "            </div>\n" +
    "          </div>\n" +
    "        </div>\n" +
    "      </div>\n" +
    "    </div>\n" +
    "  </div>\n" +
    "  <div class=\"facet\" ng-if=!vm.facet.isEnabled()>\n" +
    "    <div class=\"well well-sm\">\n" +
    "      <div class=\"row\">\n" +
    "        <div class=\"col-xs-12\">\n" +
    "          <div class=\"row vertical-align\">\n" +
    "            <div class=\"col-xs-10 text-left\">\n" +
    "              <h5 class=\"facet-name\">{{ vm.facet.name }}</h5>\n" +
    "            </div>\n" +
    "            <div class=\"facet-enable-btn-container col-xs-2 text-right\">\n" +
    "              <button\n" +
    "                ng-disabled=\"vm.isLoading()\"\n" +
    "                ng-click=\"vm.enableFacet()\"\n" +
    "                class=\"facet-enable-btn btn btn-default btn-xs pull-right glyphicon glyphicon-plus\">\n" +
    "              </button>\n" +
    "            </div>\n" +
    "          </div>\n" +
    "        </div>\n" +
    "      </div>\n" +
    "    </div>\n" +
    "  </div>\n" +
    "</div>\n"
  );

}]);
