3 # FILE: SearchFacetUI.php 5 # Part of the Collection Workflow Integration System (CWIS) 6 # Copyright 2015 Edward Almasy and Internet Scout Research Group 7 # http://scout.wisc.edu/cwis/ 18 # ---- PUBLIC INTERFACE -------------------------------------------------- 32 public function __construct($BaseLink, $SearchParams, $SearchResults,
33 $User, $MaxFacetsPerField, $SchemaId)
35 # save base URL, search parameters, and max facets for later use 36 $this->BaseLink = $BaseLink;
37 $this->SearchParams = $SearchParams;
38 $this->MaxFacetsPerField = $MaxFacetsPerField;
40 # facets which should be displayed because they were explicitly 41 # included in the search terms 42 $this->FieldsOpenByDefault = array();
44 # suggestions for Option and ControlledName fields 45 # array( FieldName => array(ModifiedSearchUrls) 46 $this->SuggestionsByFieldName = array();
48 # suggestions for Tree fields 49 # when nothing is selected, 50 # array( FieldName => array(ModifiedSearchUrls)) {exactly as above} 51 # when something is selected and we should display its children 52 # array( FieldName => array( Selection => array(ModifiedSearchURLs))) 53 # where the last Modified URL is always a link to remove the 55 $this->TreeSuggestionsByFieldName = array();
57 # iterrate over the suggested result facets, bulding up a list of 58 # those we care to display. this is necessary because GetResultFacets 59 # just gives back a list of all Tree, Option, and ControlledNames that 60 # occur in the result set. For Trees in particular, some processing 61 # is necessary as we don't want to just display a value six layers deep 62 # when our current search has selected something at the second layer of 66 foreach ($ResultFacets as $FieldName => $Suggestions)
68 if (!$Schema->FieldExists($FieldName))
73 $Field = $Schema->GetField($FieldName);
74 $FieldId = $Field->Id();
76 # foreach field, generate a list of suggested facets 77 # and determine if the field should be open by default 80 $Suggestions = $this->GenerateFacetsForTree($Field, $Suggestions);
84 $this->GenerateFacetsForName($Field, $Suggestions);
88 # Make sure that we've got a facet displayed for each selected search option 89 # (including those that were not suggested as facets) 91 # for each field in search parameters 92 foreach ($this->SearchParams->GetSearchStrings(TRUE)
93 as $FieldId => $Values)
95 # if the field does not exist, move to the next one 96 if (!$Schema->FieldExists($FieldId))
101 # if field is valid and viewable 102 $Field = $Schema->GetField($FieldId);
103 if (is_object($Field)
105 && $Field->UserCanView($User))
107 # if this was an old-format 'is under', translate it to the new format 110 $Values = $this->NormalizeTreeValues($Values);
113 # for each value for field 114 $FieldName = $Field->Name();
115 foreach ($Values as $Value)
120 # if value is not already in list 121 if (!isset($this->DisplayedValues[$Field->Name()][$Value]))
123 # allow removing the current search terms 124 $this->FieldsOpenByDefault[$FieldName] = TRUE;
126 # if this was an 'is or begins with' condition, 128 if (preg_match(
'%\^(.+) -- ?$%', $Value, $Matches))
137 $RemovalLink = $this->RemoveTermFromSearchURL(
139 $this->TreeSuggestionsByFieldName[$FieldName][$Value]
142 "RemoveLink" => $RemovalLink);
145 # for Option and Controlled Name fields 149 # if this is not a "contains" search parameter 150 if ($Value[0] ==
"=")
152 # if value is not already in list 153 if (!isset($this->DisplayedValues[$Field->Name(
154 )][substr($Value, 1)]))
156 # note that this field should be open 157 $this->FieldsOpenByDefault[$FieldName] = TRUE;
159 # mark as a facet that can be removed 160 $RemovalLink = $this->RemoveTermFromSearchURL(
162 $this->SuggestionsByFieldName[$Field->Name()][] = array(
163 "Name" => substr($Value, 1),
164 "RemoveLink" => $RemovalLink);
172 # within each field, sort the suggestions alphabetically 173 foreach ($this->TreeSuggestionsByFieldName as &$Suggestion)
175 uasort($Suggestion, array($this,
"FacetSuggestionSortCallback"));
177 foreach ($this->SuggestionsByFieldName as &$Suggestion)
179 uasort($Suggestion, array($this,
"FacetSuggestionSortCallback"));
189 return $this->SuggestionsByFieldName;
198 return $this->TreeSuggestionsByFieldName;
207 return $this->FieldsOpenByDefault;
211 # ---- PRIVATE INTERFACE ------------------------------------------------- 214 private $DisplayedValues;
215 private $FieldsOpenByDefault;
216 private $MaxFacetsPerField;
217 private $SearchParams;
218 private $SuggestionsByFieldName;
219 private $TreeSuggestionsByFieldName;
226 private function GenerateFacetsForTree($Field, $Suggestions)
228 # if we already have a selection for this field, dig out the 229 # children of that selection and present those as options 230 $CurrentValues = $this->SearchParams->GetSearchStringsForField($Field);
232 $CurrentValues = $this->NormalizeTreeValues($CurrentValues);
234 if (count($CurrentValues) == 1)
236 $this->FieldsOpenByDefault[$Field->Name()] = TRUE;
237 $ParentValue = $CurrentValues[0];
239 if (preg_match(
'%\^(.+) -- ?$%', $ParentValue, $Matches))
241 $ParentValue = $Matches[1];
244 $FacetsForThisTree = $this->GenerateFacetsForSelectedTree(
245 $Field, $Suggestions, $ParentValue);
247 # if we have facets to display, but not too many of them 248 if ((count($FacetsForThisTree) > 0)
249 && (count($FacetsForThisTree) <= $this->MaxFacetsPerField))
251 # and append the required 'remove current' link 252 $RemovalLink = $this->RemoveTermFromSearchURL($Field, $CurrentValues);
253 $FacetsForThisTree[] = array(
254 "Name" => $ParentValue,
255 "RemoveLink" => $RemovalLink);
258 # mark each value for this field displayed 259 foreach ($CurrentValues as $Val)
261 $this->DisplayedValues[$Field->Name()][$Val] = TRUE;
264 $this->TreeSuggestionsByFieldName[$Field->Name()] =
265 array($FacetsForThisTree);
270 # otherwise, list the toplevel options for this field 271 $FacetsForThisTree = $this->GenerateFacetsForUnselectedTree(
272 $Field, $Suggestions);
274 # if we have facets to display, but not too many add the toplevel options 275 # to the suggestions for this field 276 if ((count($FacetsForThisTree) > 0)
277 && (count($FacetsForThisTree) <= $this->MaxFacetsPerField))
279 $this->TreeSuggestionsByFieldName[$Field->Name()] =
293 private function GenerateFacetsForSelectedTree($Field, $Suggestions, $ParentValue)
295 $ParentSegments = explode(
" -- ", $ParentValue);
297 # keep track of what has been shown 298 $DisplayedSegments = array();
300 $FacetsForThisTree = array();
301 # pull out those classifications which were explicitly assigned to something. 302 # those are the ones we can get counts for: 303 $FieldName = $Field->Name();
304 foreach ($Suggestions as $ValueId => $ValueData)
306 $ValueName = $ValueData[
"Name"];
307 $ValueCount = $ValueData[
"Count"];
308 $FieldSegments = explode(
" -- ", $ValueName);
310 # print any children of the current field value 311 # (determined based on a prefix match 312 if ( preg_match(
"/^".preg_quote($ParentValue.
" -- ",
"/").
"/", $ValueName)
313 && (count($FieldSegments) == (count($ParentSegments) + 1)))
315 # note that we've already displayed this segment because it was selected 316 # allowing us to avoid displaying it twice 317 $DisplayedSegments[$ValueName]=TRUE;
318 $LeafValue = array_pop($FieldSegments);
320 # add the modified search URL to our list of facets for this field 321 $AdditionLink = $this->AddTermToSearchURL($Field, $ValueName);
323 $FacetsForThisTree[] = array(
324 "Name" => $LeafValue,
325 "AddLink" => $AdditionLink,
326 "Count" => $ValueCount);
328 # record value as having been displayed 329 $this->DisplayedValues[$FieldName][$LeafValue] = TRUE;
333 # now, we'll need to iterate over the suggestions again and make sure that we've 334 # displayed proper suggestions even for fields where only the lowest level 335 # elements are displayed 336 foreach ($Suggestions as $ValueId => $ValueData)
338 $ValueName = $ValueData[
"Name"];
339 $ValueCount = $ValueData[
"Count"];
341 # get an array of the segments for this suggestion 342 $FieldSegments = explode(
" -- ", $ValueName);
344 # skip segments that are along some other branch of this tree 345 if (!preg_match(
"/^".preg_quote($ParentValue.
" -- ",
"/")
351 # skip segments that aren't (grand)* children of the current parent. 352 if (count($FieldSegments) < count($ParentSegments))
357 # truncate our array of segments to include only the 358 # level below our currently selected level 359 $TargetSegments = array_slice($FieldSegments, 0,
360 (count($ParentSegments) + 1));
362 # reassemble this into a Classification string 363 $TargetName = implode(
" -- ", $TargetSegments);
365 # if we haven't already displayed this string, either 366 # because it was in an earlier suggestion or was 367 # actually included in the search, add it to our list 369 if (!isset($DisplayedSegments[$TargetName]))
371 # as above, note that we've displayed this segment 372 # and add a modified search URL to our list of 374 $DisplayedSegments[$TargetName] = TRUE;
375 $LeafValue = array_pop($TargetSegments);
376 $AdditionLink = $this->AddTermToSearchURL($Field, $TargetName);
377 $FacetsForThisTree[] = array(
378 "Name" => $LeafValue,
379 "AddLink" => $AdditionLink);
381 # record value as having been displayed 382 $this->DisplayedValues[$FieldName][$LeafValue] = TRUE;
386 return $FacetsForThisTree;
396 private function GenerateFacetsForUnselectedTree($Field, $Suggestions)
398 $FacetsForThisTree = array();
400 # keep track of what has been already shown 401 $DisplayedSegments = array();
403 # first, pull out those which are assigned to some reasources and for which 404 # we can compute counts (those with the top-level directly assigned) 405 $FieldName = $Field->Name();
406 foreach ($Suggestions as $ValueId => $ValueData )
408 $ValueName = $ValueData[
"Name"];
409 $ValueCount = $ValueData[
"Count"];
410 $FieldSegments = explode(
" -- ", $ValueName);
412 # if this is a top level field 413 if (count($FieldSegments) == 1)
415 # add it to our list of facets for this tree 416 $DisplayedSegments[$ValueName] = TRUE;
417 $AdditionLink = $this->AddTermToSearchURL($Field, $ValueName);
418 $FacetsForThisTree[] = array(
419 "Name" => $ValueName,
420 "AddLink" => $AdditionLink,
421 "Count" => $ValueCount);
423 # record value as having been displayed 424 $this->DisplayedValues[$FieldName][$ValueName] = TRUE;
428 # scan through the suggestions again, and verify that 429 # we've at least offered the top-level 430 # option for each entry (necessary for cases where no 431 # resources in the results set have the top-level value, 432 # e.g. they all have Science -- Technology but none have 433 # Science. in this case we can't display counts, but 434 # should at least suggest Science as an option) 435 foreach ($Suggestions as $ValueId => $ValueData)
437 $ValueName = $ValueData[
"Name"];
438 $FieldSegments = explode(
" -- ", $ValueName);
440 # if none of our previous efforts have displayed this segment 441 if (!isset($DisplayedSegments[$FieldSegments[0]]))
444 $DisplayedSegments[$FieldSegments[0]] = TRUE;
445 $AdditionLink = $this->AddTermToSearchURL(
446 $Field, $FieldSegments[0]);
447 $FacetsForThisTree[] = array(
448 "Name" => $FieldSegments[0],
449 "AddLink" => $AdditionLink,
450 "Count" => $ValueData[
"Count"]);
452 # record value as having been displayed 453 $this->DisplayedValues[$FieldName][$FieldSegments[0]] = TRUE;
457 return $FacetsForThisTree;
466 private function GenerateFacetsForName($Field, $Suggestions)
468 # for option fields, bail when there are too many suggestions for this option 469 if (count($Suggestions) > $this->MaxFacetsPerField)
474 # retrieve current search parameter values for field 475 $CurrentValues = $this->SearchParams->GetSearchStringsForField($Field);
477 # if a field is required, and we have only one suggestion and 478 # there is no current value, then we don't want to display this 479 # facet because the search results will be identical 480 if (($Field->Optional() == FALSE)
481 && (count($Suggestions) == 1)
482 && !count($CurrentValues))
487 # for each suggested value 488 $FieldName = $Field->Name();
489 foreach ($Suggestions as $ValueId => $ValueData)
491 $ValueName = $ValueData[
"Name"];
493 # if we have a current value that is selected for this search 494 if (in_array(
"=".$ValueName, $CurrentValues))
496 # note that this facet should be displayed and add a 498 $this->FieldsOpenByDefault[$FieldName] = TRUE;
499 $RemovalLink = $this->RemoveTermFromSearchURL(
500 $Field,
"=".$ValueName);
501 $this->SuggestionsByFieldName[$FieldName][] = array(
502 "Name" => $ValueName,
503 "RemoveLink" => $RemovalLink,
504 "Count" => $ValueData[
"Count"]);
508 # otherwise, put an 'add' link in our suggestions, 509 # displaying counts if we can 510 $AdditionLink = $this->AddTermToSearchURL(
511 $Field,
"=".$ValueName);
512 $this->SuggestionsByFieldName[$FieldName][] = array(
513 "Name" => $ValueName,
514 "AddLink" => $AdditionLink,
515 "Count" => $ValueData[
"Count"]);
518 # record value as having been displayed 519 $this->DisplayedValues[$FieldName][$ValueName] = TRUE;
530 private function AddTermToSearchURL($Field, $Term)
532 # create our own copy of search parameters 533 $OurSearchParams = clone $this->SearchParams;
535 # if this is not a tree field type 538 # retrieve subgroups from search parameters 539 $Subgroups = $OurSearchParams->GetSubgroups();
541 # find subgroup for this field 542 foreach ($Subgroups as $Group)
544 if (in_array($Field->Id(), $Group->GetFields()))
552 if (isset($Subgroup))
554 # add term to subgroup 555 $Subgroup->AddParameter($Term, $Field);
559 # create new subgroup 562 # set logic for new subgroup 563 $Subgroup->Logic($Field->SearchGroupLogic());
565 # add term to subgroup 566 $Subgroup->AddParameter($Term, $Field);
568 # add subgroup to search parameters 569 $OurSearchParams->AddSet($Subgroup);
574 # add specified term to search parameters 575 $OurSearchParams->RemoveParameter(NULL, $Field);
576 $OurSearchParams->AddParameter(
577 "^".$Term.
" --", $Field);
580 # build new URL with revised search parameters 581 $Url = implode(
"&", array_filter(array(
582 $this->BaseLink, $OurSearchParams->UrlParameterString())));
584 # return new URL to caller 595 private function RemoveTermFromSearchURL($Field, $Terms)
597 # create our own copy of search parameters with specified parameter removed 598 $OurSearchParams = clone $this->SearchParams;
602 # pull out the old setting 603 $OldSetting = $OurSearchParams->GetSearchStringsForField($Field);
604 $OurSearchParams->RemoveParameter($Terms, $Field);
606 # if we had an 'is or is a child of', move up one level 607 if (count($OldSetting) == 1 &&
608 preg_match(
'%^\^(.*) -- ?$%', $OldSetting[0], $Match))
610 # explode out the segments 611 $FieldSegments = explode(
" -- ", $Match[1]);
613 # remove the last one 614 array_pop($FieldSegments);
616 # if we have any segments left 617 if (count($FieldSegments))
619 # construct revised setting 620 $NewSetting = implode(
" -- ", $FieldSegments);
621 $OurSearchParams->AddParameter(
622 "^".$NewSetting.
" --", $Field);
628 $OurSearchParams->RemoveParameter($Terms, $Field);
631 # build new URL with revised search parameters 632 $Url = implode(
"&", array_filter(array(
633 $this->BaseLink, $OurSearchParams->UrlParameterString())));
635 # return new URL to caller 645 private function FacetSuggestionSortCallback($A, $B)
647 if (isset($A[
"Name"]))
649 return strcmp($A[
"Name"], $B[
"Name"]);
655 return ($CountA == $CountB) ? 0
656 : (($CountA < $CountB) ? -1 : 1);
666 private function NormalizeTreeValues($Values)
668 if (count($Values) == 2 &&
669 preg_match(
'/^=(.*)$/', $Values[0], $FirstMatch) &&
670 preg_match(
'/^\\^(.*) -- $/', $Values[1], $SecondMatch) &&
671 $FirstMatch[1] == $SecondMatch[1] )
673 $Values = [
"^".$FirstMatch[1].
" --"];
GetFieldsOpenByDefault()
Retrieve which fields should be initially open in facet UI.
Set of parameters used to perform a search.
GetSuggestionsByFieldName()
Retrieve facet UI data for non-tree metadata fields.
__construct($BaseLink, $SearchParams, $SearchResults, $User, $MaxFacetsPerField, $SchemaId)
Constructor, that accepts the search parameters and results and prepares the facet UI data to be retr...
GetTreeSuggestionsByFieldName()
Retrieve facet UI data for tree metadata fields.
static GetResultFacets($SearchResults, $User)
Generate a list of suggested additional search terms that can be used for faceted searching...
SearchFacetUI supports the generation of a user interface for faceted search, by taking the search pa...