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();
77 foreach ($Suggestions as $ValueId => $ValueData)
79 # only display fields with non-empty value 80 $NewVal = $this->FilterFieldValue($Field, $ValueData[
"Name"]);
83 unset($Suggestions[$ValueId]);
87 $Suggestions[$ValueId][
"Name"] = $NewVal;
91 # foreach field, generate a list of suggested facets 92 # and determine if the field should be open by default 95 $Suggestions = $this->GenerateFacetsForTree($Field, $Suggestions);
99 $this->GenerateFacetsForName($Field, $Suggestions);
103 # Make sure that we've got a facet displayed for each selected search option 104 # (including those that were not suggested as facets) 106 # for each field in search parameters 107 foreach ($this->SearchParams->GetSearchStrings(TRUE)
108 as $FieldId => $Values)
110 # if the field does not exist, move to the next one 111 if (!$Schema->FieldExists($FieldId))
116 # if field is valid and viewable 117 $Field = $Schema->GetField($FieldId);
118 if (is_object($Field)
120 && $Field->UserCanView($User))
122 # if this was an old-format 'is under', translate it to the new format 125 $Values = $this->NormalizeTreeValues($Values);
128 # for each value for field 129 $FieldName = $Field->Name();
130 foreach ($Values as $Value)
132 # first filter field value 133 $NewVal = $this->FilterFieldValue($Field, $Value);
146 # if value is not already in list 147 if (!isset($this->DisplayedValues[$Field->Name()][$Value]))
149 # allow removing the current search terms 150 $this->FieldsOpenByDefault[$FieldName] = TRUE;
152 # if this was an 'is or begins with' condition, 154 if (preg_match(
'%\^(.+) -- ?$%', $Value, $Matches))
163 $RemovalLink = $this->RemoveTermFromSearchURL(
165 $this->TreeSuggestionsByFieldName[$FieldName][$Value]
168 "RemoveLink" => $RemovalLink);
171 # for Option and Controlled Name fields 175 # if this is not a "contains" search parameter 176 if ($Value[0] ==
"=")
178 # if value is not already in list 179 if (!isset($this->DisplayedValues[$Field->Name(
180 )][substr($Value, 1)]))
182 # note that this field should be open 183 $this->FieldsOpenByDefault[$FieldName] = TRUE;
185 # mark as a facet that can be removed 186 $RemovalLink = $this->RemoveTermFromSearchURL(
188 $this->SuggestionsByFieldName[$Field->Name()][] = array(
189 "Name" => substr($Value, 1),
190 "RemoveLink" => $RemovalLink);
198 # within each field, sort the suggestions alphabetically 199 foreach ($this->TreeSuggestionsByFieldName as &$Suggestion)
201 uasort($Suggestion, array($this,
"FacetSuggestionSortCallback"));
203 foreach ($this->SuggestionsByFieldName as &$Suggestion)
205 uasort($Suggestion, array($this,
"FacetSuggestionSortCallback"));
215 return $this->SuggestionsByFieldName;
224 return $this->TreeSuggestionsByFieldName;
233 return $this->FieldsOpenByDefault;
237 # ---- PRIVATE INTERFACE ------------------------------------------------- 240 private $DisplayedValues;
241 private $FieldsOpenByDefault;
242 private $MaxFacetsPerField;
243 private $SearchParams;
244 private $SuggestionsByFieldName;
245 private $TreeSuggestionsByFieldName;
253 private function FilterFieldValue($Field, $Value)
255 return $GLOBALS[
"AF"]->SignalEvent(
256 "EVENT_FIELD_DISPLAY_FILTER", array(
259 "Value" => $Value))[
"Value"];
267 private function GenerateFacetsForTree($Field, $Suggestions)
269 # if we already have a selection for this field, dig out the 270 # children of that selection and present those as options 271 $CurrentValues = $this->SearchParams->GetSearchStringsForField($Field);
273 $CurrentValues = $this->NormalizeTreeValues($CurrentValues);
275 if (count($CurrentValues) == 1)
277 $this->FieldsOpenByDefault[$Field->Name()] = TRUE;
278 $ParentValue = $CurrentValues[0];
280 if (preg_match(
'%\^(.+) -- ?$%', $ParentValue, $Matches))
282 $ParentValue = $Matches[1];
285 $FacetsForThisTree = $this->GenerateFacetsForSelectedTree(
286 $Field, $Suggestions, $ParentValue);
288 # if we have facets to display, but not too many of them 289 if ((count($FacetsForThisTree) > 0)
290 && (count($FacetsForThisTree) <= $this->MaxFacetsPerField))
292 # and append the required 'remove current' link 293 $RemovalLink = $this->RemoveTermFromSearchURL($Field, $CurrentValues);
294 $FacetsForThisTree[] = array(
295 "Name" => $ParentValue,
296 "RemoveLink" => $RemovalLink);
299 # mark each value for this field displayed 300 foreach ($CurrentValues as $Val)
302 $this->DisplayedValues[$Field->Name()][$Val] = TRUE;
305 $this->TreeSuggestionsByFieldName[$Field->Name()] =
306 array($FacetsForThisTree);
311 # otherwise, list the toplevel options for this field 312 $FacetsForThisTree = $this->GenerateFacetsForUnselectedTree(
313 $Field, $Suggestions);
315 # if we have facets to display, but not too many add the toplevel options 316 # to the suggestions for this field 317 if ((count($FacetsForThisTree) > 0)
318 && (count($FacetsForThisTree) <= $this->MaxFacetsPerField))
320 $this->TreeSuggestionsByFieldName[$Field->Name()] =
334 private function GenerateFacetsForSelectedTree($Field, $Suggestions, $ParentValue)
336 $ParentSegments = explode(
" -- ", $ParentValue);
338 # keep track of what has been shown 339 $DisplayedSegments = array();
341 $FacetsForThisTree = array();
342 # pull out those classifications which were explicitly assigned to something. 343 # those are the ones we can get counts for: 344 $FieldName = $Field->Name();
345 foreach ($Suggestions as $ValueId => $ValueData)
347 $ValueName = $ValueData[
"Name"];
348 $ValueCount = $ValueData[
"Count"];
349 $FieldSegments = explode(
" -- ", $ValueName);
351 # print any children of the current field value 352 # (determined based on a prefix match 353 if ( preg_match(
"/^".preg_quote($ParentValue.
" -- ",
"/").
"/", $ValueName)
354 && (count($FieldSegments) == (count($ParentSegments) + 1)))
356 # note that we've already displayed this segment because it was selected 357 # allowing us to avoid displaying it twice 358 $DisplayedSegments[$ValueName]=TRUE;
359 $LeafValue = array_pop($FieldSegments);
361 # add the modified search URL to our list of facets for this field 362 $AdditionLink = $this->AddTermToSearchURL($Field, $ValueName);
364 $FacetsForThisTree[] = array(
365 "Name" => $LeafValue,
366 "AddLink" => $AdditionLink,
367 "Count" => $ValueCount);
369 # record value as having been displayed 370 $this->DisplayedValues[$FieldName][$LeafValue] = TRUE;
374 # now, we'll need to iterate over the suggestions again and make sure that we've 375 # displayed proper suggestions even for fields where only the lowest level 376 # elements are displayed 377 foreach ($Suggestions as $ValueId => $ValueData)
379 $ValueName = $ValueData[
"Name"];
380 $ValueCount = $ValueData[
"Count"];
382 # get an array of the segments for this suggestion 383 $FieldSegments = explode(
" -- ", $ValueName);
385 # skip segments that are along some other branch of this tree 386 if (!preg_match(
"/^".preg_quote($ParentValue.
" -- ",
"/")
392 # skip segments that aren't (grand)* children of the current parent. 393 if (count($FieldSegments) < count($ParentSegments))
398 # truncate our array of segments to include only the 399 # level below our currently selected level 400 $TargetSegments = array_slice($FieldSegments, 0,
401 (count($ParentSegments) + 1));
403 # reassemble this into a Classification string 404 $TargetName = implode(
" -- ", $TargetSegments);
406 # if we haven't already displayed this string, either 407 # because it was in an earlier suggestion or was 408 # actually included in the search, add it to our list 410 if (!isset($DisplayedSegments[$TargetName]))
412 # as above, note that we've displayed this segment 413 # and add a modified search URL to our list of 415 $DisplayedSegments[$TargetName] = TRUE;
416 $LeafValue = array_pop($TargetSegments);
417 $AdditionLink = $this->AddTermToSearchURL($Field, $TargetName);
418 $FacetsForThisTree[] = array(
419 "Name" => $LeafValue,
420 "AddLink" => $AdditionLink);
422 # record value as having been displayed 423 $this->DisplayedValues[$FieldName][$LeafValue] = TRUE;
427 return $FacetsForThisTree;
437 private function GenerateFacetsForUnselectedTree($Field, $Suggestions)
439 $FacetsForThisTree = array();
441 # keep track of what has been already shown 442 $DisplayedSegments = array();
444 # first, pull out those which are assigned to some reasources and for which 445 # we can compute counts (those with the top-level directly assigned) 446 $FieldName = $Field->Name();
447 foreach ($Suggestions as $ValueId => $ValueData )
449 $ValueName = $ValueData[
"Name"];
450 $ValueCount = $ValueData[
"Count"];
451 $FieldSegments = explode(
" -- ", $ValueName);
453 # if this is a top level field 454 if (count($FieldSegments) == 1)
456 # add it to our list of facets for this tree 457 $DisplayedSegments[$ValueName] = TRUE;
458 $AdditionLink = $this->AddTermToSearchURL($Field, $ValueName);
459 $FacetsForThisTree[] = array(
460 "Name" => $ValueName,
461 "AddLink" => $AdditionLink,
462 "Count" => $ValueCount);
464 # record value as having been displayed 465 $this->DisplayedValues[$FieldName][$ValueName] = TRUE;
469 # scan through the suggestions again, and verify that 470 # we've at least offered the top-level 471 # option for each entry (necessary for cases where no 472 # resources in the results set have the top-level value, 473 # e.g. they all have Science -- Technology but none have 474 # Science. in this case we can't display counts, but 475 # should at least suggest Science as an option) 476 foreach ($Suggestions as $ValueId => $ValueData)
478 $ValueName = $ValueData[
"Name"];
479 $FieldSegments = explode(
" -- ", $ValueName);
481 # if none of our previous efforts have displayed this segment 482 if (!isset($DisplayedSegments[$FieldSegments[0]]))
485 $DisplayedSegments[$FieldSegments[0]] = TRUE;
486 $AdditionLink = $this->AddTermToSearchURL(
487 $Field, $FieldSegments[0]);
488 $FacetsForThisTree[] = array(
489 "Name" => $FieldSegments[0],
490 "AddLink" => $AdditionLink,
491 "Count" => $ValueData[
"Count"]);
493 # record value as having been displayed 494 $this->DisplayedValues[$FieldName][$FieldSegments[0]] = TRUE;
498 return $FacetsForThisTree;
507 private function GenerateFacetsForName($Field, $Suggestions)
509 # for option fields, bail when there are too many suggestions for this option 510 if (count($Suggestions) > $this->MaxFacetsPerField)
515 # retrieve current search parameter values for field 516 $CurrentValues = $this->SearchParams->GetSearchStringsForField($Field);
518 # if a field is required, and we have only one suggestion and 519 # there is no current value, then we don't want to display this 520 # facet because the search results will be identical 521 if (($Field->Optional() == FALSE)
522 && (count($Suggestions) == 1)
523 && !count($CurrentValues))
528 # for each suggested value 529 $FieldName = $Field->Name();
530 foreach ($Suggestions as $ValueId => $ValueData)
532 $ValueName = $ValueData[
"Name"];
534 # if we have a current value that is selected for this search 535 if (in_array(
"=".$ValueName, $CurrentValues))
537 # note that this facet should be displayed and add a 539 $this->FieldsOpenByDefault[$FieldName] = TRUE;
540 $RemovalLink = $this->RemoveTermFromSearchURL(
541 $Field,
"=".$ValueName);
542 $this->SuggestionsByFieldName[$FieldName][] = array(
543 "Name" => $ValueName,
544 "RemoveLink" => $RemovalLink,
545 "Count" => $ValueData[
"Count"]);
549 # otherwise, put an 'add' link in our suggestions, 550 # displaying counts if we can 551 $AdditionLink = $this->AddTermToSearchURL(
552 $Field,
"=".$ValueName);
553 $this->SuggestionsByFieldName[$FieldName][] = array(
554 "Name" => $ValueName,
555 "AddLink" => $AdditionLink,
556 "Count" => $ValueData[
"Count"]);
559 # record value as having been displayed 560 $this->DisplayedValues[$FieldName][$ValueName] = TRUE;
571 private function AddTermToSearchURL($Field, $Term)
573 # create our own copy of search parameters 574 $OurSearchParams = clone $this->SearchParams;
576 # if this is not a tree field type 579 # retrieve subgroups from search parameters 580 $Subgroups = $OurSearchParams->GetSubgroups();
582 # find subgroup for this field 583 foreach ($Subgroups as $Group)
585 if (in_array($Field->Id(), $Group->GetFields()))
593 if (isset($Subgroup))
595 # add term to subgroup 596 $Subgroup->AddParameter($Term, $Field);
600 # create new subgroup 603 # set logic for new subgroup 604 $Subgroup->Logic($Field->SearchGroupLogic());
606 # add term to subgroup 607 $Subgroup->AddParameter($Term, $Field);
609 # add subgroup to search parameters 610 $OurSearchParams->AddSet($Subgroup);
615 # add specified term to search parameters 616 $OurSearchParams->RemoveParameter(NULL, $Field);
617 $OurSearchParams->AddParameter(
618 "^".$Term.
" --", $Field);
621 # build new URL with revised search parameters 622 $Url = implode(
"&", array_filter(array(
623 $this->BaseLink, $OurSearchParams->UrlParameterString())));
625 # return new URL to caller 636 private function RemoveTermFromSearchURL($Field, $Terms)
638 # create our own copy of search parameters with specified parameter removed 639 $OurSearchParams = clone $this->SearchParams;
643 # pull out the old setting 644 $OldSetting = $OurSearchParams->GetSearchStringsForField($Field);
645 $OurSearchParams->RemoveParameter($Terms, $Field);
647 # if we had an 'is or is a child of', move up one level 648 if (count($OldSetting) == 1 &&
649 preg_match(
'%^\^(.*) -- ?$%', $OldSetting[0], $Match))
651 # explode out the segments 652 $FieldSegments = explode(
" -- ", $Match[1]);
654 # remove the last one 655 array_pop($FieldSegments);
657 # if we have any segments left 658 if (count($FieldSegments))
660 # construct revised setting 661 $NewSetting = implode(
" -- ", $FieldSegments);
662 $OurSearchParams->AddParameter(
663 "^".$NewSetting.
" --", $Field);
669 $OurSearchParams->RemoveParameter($Terms, $Field);
672 # build new URL with revised search parameters 673 $Url = implode(
"&", array_filter(array(
674 $this->BaseLink, $OurSearchParams->UrlParameterString())));
676 # return new URL to caller 686 private function FacetSuggestionSortCallback($A, $B)
688 if (isset($A[
"Name"]))
690 return strcmp($A[
"Name"], $B[
"Name"]);
696 return ($CountA == $CountB) ? 0
697 : (($CountA < $CountB) ? -1 : 1);
707 private function NormalizeTreeValues($Values)
709 if (count($Values) == 2 &&
710 preg_match(
'/^=(.*)$/', $Values[0], $FirstMatch) &&
711 preg_match(
'/^\\^(.*) -- $/', $Values[1], $SecondMatch) &&
712 $FirstMatch[1] == $SecondMatch[1] )
714 $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...