CWIS Developer Documentation
SearchFacetUI.php
Go to the documentation of this file.
1 <?PHP
2 #
3 # FILE: SearchFacetUI.php
4 #
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/
8 #
9 
16 {
17 
18  # ---- PUBLIC INTERFACE --------------------------------------------------
19 
32  public function __construct($BaseLink, $SearchParams, $SearchResults,
33  $User, $MaxFacetsPerField, $SchemaId)
34  {
35  # save base URL, search parameters, and max facets for later use
36  $this->BaseLink = $BaseLink;
37  $this->SearchParams = $SearchParams;
38  $this->MaxFacetsPerField = $MaxFacetsPerField;
39 
40  # facets which should be displayed because they were explicitly
41  # included in the search terms
42  $this->FieldsOpenByDefault = array();
43 
44  # suggestions for Option and ControlledName fields
45  # array( FieldName => array(ModifiedSearchUrls)
46  $this->SuggestionsByFieldName = array();
47 
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
54  # current selection
55  $this->TreeSuggestionsByFieldName = array();
56 
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
63  # the tree.
64  $Schema = new MetadataSchema($SchemaId);
65  $ResultFacets = SPTSearchEngine::GetResultFacets($SearchResults, $User);
66  foreach ($ResultFacets as $FieldName => $Suggestions)
67  {
68  if (!$Schema->FieldExists($FieldName))
69  {
70  continue;
71  }
72 
73  $Field = $Schema->GetField($FieldName);
74  $FieldId = $Field->Id();
75 
76  # filter suggestions
77  foreach ($Suggestions as $ValueId => $ValueData)
78  {
79  # only display fields with non-empty value
80  $NewVal = $this->FilterFieldValue($Field, $ValueData["Name"]);
81  if (empty($NewVal))
82  {
83  unset($Suggestions[$ValueId]);
84  }
85  else
86  {
87  $Suggestions[$ValueId]["Name"] = $NewVal;
88  }
89  }
90 
91  # foreach field, generate a list of suggested facets
92  # and determine if the field should be open by default
93  if ($Field->Type() == MetadataSchema::MDFTYPE_TREE)
94  {
95  $Suggestions = $this->GenerateFacetsForTree($Field, $Suggestions);
96  }
97  else
98  {
99  $this->GenerateFacetsForName($Field, $Suggestions);
100  }
101  }
102 
103  # Make sure that we've got a facet displayed for each selected search option
104  # (including those that were not suggested as facets)
105 
106  # for each field in search parameters
107  foreach ($this->SearchParams->GetSearchStrings(TRUE)
108  as $FieldId => $Values)
109  {
110  # if the field does not exist, move to the next one
111  if (!$Schema->FieldExists($FieldId))
112  {
113  continue;
114  }
115 
116  # if field is valid and viewable
117  $Field = $Schema->GetField($FieldId);
118  if (is_object($Field)
119  && ($Field->Status() === MetadataSchema::MDFSTAT_OK)
120  && $Field->UserCanView($User))
121  {
122  # if this was an old-format 'is under', translate it to the new format
123  if ($Field->Type() == MetadataSchema::MDFTYPE_TREE)
124  {
125  $Values = $this->NormalizeTreeValues($Values);
126  }
127 
128  # for each value for field
129  $FieldName = $Field->Name();
130  foreach ($Values as $Value)
131  {
132  # first filter field value
133  $NewVal = $this->FilterFieldValue($Field, $Value);
134  if (empty($NewVal))
135  {
136  continue;
137  }
138  else
139  {
140  $Value = $NewVal;
141  }
142 
143  # for Tree fields
144  if ($Field->Type() == MetadataSchema::MDFTYPE_TREE)
145  {
146  # if value is not already in list
147  if (!isset($this->DisplayedValues[$Field->Name()][$Value]))
148  {
149  # allow removing the current search terms
150  $this->FieldsOpenByDefault[$FieldName] = TRUE;
151 
152  # if this was an 'is or begins with' condition,
153  # display it nicely
154  if (preg_match('%\^(.+) -- ?$%', $Value, $Matches))
155  {
156  $Name = $Matches[1];
157  }
158  else
159  {
160  $Name = $Value;
161  }
162 
163  $RemovalLink = $this->RemoveTermFromSearchURL(
164  $Field, $Value);
165  $this->TreeSuggestionsByFieldName[$FieldName][$Value]
166  = array(
167  "Name" => $Name,
168  "RemoveLink" => $RemovalLink);
169  }
170  }
171  # for Option and Controlled Name fields
172  elseif ($Field->Type() == MetadataSchema::MDFTYPE_OPTION
173  || $Field->Type() == MetadataSchema::MDFTYPE_CONTROLLEDNAME)
174  {
175  # if this is not a "contains" search parameter
176  if ($Value[0] == "=")
177  {
178  # if value is not already in list
179  if (!isset($this->DisplayedValues[$Field->Name(
180  )][substr($Value, 1)]))
181  {
182  # note that this field should be open
183  $this->FieldsOpenByDefault[$FieldName] = TRUE;
184 
185  # mark as a facet that can be removed
186  $RemovalLink = $this->RemoveTermFromSearchURL(
187  $Field, $Value);
188  $this->SuggestionsByFieldName[$Field->Name()][] = array(
189  "Name" => substr($Value, 1),
190  "RemoveLink" => $RemovalLink);
191  }
192  }
193  }
194  }
195  }
196  }
197 
198  # within each field, sort the suggestions alphabetically
199  foreach ($this->TreeSuggestionsByFieldName as &$Suggestion)
200  {
201  uasort($Suggestion, array($this, "FacetSuggestionSortCallback"));
202  }
203  foreach ($this->SuggestionsByFieldName as &$Suggestion)
204  {
205  uasort($Suggestion, array($this, "FacetSuggestionSortCallback"));
206  }
207  }
208 
213  public function GetSuggestionsByFieldName()
214  {
215  return $this->SuggestionsByFieldName;
216  }
217 
223  {
224  return $this->TreeSuggestionsByFieldName;
225  }
226 
231  public function GetFieldsOpenByDefault()
232  {
233  return $this->FieldsOpenByDefault;
234  }
235 
236 
237  # ---- PRIVATE INTERFACE -------------------------------------------------
238 
239  private $BaseLink;
240  private $DisplayedValues;
241  private $FieldsOpenByDefault;
242  private $MaxFacetsPerField;
243  private $SearchParams;
244  private $SuggestionsByFieldName;
245  private $TreeSuggestionsByFieldName;
246 
253  private function FilterFieldValue($Field, $Value)
254  {
255  return $GLOBALS["AF"]->SignalEvent(
256  "EVENT_FIELD_DISPLAY_FILTER", array(
257  "Field" => $Field,
258  "Resource" => NULL,
259  "Value" => $Value))["Value"];
260  }
261 
267  private function GenerateFacetsForTree($Field, $Suggestions)
268  {
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);
272 
273  $CurrentValues = $this->NormalizeTreeValues($CurrentValues);
274 
275  if (count($CurrentValues) == 1)
276  {
277  $this->FieldsOpenByDefault[$Field->Name()] = TRUE;
278  $ParentValue = $CurrentValues[0];
279 
280  if (preg_match('%\^(.+) -- ?$%', $ParentValue, $Matches))
281  {
282  $ParentValue = $Matches[1];
283  }
284 
285  $FacetsForThisTree = $this->GenerateFacetsForSelectedTree(
286  $Field, $Suggestions, $ParentValue);
287 
288  # if we have facets to display, but not too many of them
289  if ((count($FacetsForThisTree) > 0)
290  && (count($FacetsForThisTree) <= $this->MaxFacetsPerField))
291  {
292  # and append the required 'remove current' link
293  $RemovalLink = $this->RemoveTermFromSearchURL($Field, $CurrentValues);
294  $FacetsForThisTree[] = array(
295  "Name" => $ParentValue,
296  "RemoveLink" => $RemovalLink);
297 
298 
299  # mark each value for this field displayed
300  foreach ($CurrentValues as $Val)
301  {
302  $this->DisplayedValues[$Field->Name()][$Val] = TRUE;
303  }
304 
305  $this->TreeSuggestionsByFieldName[$Field->Name()] =
306  array($FacetsForThisTree);
307  }
308  }
309  else
310  {
311  # otherwise, list the toplevel options for this field
312  $FacetsForThisTree = $this->GenerateFacetsForUnselectedTree(
313  $Field, $Suggestions);
314 
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))
319  {
320  $this->TreeSuggestionsByFieldName[$Field->Name()] =
321  $FacetsForThisTree;
322  }
323  }
324  }
325 
334  private function GenerateFacetsForSelectedTree($Field, $Suggestions, $ParentValue)
335  {
336  $ParentSegments = explode(" -- ", $ParentValue);
337 
338  # keep track of what has been shown
339  $DisplayedSegments = array();
340 
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)
346  {
347  $ValueName = $ValueData["Name"];
348  $ValueCount = $ValueData["Count"];
349  $FieldSegments = explode(" -- ", $ValueName);
350 
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)))
355  {
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);
360 
361  # add the modified search URL to our list of facets for this field
362  $AdditionLink = $this->AddTermToSearchURL($Field, $ValueName);
363 
364  $FacetsForThisTree[] = array(
365  "Name" => $LeafValue,
366  "AddLink" => $AdditionLink,
367  "Count" => $ValueCount);
368 
369  # record value as having been displayed
370  $this->DisplayedValues[$FieldName][$LeafValue] = TRUE;
371  }
372  }
373 
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)
378  {
379  $ValueName = $ValueData["Name"];
380  $ValueCount = $ValueData["Count"];
381 
382  # get an array of the segments for this suggestion
383  $FieldSegments = explode(" -- ", $ValueName);
384 
385  # skip segments that are along some other branch of this tree
386  if (!preg_match("/^".preg_quote($ParentValue." -- ", "/")
387  ."/", $ValueName))
388  {
389  continue;
390  }
391 
392  # skip segments that aren't (grand)* children of the current parent.
393  if (count($FieldSegments) < count($ParentSegments))
394  {
395  continue;
396  }
397 
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));
402 
403  # reassemble this into a Classification string
404  $TargetName = implode(" -- ", $TargetSegments);
405 
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
409  # now
410  if (!isset($DisplayedSegments[$TargetName]))
411  {
412  # as above, note that we've displayed this segment
413  # and add a modified search URL to our list of
414  # facets to display
415  $DisplayedSegments[$TargetName] = TRUE;
416  $LeafValue = array_pop($TargetSegments);
417  $AdditionLink = $this->AddTermToSearchURL($Field, $TargetName);
418  $FacetsForThisTree[] = array(
419  "Name" => $LeafValue,
420  "AddLink" => $AdditionLink);
421 
422  # record value as having been displayed
423  $this->DisplayedValues[$FieldName][$LeafValue] = TRUE;
424  }
425  }
426 
427  return $FacetsForThisTree;
428  }
429 
437  private function GenerateFacetsForUnselectedTree($Field, $Suggestions)
438  {
439  $FacetsForThisTree = array();
440 
441  # keep track of what has been already shown
442  $DisplayedSegments = array();
443 
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 )
448  {
449  $ValueName = $ValueData["Name"];
450  $ValueCount = $ValueData["Count"];
451  $FieldSegments = explode(" -- ", $ValueName);
452 
453  # if this is a top level field
454  if (count($FieldSegments) == 1)
455  {
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);
463 
464  # record value as having been displayed
465  $this->DisplayedValues[$FieldName][$ValueName] = TRUE;
466  }
467  }
468 
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)
477  {
478  $ValueName = $ValueData["Name"];
479  $FieldSegments = explode(" -- ", $ValueName);
480 
481  # if none of our previous efforts have displayed this segment
482  if (!isset($DisplayedSegments[$FieldSegments[0]]))
483  {
484  # add it to our list
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"]);
492 
493  # record value as having been displayed
494  $this->DisplayedValues[$FieldName][$FieldSegments[0]] = TRUE;
495  }
496  }
497 
498  return $FacetsForThisTree;
499  }
500 
507  private function GenerateFacetsForName($Field, $Suggestions)
508  {
509  # for option fields, bail when there are too many suggestions for this option
510  if (count($Suggestions) > $this->MaxFacetsPerField)
511  {
512  return;
513  }
514 
515  # retrieve current search parameter values for field
516  $CurrentValues = $this->SearchParams->GetSearchStringsForField($Field);
517 
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))
524  {
525  return;
526  }
527 
528  # for each suggested value
529  $FieldName = $Field->Name();
530  foreach ($Suggestions as $ValueId => $ValueData)
531  {
532  $ValueName = $ValueData["Name"];
533 
534  # if we have a current value that is selected for this search
535  if (in_array("=".$ValueName, $CurrentValues))
536  {
537  # note that this facet should be displayed and add a
538  # remove link for it
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"]);
546  }
547  else
548  {
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"]);
557  }
558 
559  # record value as having been displayed
560  $this->DisplayedValues[$FieldName][$ValueName] = TRUE;
561  }
562  }
563 
571  private function AddTermToSearchURL($Field, $Term)
572  {
573  # create our own copy of search parameters
574  $OurSearchParams = clone $this->SearchParams;
575 
576  # if this is not a tree field type
577  if ($Field->Type() != MetadataSchema::MDFTYPE_TREE)
578  {
579  # retrieve subgroups from search parameters
580  $Subgroups = $OurSearchParams->GetSubgroups();
581 
582  # find subgroup for this field
583  foreach ($Subgroups as $Group)
584  {
585  if (in_array($Field->Id(), $Group->GetFields()))
586  {
587  $Subgroup = $Group;
588  break;
589  }
590  }
591 
592  # if subgroup found
593  if (isset($Subgroup))
594  {
595  # add term to subgroup
596  $Subgroup->AddParameter($Term, $Field);
597  }
598  else
599  {
600  # create new subgroup
601  $Subgroup = new SearchParameterSet();
602 
603  # set logic for new subgroup
604  $Subgroup->Logic($Field->SearchGroupLogic());
605 
606  # add term to subgroup
607  $Subgroup->AddParameter($Term, $Field);
608 
609  # add subgroup to search parameters
610  $OurSearchParams->AddSet($Subgroup);
611  }
612  }
613  else
614  {
615  # add specified term to search parameters
616  $OurSearchParams->RemoveParameter(NULL, $Field);
617  $OurSearchParams->AddParameter(
618  "^".$Term." --", $Field);
619  }
620 
621  # build new URL with revised search parameters
622  $Url = implode("&amp;", array_filter(array(
623  $this->BaseLink, $OurSearchParams->UrlParameterString())));
624 
625  # return new URL to caller
626  return $Url;
627  }
628 
636  private function RemoveTermFromSearchURL($Field, $Terms)
637  {
638  # create our own copy of search parameters with specified parameter removed
639  $OurSearchParams = clone $this->SearchParams;
640 
641  if ($Field->Type() == MetadataSchema::MDFTYPE_TREE)
642  {
643  # pull out the old setting
644  $OldSetting = $OurSearchParams->GetSearchStringsForField($Field);
645  $OurSearchParams->RemoveParameter($Terms, $Field);
646 
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))
650  {
651  # explode out the segments
652  $FieldSegments = explode(" -- ", $Match[1]);
653 
654  # remove the last one
655  array_pop($FieldSegments);
656 
657  # if we have any segments left
658  if (count($FieldSegments))
659  {
660  # construct revised setting
661  $NewSetting = implode(" -- ", $FieldSegments);
662  $OurSearchParams->AddParameter(
663  "^".$NewSetting." --", $Field);
664  }
665  }
666  }
667  else
668  {
669  $OurSearchParams->RemoveParameter($Terms, $Field);
670  }
671 
672  # build new URL with revised search parameters
673  $Url = implode("&amp;", array_filter(array(
674  $this->BaseLink, $OurSearchParams->UrlParameterString())));
675 
676  # return new URL to caller
677  return $Url;
678  }
679 
686  private function FacetSuggestionSortCallback($A, $B)
687  {
688  if (isset($A["Name"]))
689  {
690  return strcmp($A["Name"], $B["Name"]);
691  }
692  else
693  {
694  $CountA = count($A);
695  $CountB = count($B);
696  return ($CountA == $CountB) ? 0
697  : (($CountA < $CountB) ? -1 : 1);
698  }
699  }
700 
707  private function NormalizeTreeValues($Values)
708  {
709  if (count($Values) == 2 &&
710  preg_match('/^=(.*)$/', $Values[0], $FirstMatch) &&
711  preg_match('/^\\^(.*) -- $/', $Values[1], $SecondMatch) &&
712  $FirstMatch[1] == $SecondMatch[1] )
713  {
714  $Values = ["^".$FirstMatch[1]." --"];
715  }
716 
717  return $Values;
718  }
719 }
GetFieldsOpenByDefault()
Retrieve which fields should be initially open in facet UI.
Metadata schema (in effect a Factory class for MetadataField).
Set of parameters used to perform a search.
GetSuggestionsByFieldName()
Retrieve facet UI data for non-tree metadata fields.
const MDFTYPE_CONTROLLEDNAME
__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...