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 $TreeFieldsToCheck = array();
92 # for each field in search parameters 93 foreach ($this->SearchParams->GetSearchStrings(TRUE)
94 as $FieldId => $Values)
96 # if the field does not exist, move to the next one 97 if (!$Schema->FieldExists($FieldId))
102 # if field is valid and viewable 103 $Field = $Schema->GetField($FieldId);
104 if (is_object($Field)
106 && $Field->UserCanView($User))
108 # for each value for field 109 $FieldName = $Field->Name();
110 foreach ($Values as $Value)
115 # if value is not already in list 116 if (!isset($this->DisplayedValues[$Field->Name()][$Value]))
118 $TreeFieldsToCheck[$Field->Name()][]= $Value;
119 # allow removing the current search terms 120 $this->FieldsOpenByDefault[$FieldName] = TRUE;
121 $RemovalLink = $this->RemoveTermFromSearchURL(
123 $this->TreeSuggestionsByFieldName[$FieldName][$Value][]
126 "RemoveLink" => $RemovalLink);
129 # for Option and Controlled Name fields 133 # if this is not a "contains" search parameter 134 if ($Value[0] ==
"=")
136 # if value is not already in list 137 if (!isset($this->DisplayedValues[$Field->Name(
138 )][substr($Value, 1)]))
140 # note that this field should be open 141 $this->FieldsOpenByDefault[$FieldName] = TRUE;
143 # mark as a facet that can be removed 144 $RemovalLink = $this->RemoveTermFromSearchURL(
146 $this->SuggestionsByFieldName[$Field->Name()][] = array(
147 "Name" => substr($Value, 1),
148 "RemoveLink" => $RemovalLink);
156 # next, iterate over tree fields and check to see if we need 157 # to clean up the link suggestions 158 foreach ($TreeFieldsToCheck as $FieldName => $Values)
160 # if we have two conditions for this field, the first one 161 # is an exact match, the second one is a prefix match, and 162 # they refer to the same string then we need to clean this 164 if (count($Values) == 2 &&
165 preg_match(
'/^=(.*)$/', $Values[0], $FirstMatch) &&
166 preg_match(
'/^\\^(.*) -- $/', $Values[1], $SecondMatch) &&
167 $FirstMatch[1] == $SecondMatch[1] )
169 # erase the two separate remove links 170 unset ($this->TreeSuggestionsByFieldName[$FieldName][$Values[0]]);
171 unset ($this->TreeSuggestionsByFieldName[$FieldName][$Values[1]]);
174 $Field = $Schema->GetField($FieldName);
176 # generate a remove link that covers both conditions 177 $RemovalLink = $this->RemoveTermFromSearchUrl(
178 $Field, array($Values[0], $Values[1]) );
180 # add that link in to our suggestions 181 $this->TreeSuggestionsByFieldName[$FieldName][$FirstMatch[1]][]=
182 array(
"Name" => $FirstMatch[1],
183 "RemoveLink" => $RemovalLink);
187 # within each field, sort the suggestions alphabetically 188 foreach ($this->TreeSuggestionsByFieldName as &$Suggestion)
190 uasort($Suggestion, array($this,
"FacetSuggestionSortCallback"));
192 foreach ($this->SuggestionsByFieldName as &$Suggestion)
194 uasort($Suggestion, array($this,
"FacetSuggestionSortCallback"));
204 return $this->SuggestionsByFieldName;
213 return $this->TreeSuggestionsByFieldName;
222 return $this->FieldsOpenByDefault;
226 # ---- PRIVATE INTERFACE ------------------------------------------------- 229 private $DisplayedValues;
230 private $FieldsOpenByDefault;
231 private $MaxFacetsPerField;
232 private $SearchParams;
233 private $SuggestionsByFieldName;
234 private $TreeSuggestionsByFieldName;
241 private function GenerateFacetsForTree($Field, $Suggestions)
243 # if we already have selections for this field, dig out the children of that 244 # selection and present those as options: 245 $CurrentValues = $this->SearchParams->GetSearchStringsForField($Field);
246 if (count($CurrentValues))
248 $this->FieldsOpenByDefault[$Field->Name()] = TRUE;
249 $ParentValue = $CurrentValues[0];
251 # if this was an exact match, rather than a 'contains' 252 if (strpos($ParentValue,
"=") === 0)
254 # cut off the leading '=' 255 $ParentValue = substr($ParentValue, 1);
258 $FacetsForThisTree = $this->GenerateFacetsForSelectedTree(
259 $Field, $Suggestions, $ParentValue);
261 # if we have facets to display, but not too many of them 262 if ((count($FacetsForThisTree) > 0)
263 && (count($FacetsForThisTree) <= $this->MaxFacetsPerField))
265 # and append the required 'remove current' link 266 $RemovalLink = $this->RemoveTermFromSearchURL($Field, $CurrentValues);
267 $FacetsForThisTree[] = array(
268 "Name" => $ParentValue,
269 "RemoveLink" => $RemovalLink);
271 # mark each value for this field displayed 272 foreach ($CurrentValues as $Val)
274 $this->DisplayedValues[$Field->Name()][$Val] = TRUE;
277 $this->TreeSuggestionsByFieldName[$Field->Name()] =
278 array($FacetsForThisTree);
283 # otherwise, list the toplevel options for this field 284 $FacetsForThisTree = $this->GenerateFacetsForUnselectedTree(
285 $Field, $Suggestions);
287 # if we have facets to display, but not too many add the toplevel options 288 # to the suggestions for this field 289 if ((count($FacetsForThisTree) > 0)
290 && (count($FacetsForThisTree) <= $this->MaxFacetsPerField))
292 $this->TreeSuggestionsByFieldName[$Field->Name()] =
306 private function GenerateFacetsForSelectedTree($Field, $Suggestions, $ParentValue)
308 $ParentSegments = explode(
" -- ", $ParentValue);
310 # keep track of what has been shown 311 $DisplayedSegments = array();
313 $FacetsForThisTree = array();
314 # pull out those classifications which were explicitly assigned to something. 315 # those are the ones we can get counts for: 316 $FieldName = $Field->Name();
317 foreach ($Suggestions as $ValueId => $ValueData)
319 $ValueName = $ValueData[
"Name"];
320 $ValueCount = $ValueData[
"Count"];
321 $FieldSegments = explode(
" -- ", $ValueName);
323 # print any children of the current field value 324 # (determined based on a prefix match 325 if ( preg_match(
"/^".preg_quote($ParentValue.
" -- ",
"/").
"/", $ValueName)
326 && (count($FieldSegments) == (count($ParentSegments) + 1)))
328 # note that we've already displayed this segment because it was selected 329 # allowing us to avoid displaying it twice 330 $DisplayedSegments[$ValueName]=TRUE;
331 $LeafValue = array_pop($FieldSegments);
333 # add the modified search URL to our list of facets for this field 334 $AdditionLink = $this->AddTermToSearchURL($Field, $ValueName);
336 $FacetsForThisTree[] = array(
337 "Name" => $LeafValue,
338 "AddLink" => $AdditionLink,
339 "Count" => $ValueCount);
341 # record value as having been displayed 342 $this->DisplayedValues[$FieldName][$LeafValue] = TRUE;
346 # now, we'll need to iterate over the suggestions again and make sure that we've 347 # displayed proper suggestions even for fields where only the lowest level 348 # elements are displayed 349 foreach ($Suggestions as $ValueId => $ValueData)
351 $ValueName = $ValueData[
"Name"];
352 $ValueCount = $ValueData[
"Count"];
354 # get an array of the segments for this suggestion 355 $FieldSegments = explode(
" -- ", $ValueName);
357 # skip segments that are along some other branch of this tree 358 if (!preg_match(
"/^".preg_quote($ParentValue.
" -- ",
"/")
364 # skip segments that aren't (grand)* children of the current parent. 365 if (count($FieldSegments) < count($ParentSegments))
370 # truncate our array of segments to include only the 371 # level below our currently selected level 372 $TargetSegments = array_slice($FieldSegments, 0,
373 (count($ParentSegments) + 1));
375 # reassemble this into a Classification string 376 $TargetName = implode(
" -- ", $TargetSegments);
378 # if we haven't already displayed this string, either 379 # because it was in an earlier suggestion or was 380 # actually included in the search, add it to our list 382 if (!isset($DisplayedSegments[$TargetName]))
384 # as above, note that we've displayed this segment 385 # and add a modified search URL to our list of 387 $DisplayedSegments[$TargetName] = TRUE;
388 $LeafValue = array_pop($TargetSegments);
389 $AdditionLink = $this->AddTermToSearchURL($Field, $TargetName);
390 $FacetsForThisTree[] = array(
391 "Name" => $LeafValue,
392 "AddLink" => $AdditionLink);
394 # record value as having been displayed 395 $this->DisplayedValues[$FieldName][$LeafValue] = TRUE;
399 return $FacetsForThisTree;
409 private function GenerateFacetsForUnselectedTree($Field, $Suggestions)
411 $FacetsForThisTree = array();
413 # keep track of what has been already shown 414 $DisplayedSegments = array();
416 # first, pull out those which are assigned to some reasources and for which 417 # we can compute counts (those with the top-level directly assigned) 418 $FieldName = $Field->Name();
419 foreach ($Suggestions as $ValueId => $ValueData )
421 $ValueName = $ValueData[
"Name"];
422 $ValueCount = $ValueData[
"Count"];
423 $FieldSegments = explode(
" -- ", $ValueName);
425 # if this is a top level field 426 if (count($FieldSegments) == 1)
428 # add it to our list of facets for this tree 429 $DisplayedSegments[$ValueName] = TRUE;
430 $AdditionLink = $this->AddTermToSearchURL($Field, $ValueName);
431 $FacetsForThisTree[] = array(
432 "Name" => $ValueName,
433 "AddLink" => $AdditionLink,
434 "Count" => $ValueCount);
436 # record value as having been displayed 437 $this->DisplayedValues[$FieldName][$ValueName] = TRUE;
441 # scan through the suggestions again, and verify that 442 # we've at least offered the top-level 443 # option for each entry (necessary for cases where no 444 # resources in the results set have the top-level value, 445 # e.g. they all have Science -- Technology but none have 446 # Science. in this case we can't display counts, but 447 # should at least suggest Science as an option) 448 foreach ($Suggestions as $ValueId => $ValueData)
450 $ValueName = $ValueData[
"Name"];
451 $FieldSegments = explode(
" -- ", $ValueName);
453 # if none of our previous efforts have displayed this segment 454 if (!isset($DisplayedSegments[$FieldSegments[0]]))
457 $DisplayedSegments[$FieldSegments[0]] = TRUE;
458 $AdditionLink = $this->AddTermToSearchURL(
459 $Field, $FieldSegments[0]);
460 $FacetsForThisTree[] = array(
461 "Name" => $FieldSegments[0],
462 "AddLink" => $AdditionLink,
463 "Count" => $ValueData[
"Count"]);
465 # record value as having been displayed 466 $this->DisplayedValues[$FieldName][$FieldSegments[0]] = TRUE;
470 return $FacetsForThisTree;
479 private function GenerateFacetsForName($Field, $Suggestions)
481 # for option fields, bail when there are too many suggestions for this option 482 if (count($Suggestions) > $this->MaxFacetsPerField)
487 # retrieve current search parameter values for field 488 $CurrentValues = $this->SearchParams->GetSearchStringsForField($Field);
490 # if a field is required, and we have only one suggestion and 491 # there is no current value, then we don't want to display this 492 # facet because the search results will be identical 493 if (($Field->Optional() == FALSE)
494 && (count($Suggestions) == 1)
495 && !count($CurrentValues))
500 # for each suggested value 501 $FieldName = $Field->Name();
502 foreach ($Suggestions as $ValueId => $ValueData)
504 $ValueName = $ValueData[
"Name"];
506 # if we have a current value that is selected for this search 507 if (in_array(
"=".$ValueName, $CurrentValues))
509 # note that this facet should be displayed and add a 511 $this->FieldsOpenByDefault[$FieldName] = TRUE;
512 $RemovalLink = $this->RemoveTermFromSearchURL(
513 $Field,
"=".$ValueName);
514 $this->SuggestionsByFieldName[$FieldName][] = array(
515 "Name" => $ValueName,
516 "RemoveLink" => $RemovalLink,
517 "Count" => $ValueData[
"Count"]);
521 # otherwise, put an 'add' link in our suggestions, 522 # displaying counts if we can 523 $AdditionLink = $this->AddTermToSearchURL(
524 $Field,
"=".$ValueName);
525 $this->SuggestionsByFieldName[$FieldName][] = array(
526 "Name" => $ValueName,
527 "AddLink" => $AdditionLink,
528 "Count" => $ValueData[
"Count"]);
531 # record value as having been displayed 532 $this->DisplayedValues[$FieldName][$ValueName] = TRUE;
543 private function AddTermToSearchURL($Field, $Term)
545 # create our own copy of search parameters 546 $OurSearchParams = clone $this->SearchParams;
548 # if this is not a tree field type 551 # retrieve subgroups from search parameters 552 $Subgroups = $OurSearchParams->GetSubgroups();
554 # find subgroup for this field 555 foreach ($Subgroups as $Group)
557 if (in_array($Field->Id(), $Group->GetFields()))
565 if (isset($Subgroup))
567 # add term to subgroup 568 $Subgroup->AddParameter($Term, $Field);
572 # create new subgroup 575 # set logic for new subgroup 576 $Subgroup->Logic($Field->SearchGroupLogic());
578 # add term to subgroup 579 $Subgroup->AddParameter($Term, $Field);
581 # add subgroup to search parameters 582 $OurSearchParams->AddSet($Subgroup);
587 # add specified term to search parameters 588 $OurSearchParams->RemoveParameter(NULL, $Field);
590 $Subgroup->Logic(
"OR");
591 $Subgroup->AddParameter(array(
592 "=".$Term,
"^".$Term.
" -- "), $Field);
593 $OurSearchParams->AddSet($Subgroup);
596 # build new URL with revised search parameters 597 $Url = implode(
"&", array_filter(array(
598 $this->BaseLink, $OurSearchParams->UrlParameterString())));
600 # return new URL to caller 611 private function RemoveTermFromSearchURL($Field, $Terms)
613 # create our own copy of search parameters with specified parameter removed 614 $OurSearchParams = clone $this->SearchParams;
618 # pull out the old setting 619 $OldSetting = $OurSearchParams->GetSearchStringsForField($Field);
620 $OurSearchParams->RemoveParameter($Terms, $Field);
622 # if we had an exact + prefix match for a specified level of the tree, 624 if (count($OldSetting) == 2 &&
625 preg_match(
'/^=(.*)$/', $OldSetting[0], $FirstMatch) &&
626 preg_match(
'/^\\^(.*) -- $/', $OldSetting[1], $SecondMatch) &&
627 $FirstMatch[1] == $SecondMatch[1] )
629 # explode out the segments 630 $FieldSegments = explode(
" -- ", $FirstMatch[1]);
632 # remove the last one 633 array_pop($FieldSegments);
635 # if we have any segments left 636 if (count($FieldSegments))
638 # construct revised setting 639 $NewSetting = implode(
" -- ", $FieldSegments);
641 $Subset->Logic(
"OR");
642 $Subset->AddParameter(
643 array(
"=".$NewSetting,
"^".$NewSetting.
" -- "),
645 $OurSearchParams->AddSet($Subset);
651 $OurSearchParams->RemoveParameter($Terms, $Field);
654 # build new URL with revised search parameters 655 $Url = implode(
"&", array_filter(array(
656 $this->BaseLink, $OurSearchParams->UrlParameterString())));
658 # return new URL to caller 668 private function FacetSuggestionSortCallback($A, $B)
670 if (isset($A[
"Name"]))
672 return strcmp($A[
"Name"], $B[
"Name"]);
678 return ($CountA == $CountB) ? 0
679 : (($CountA < $CountB) ? -1 : 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...