Skip to content

Commit

Permalink
Facet / Add hierarchical facet support.
Browse files Browse the repository at this point in the history
Hierarchical facet support was added in geonetwork#680
providing server code to handle facet hierarchy based on thesaurus broader relations.

This commit add the client side support for it, which means:
* add directive to handle facet response using the dimension format ie.
 * example for flat facet
```
<summary count="3949" type="local">
  <dimension name="type" label="types">
    <category value="dataset" label="Dataset" count="3043"/>
    <category value="series" label="Series" count="408"/>
...
```
 * example for hierachical facet
```
<category value="http://vocab.nerc.ac.uk/collection/C19/current/SVX00025/" label="World" count="110">
  <category value="http://vocab.nerc.ac.uk/collection/C19/current/SVX00005/" label="Atlantic Ocean" count="13">
    <category value="http://vocab.nerc.ac.uk/collection/C19/current/1/" label="North Atlantic Ocean" count="13">
      <category value="http://vocab.nerc.ac.uk/collection/C19/current/SVX00015/" label="Northeast Atlantic Ocean (40W)" count="13">
        <category value="http://vocab.nerc.ac.uk/collection/C19/current/1_7/" label="English Channel" count="12"/>
        <category value="http://vocab.nerc.ac.uk/collection/C19/current/1_8/" label="Bay of Biscay" count="3"/>
....
```
* old facet format is still supported but it sounds relevant to deprecate it and progressively move to the dimension format. Search and Editor board app are migrated to the dimension format.
* add example to know how to configure hierarchical facet using the regions thesaurus provided by GeoNetwork or the GEMET thesaurus.
  • Loading branch information
François Prunayre committed Aug 6, 2015
1 parent a896987 commit 0326576
Show file tree
Hide file tree
Showing 23 changed files with 588 additions and 54 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -697,13 +697,13 @@
isWorkflowEnabled: function() {
var st = this.mdStatus;
var res = st &&
//Status is unknown
//Status is unknown
(!isNaN(st) && st != '0');

//What if it is an array: gmd:MD_ProgressCode
if(!res && Array.isArray(st)) {
if (!res && Array.isArray(st)) {
angular.forEach(st, function(s) {
if(!isNaN(s) && s != '0') {
if (!isNaN(s) && s != '0') {
res = true;
}
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@
proj.getWorldExtent(),
bbox.extent) ?
ol.proj.transformExtent(bbox.extent,
bbox.crs || 'EPSG:4326', proj) :
bbox.crs || 'EPSG:4326', proj) :
proj.getExtent();
break;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
(function() {
goog.provide('gn_facets_dimension_directive');
goog.require('gn_utility_service');

var module = angular.module('gn_facets_dimension_directive',
['gn_utility_service']);

module.directive('gnFacetDimensionList', [
'gnFacetConfigService', '$timeout',
function(gnFacetConfigService, $timeout) {
return {
restrict: 'A',
templateUrl: '../../catalog/components/search/facets/' +
'partials/dimension-facet-list.html',
scope: {
dimension: '=gnFacetDimensionList',
facetType: '=',
params: '='
},
link: function(scope, element) {
scope.facetQuery = scope.params['facet.q'];
scope.facetConfig = null;

scope.collapseAll = function() {
$timeout(function() {
element.parent().find('div.gn-facet > h4').click();
});
};

// Facet is collapsed if not in current search criteria
scope.isFacetsCollapse = function(facetKey) {
return !angular.isDefined(scope.params[facetKey]);
};

// Load facet configuration to know which index field
// correspond to which dimension.
gnFacetConfigService.loadConfig(scope.facetType).
then(function(data) {
scope.facetConfig = {
config: data,
map:  {}
};

angular.forEach(scope.facetConfig.config, function(key) {
scope.facetConfig.map[key.label] = key.name;
});
});
}
};
}]);

module.directive('gnFacetDimensionCategory', [
'gnFacetConfigService', 'RecursionHelper', '$parse',
function(gnFacetConfigService, RecursionHelper, $parse) {
return {
restrict: 'A',
templateUrl: '../../catalog/components/search/facets/' +
'partials/dimension-facet-category.html',
scope: {
category: '=gnFacetDimensionCategory',
categoryKey: '=',
path: '=',
params: '=',
facetConfig: '='
},
compile: function(element) {
// Use the compile function from the RecursionHelper,
// And return the linking function(s) which it returns
return RecursionHelper.compile(element,
function(scope, element, attrs) {
var initialMaxItems = 5;
scope.initialMaxItems = initialMaxItems;
scope.maxItems = initialMaxItems;
scope.toggleAll = function() {
scope.maxItems = (scope.maxItems == Infinity) ?
initialMaxItems : Infinity;
};

// Facet drill down is based on facet.q parameter.
// The facet.q parameter contains a list of comma separated
// dimensions
// <dimension_name>{"/"<category_value>}
// Note that drill down paths use '/' as the separator
// between categories in the path, so embedded '/' characters
// in categories should be escaped using %2F or alternatively,
// each category in the path url encoded in addition to
// normal parameter encoding.
//
// Multiple drill down queries can be specified by providing
// multiple facet.q parameters or by combining drill down
// queries in one facet.q parameter using '&'
// appropriately encoded.
//
// http://localhost:8080/geonetwork/srv/fre/q?
// resultType=hierarchy&
// facet.q=gemetKeyword/http%253A%252F%252Fwww.eionet.europa.eu
// %252Fgemet%252Fconcept%252F2643
// %2F
// http%253A%252F%252Fwww.eionet.europa.eu
// %252Fgemet%252Fconcept%252F2641

/**
* Build the drill down path based on current category value
* and its parent.
* @param {Object} category
* @return {boolean|*}
*/
scope.buildPath = function(category) {
category.path =
(scope.path === undefined ? '' : scope.path + '/') +
encodeURIComponent(category['@value']);
return category.path;
};


/**
* Build the facet.q paramaeter
*/
scope.filter = function(category, $event) {
if (!scope.facetConfig) {
return; // Facet configuration not yet loaded.
}

var checked = $event.currentTarget.checked;


// Extract facet.q info
if (angular.isUndefined(scope.params['facet.q'])) {
scope.params['facet.q'] = '';
}
var facetQParam = scope.params['facet.q'];
var dimensionList =
facetQParam.split('&');
var categoryList = [];
$.each(dimensionList, function(idx) {
// Dimension filter contains the dimension name first
// and then the drilldown path. User may uncheck
// an element in the middle of the path. In such case
// only activate the parent node.
var dimensionFilter = dimensionList[idx].split('/');

// Dimension but not in that category path. Add filter.
if (dimensionFilter[1] &&
dimensionFilter[0] !=
scope.facetConfig.map[scope.categoryKey]) {
categoryList.push({
dimension: dimensionFilter[0],
value: dimensionFilter.slice(1, dimensionFilter.length)
});
} else if (dimensionFilter[1] &&
dimensionFilter.length > 2 &&
dimensionFilter[0] ==
scope.facetConfig.map[scope.categoryKey]) {

var filteredElementInPath =
$.inArray(
encodeURIComponent(category['@value']),
dimensionFilter);
// Restrict the path to its parent
if (filteredElementInPath !== -1) {
categoryList.push({
dimension: scope.categoryKey,
value: dimensionFilter.
slice(1, filteredElementInPath).
join('/')
});
}
}
});
// Add or remove new category
if (checked) {
categoryList.push({
dimension: scope.categoryKey,
value: category.path
});
} else {

}

// Build facet.q
facetQParam = '';
$.each(categoryList, function(idx) {
if (categoryList[idx].value) {
facetQParam = facetQParam +
(scope.facetConfig.map[categoryList[idx].dimension] ||
categoryList[idx].dimension) +
'/' +
categoryList[idx].value +
(idx < categoryList.length - 1 ? '&' : '');
}
});
scope.params['facet.q'] = facetQParam;

scope.$emit('resetSearch', scope.params);
$event.preventDefault();
};


/**
* Check that current category is already used
* in current filter.
* @param {Object} category
* @return {boolean|*}
*/
scope.isOnDrillDownPath = function(category) {
// Is selected if the category value is defined in the
// facet.q parameter (ie. combination of
// dim1/dim1val/dim1val2&dim2/dim2Val...).
category.isSelected =
angular.isUndefined(scope.params['facet.q']) ?
false :
$.inArray(
encodeURIComponent(category['@value']),
scope.params['facet.q'].split(/&|\//)) !== -1;
return category.isSelected;
};

scope.isInFilter = function(category) {
if (!scope.facetConfig) {
return false;
// Facet configuration not yet loaded
}

var facetQParam = scope.params['facet.q'];
if (facetQParam === undefined) {
return false;
}
var dimensionList =
facetQParam.split('&');
var categoryList = [];
for (var i = 0; i < dimensionList.length; i++) {
var dimensionFilter = dimensionList[i].split('/');
if (dimensionFilter[0] ==
scope.facetConfig.map[scope.categoryKey] &&
$.inArray(
encodeURIComponent(category['@value']),
dimensionFilter) !== -1) {
return true;
}
}
return false;
};

scope.toggleNode = function(evt) {
el = evt ?
$(evt.currentTarget).parent() :
element.find('span.fa');
el.find('.fa').first()
.toggleClass('fa-minus-square')
.toggleClass('fa-plus-square');
el.children('div').toggleClass('hidden');
!evt || evt.preventDefault();
return false;
};
});
}
};
}]);
})();
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
var initialMaxItems = 5;

// Facet is collapsed if not in current search criteria
function isFacetsCollapse (facetKey) {
function isFacetsCollapse(facetKey) {
return !(scope.params && angular.isDefined(scope.params[facetKey]));
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,18 @@







goog.require('gn_facets_config_service');
goog.require('gn_facets_dimension_directive');
goog.require('gn_facets_directive');
goog.require('gn_facets_service');

angular.module('gn_facets', [
'gn_facets_directive',
'gn_facets_dimension_directive',
'gn_facets_config_service',
'gn_facets_service'
]);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<ul class="list-group">
<li data-ng-repeat="c in category"
data-ng-init="path = buildPath(c); isSelected = isOnDrillDownPath(c)"
data-ng-if="$index < maxItems"
class="list-group-item">
<span data-ng-click="toggleNode($event)"
class="fa fa-fw gn-facet-toggle"
data-ng-class="c.category?(isSelected?'fa-minus-square':'fa-plus-square'):'invisible'" ></span>
<label title="{{c['@label'] || c['@value']}}"
data-ng-class="{'gn-facet-active': isSelected}">
<input type="checkbox"
data-ng-checked="isInFilter(c)"
data-ng-click="filter(c, $event)"/>
{{(c['@label'] || c['@value']) | characters:30}}&nbsp;
<span class="gn-facet-count">({{c['@count']}})</span>
</label>
<div data-gn-facet-dimension-category="c.category"
data-category-key="categoryKey"
data-facet-config="facetConfig"
data-path="path"
data-params="params"
data-ng-class="isSelected ? '' : 'hidden'"></div>
</li>
<li class="gn-facet-moreorless"
data-ng-if="category.length - initialMaxItems > 0">
<button data-ng-click="toggleAll()"
class="btn btn-link">
{{category.length - initialMaxItems}}&nbsp;
{{(category.length >= maxItems) ? ('more' | translate) : ('less' | translate)}}
</button>
</li>
</ul>
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<div class="gn-facet-list">
<div data-ng-repeat="f in dimension"
class="gn-facet"
data-ng-show="f.category">
<a class="gn-facet-collapseall"
data-ng-if="$index === 0"
data-ng-click="collapseAll()"
title="{{'collapseAllFacet' | translate}}">
<i class="fa fa-fw fa-angle-double-left"></i>
</a>

<h4 data-gn-collapse="{{collapsed}}">
<i class="fa fa-fw" data-ng-class="collapsed ? 'fa-angle-left' : ''"></i>
{{f['@label'] | translate}}
</h4>
<div data-gn-facet-dimension-category="f.category"
data-category-key="f['@label']"
data-params="params"
data-facet-config="facetConfig"
data-ng-class="{'collapse': collapsed}">
</div>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
<div class="gn-facet" data-ng-show="facetResults[facet].length > 1">
<h5 data-gn-collapse="{{collapsed}}">
<span class="fa fa-arrow-circle-o-right"
data-ng-class="{'fa-rotate-90': !collapsed}"></span>
<i class="fa" data-ng-class="collapsed ? 'fa-angle-left' : ''"></i>
{{facet | translate}}
</h5>
<div class="list-group"
Expand Down
Loading

0 comments on commit 0326576

Please sign in to comment.