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  # foreach field, generate a list of suggested facets
77  # and determine if the field should be open by default
78  if ($Field->Type() == MetadataSchema::MDFTYPE_TREE)
79  {
80  $Suggestions = $this->GenerateFacetsForTree($Field, $Suggestions);
81  }
82  else
83  {
84  $this->GenerateFacetsForName($Field, $Suggestions);
85  }
86  }
87 
88  # Make sure that we've got a facet displayed for each selected search option
89  # (including those that were not suggested as facets)
90 
91  # for each field in search parameters
92  foreach ($this->SearchParams->GetSearchStrings(TRUE)
93  as $FieldId => $Values)
94  {
95  # if the field does not exist, move to the next one
96  if (!$Schema->FieldExists($FieldId))
97  {
98  continue;
99  }
100 
101  # if field is valid and viewable
102  $Field = $Schema->GetField($FieldId);
103  if (is_object($Field)
104  && ($Field->Status() === MetadataSchema::MDFSTAT_OK)
105  && $Field->UserCanView($User))
106  {
107  # if this was an old-format 'is under', translate it to the new format
108  if ($Field->Type() == MetadataSchema::MDFTYPE_TREE)
109  {
110  $Values = $this->NormalizeTreeValues($Values);
111  }
112 
113  # for each value for field
114  $FieldName = $Field->Name();
115  foreach ($Values as $Value)
116  {
117  # for Tree fields
118  if ($Field->Type() == MetadataSchema::MDFTYPE_TREE)
119  {
120  # if value is not already in list
121  if (!isset($this->DisplayedValues[$Field->Name()][$Value]))
122  {
123  # allow removing the current search terms
124  $this->FieldsOpenByDefault[$FieldName] = TRUE;
125 
126  # if this was an 'is or begins with' condition,
127  # display it nicely
128  if (preg_match('%\^(.+) -- ?$%', $Value, $Matches))
129  {
130  $Name = $Matches[1];
131  }
132  else
133  {
134  $Name = $Value;
135  }
136 
137  $RemovalLink = $this->RemoveTermFromSearchURL(
138  $Field, $Value);
139  $this->TreeSuggestionsByFieldName[$FieldName][$Value]
140  = array(
141  "Name" => $Name,
142  "RemoveLink" => $RemovalLink);
143  }
144  }
145  # for Option and Controlled Name fields
146  elseif ($Field->Type() == MetadataSchema::MDFTYPE_OPTION
147  || $Field->Type() == MetadataSchema::MDFTYPE_CONTROLLEDNAME)
148  {
149  # if this is not a "contains" search parameter
150  if ($Value[0] == "=")
151  {
152  # if value is not already in list
153  if (!isset($this->DisplayedValues[$Field->Name(
154  )][substr($Value, 1)]))
155  {
156  # note that this field should be open
157  $this->FieldsOpenByDefault[$FieldName] = TRUE;
158 
159  # mark as a facet that can be removed
160  $RemovalLink = $this->RemoveTermFromSearchURL(
161  $Field, $Value);
162  $this->SuggestionsByFieldName[$Field->Name()][] = array(
163  "Name" => substr($Value, 1),
164  "RemoveLink" => $RemovalLink);
165  }
166  }
167  }
168  }
169  }
170  }
171 
172  # within each field, sort the suggestions alphabetically
173  foreach ($this->TreeSuggestionsByFieldName as &$Suggestion)
174  {
175  uasort($Suggestion, array($this, "FacetSuggestionSortCallback"));
176  }
177  foreach ($this->SuggestionsByFieldName as &$Suggestion)
178  {
179  uasort($Suggestion, array($this, "FacetSuggestionSortCallback"));
180  }
181  }
182 
187  public function GetSuggestionsByFieldName()
188  {
189  return $this->SuggestionsByFieldName;
190  }
191 
197  {
198  return $this->TreeSuggestionsByFieldName;
199  }
200 
205  public function GetFieldsOpenByDefault()
206  {
207  return $this->FieldsOpenByDefault;
208  }
209 
210 
211  # ---- PRIVATE INTERFACE -------------------------------------------------
212 
213  private $BaseLink;
214  private $DisplayedValues;
215  private $FieldsOpenByDefault;
216  private $MaxFacetsPerField;
217  private $SearchParams;
218  private $SuggestionsByFieldName;
219  private $TreeSuggestionsByFieldName;
220 
226  private function GenerateFacetsForTree($Field, $Suggestions)
227  {
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);
231 
232  $CurrentValues = $this->NormalizeTreeValues($CurrentValues);
233 
234  if (count($CurrentValues) == 1)
235  {
236  $this->FieldsOpenByDefault[$Field->Name()] = TRUE;
237  $ParentValue = $CurrentValues[0];
238 
239  if (preg_match('%\^(.+) -- ?$%', $ParentValue, $Matches))
240  {
241  $ParentValue = $Matches[1];
242  }
243 
244  $FacetsForThisTree = $this->GenerateFacetsForSelectedTree(
245  $Field, $Suggestions, $ParentValue);
246 
247  # if we have facets to display, but not too many of them
248  if ((count($FacetsForThisTree) > 0)
249  && (count($FacetsForThisTree) <= $this->MaxFacetsPerField))
250  {
251  # and append the required 'remove current' link
252  $RemovalLink = $this->RemoveTermFromSearchURL($Field, $CurrentValues);
253  $FacetsForThisTree[] = array(
254  "Name" => $ParentValue,
255  "RemoveLink" => $RemovalLink);
256 
257 
258  # mark each value for this field displayed
259  foreach ($CurrentValues as $Val)
260  {
261  $this->DisplayedValues[$Field->Name()][$Val] = TRUE;
262  }
263 
264  $this->TreeSuggestionsByFieldName[$Field->Name()] =
265  array($FacetsForThisTree);
266  }
267  }
268  else
269  {
270  # otherwise, list the toplevel options for this field
271  $FacetsForThisTree = $this->GenerateFacetsForUnselectedTree(
272  $Field, $Suggestions);
273 
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))
278  {
279  $this->TreeSuggestionsByFieldName[$Field->Name()] =
280  $FacetsForThisTree;
281  }
282  }
283  }
284 
293  private function GenerateFacetsForSelectedTree($Field, $Suggestions, $ParentValue)
294  {
295  $ParentSegments = explode(" -- ", $ParentValue);
296 
297  # keep track of what has been shown
298  $DisplayedSegments = array();
299 
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)
305  {
306  $ValueName = $ValueData["Name"];
307  $ValueCount = $ValueData["Count"];
308  $FieldSegments = explode(" -- ", $ValueName);
309 
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)))
314  {
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);
319 
320  # add the modified search URL to our list of facets for this field
321  $AdditionLink = $this->AddTermToSearchURL($Field, $ValueName);
322 
323  $FacetsForThisTree[] = array(
324  "Name" => $LeafValue,
325  "AddLink" => $AdditionLink,
326  "Count" => $ValueCount);
327 
328  # record value as having been displayed
329  $this->DisplayedValues[$FieldName][$LeafValue] = TRUE;
330  }
331  }
332 
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)
337  {
338  $ValueName = $ValueData["Name"];
339  $ValueCount = $ValueData["Count"];
340 
341  # get an array of the segments for this suggestion
342  $FieldSegments = explode(" -- ", $ValueName);
343 
344  # skip segments that are along some other branch of this tree
345  if (!preg_match("/^".preg_quote($ParentValue." -- ", "/")
346  ."/", $ValueName))
347  {
348  continue;
349  }
350 
351  # skip segments that aren't (grand)* children of the current parent.
352  if (count($FieldSegments) < count($ParentSegments))
353  {
354  continue;
355  }
356 
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));
361 
362  # reassemble this into a Classification string
363  $TargetName = implode(" -- ", $TargetSegments);
364 
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
368  # now
369  if (!isset($DisplayedSegments[$TargetName]))
370  {
371  # as above, note that we've displayed this segment
372  # and add a modified search URL to our list of
373  # facets to display
374  $DisplayedSegments[$TargetName] = TRUE;
375  $LeafValue = array_pop($TargetSegments);
376  $AdditionLink = $this->AddTermToSearchURL($Field, $TargetName);
377  $FacetsForThisTree[] = array(
378  "Name" => $LeafValue,
379  "AddLink" => $AdditionLink);
380 
381  # record value as having been displayed
382  $this->DisplayedValues[$FieldName][$LeafValue] = TRUE;
383  }
384  }
385 
386  return $FacetsForThisTree;
387  }
388 
396  private function GenerateFacetsForUnselectedTree($Field, $Suggestions)
397  {
398  $FacetsForThisTree = array();
399 
400  # keep track of what has been already shown
401  $DisplayedSegments = array();
402 
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 )
407  {
408  $ValueName = $ValueData["Name"];
409  $ValueCount = $ValueData["Count"];
410  $FieldSegments = explode(" -- ", $ValueName);
411 
412  # if this is a top level field
413  if (count($FieldSegments) == 1)
414  {
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);
422 
423  # record value as having been displayed
424  $this->DisplayedValues[$FieldName][$ValueName] = TRUE;
425  }
426  }
427 
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)
436  {
437  $ValueName = $ValueData["Name"];
438  $FieldSegments = explode(" -- ", $ValueName);
439 
440  # if none of our previous efforts have displayed this segment
441  if (!isset($DisplayedSegments[$FieldSegments[0]]))
442  {
443  # add it to our list
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"]);
451 
452  # record value as having been displayed
453  $this->DisplayedValues[$FieldName][$FieldSegments[0]] = TRUE;
454  }
455  }
456 
457  return $FacetsForThisTree;
458  }
459 
466  private function GenerateFacetsForName($Field, $Suggestions)
467  {
468  # for option fields, bail when there are too many suggestions for this option
469  if (count($Suggestions) > $this->MaxFacetsPerField)
470  {
471  return;
472  }
473 
474  # retrieve current search parameter values for field
475  $CurrentValues = $this->SearchParams->GetSearchStringsForField($Field);
476 
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))
483  {
484  return;
485  }
486 
487  # for each suggested value
488  $FieldName = $Field->Name();
489  foreach ($Suggestions as $ValueId => $ValueData)
490  {
491  $ValueName = $ValueData["Name"];
492 
493  # if we have a current value that is selected for this search
494  if (in_array("=".$ValueName, $CurrentValues))
495  {
496  # note that this facet should be displayed and add a
497  # remove link for it
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"]);
505  }
506  else
507  {
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"]);
516  }
517 
518  # record value as having been displayed
519  $this->DisplayedValues[$FieldName][$ValueName] = TRUE;
520  }
521  }
522 
530  private function AddTermToSearchURL($Field, $Term)
531  {
532  # create our own copy of search parameters
533  $OurSearchParams = clone $this->SearchParams;
534 
535  # if this is not a tree field type
536  if ($Field->Type() != MetadataSchema::MDFTYPE_TREE)
537  {
538  # retrieve subgroups from search parameters
539  $Subgroups = $OurSearchParams->GetSubgroups();
540 
541  # find subgroup for this field
542  foreach ($Subgroups as $Group)
543  {
544  if (in_array($Field->Id(), $Group->GetFields()))
545  {
546  $Subgroup = $Group;
547  break;
548  }
549  }
550 
551  # if subgroup found
552  if (isset($Subgroup))
553  {
554  # add term to subgroup
555  $Subgroup->AddParameter($Term, $Field);
556  }
557  else
558  {
559  # create new subgroup
560  $Subgroup = new SearchParameterSet();
561 
562  # set logic for new subgroup
563  $Subgroup->Logic($Field->SearchGroupLogic());
564 
565  # add term to subgroup
566  $Subgroup->AddParameter($Term, $Field);
567 
568  # add subgroup to search parameters
569  $OurSearchParams->AddSet($Subgroup);
570  }
571  }
572  else
573  {
574  # add specified term to search parameters
575  $OurSearchParams->RemoveParameter(NULL, $Field);
576  $OurSearchParams->AddParameter(
577  "^".$Term." --", $Field);
578  }
579 
580  # build new URL with revised search parameters
581  $Url = implode("&amp;", array_filter(array(
582  $this->BaseLink, $OurSearchParams->UrlParameterString())));
583 
584  # return new URL to caller
585  return $Url;
586  }
587 
595  private function RemoveTermFromSearchURL($Field, $Terms)
596  {
597  # create our own copy of search parameters with specified parameter removed
598  $OurSearchParams = clone $this->SearchParams;
599 
600  if ($Field->Type() == MetadataSchema::MDFTYPE_TREE)
601  {
602  # pull out the old setting
603  $OldSetting = $OurSearchParams->GetSearchStringsForField($Field);
604  $OurSearchParams->RemoveParameter($Terms, $Field);
605 
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))
609  {
610  # explode out the segments
611  $FieldSegments = explode(" -- ", $Match[1]);
612 
613  # remove the last one
614  array_pop($FieldSegments);
615 
616  # if we have any segments left
617  if (count($FieldSegments))
618  {
619  # construct revised setting
620  $NewSetting = implode(" -- ", $FieldSegments);
621  $OurSearchParams->AddParameter(
622  "^".$NewSetting." --", $Field);
623  }
624  }
625  }
626  else
627  {
628  $OurSearchParams->RemoveParameter($Terms, $Field);
629  }
630 
631  # build new URL with revised search parameters
632  $Url = implode("&amp;", array_filter(array(
633  $this->BaseLink, $OurSearchParams->UrlParameterString())));
634 
635  # return new URL to caller
636  return $Url;
637  }
638 
645  private function FacetSuggestionSortCallback($A, $B)
646  {
647  if (isset($A["Name"]))
648  {
649  return strcmp($A["Name"], $B["Name"]);
650  }
651  else
652  {
653  $CountA = count($A);
654  $CountB = count($B);
655  return ($CountA == $CountB) ? 0
656  : (($CountA < $CountB) ? -1 : 1);
657  }
658  }
659 
666  private function NormalizeTreeValues($Values)
667  {
668  if (count($Values) == 2 &&
669  preg_match('/^=(.*)$/', $Values[0], $FirstMatch) &&
670  preg_match('/^\\^(.*) -- $/', $Values[1], $SecondMatch) &&
671  $FirstMatch[1] == $SecondMatch[1] )
672  {
673  $Values = ["^".$FirstMatch[1]." --"];
674  }
675 
676  return $Values;
677  }
678 }
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...