CWIS Developer Documentation

- Home
- Main Page
- Namespaces
- Classes
- Files

- File List
- File Members

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
15 class SearchFacetUI
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
222 public function GetTreeSuggestionsByFieldName()
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 }
SearchFacetUI\GetFieldsOpenByDefault
GetFieldsOpenByDefault()
Retrieve which fields should be initially open in facet UI.
Definition: SearchFacetUI.php:231
MetadataSchema
Metadata schema (in effect a Factory class for MetadataField).
Definition: MetadataSchema.php:13
SearchParameterSet
Set of parameters used to perform a search.
Definition: SearchParameterSet.php:13
MetadataSchema\MDFSTAT_OK
const MDFSTAT_OK
Definition: MetadataSchema.php:43
MetadataSchema\MDFTYPE_TREE
const MDFTYPE_TREE
Definition: MetadataSchema.php:27
SearchFacetUI\GetSuggestionsByFieldName
GetSuggestionsByFieldName()
Retrieve facet UI data for non-tree metadata fields.
Definition: SearchFacetUI.php:213
MetadataSchema\MDFTYPE_OPTION
const MDFTYPE_OPTION
Definition: MetadataSchema.php:29
MetadataSchema\MDFTYPE_CONTROLLEDNAME
const MDFTYPE_CONTROLLEDNAME
Definition: MetadataSchema.php:28
PHP
PHP
Definition: RSSClient--Test.php:12
SearchFacetUI\__construct
__construct($BaseLink, $SearchParams, $SearchResults, $User, $MaxFacetsPerField, $SchemaId)
Constructor, that accepts the search parameters and results and prepares the facet UI data to be retr...
Definition: SearchFacetUI.php:32
SearchFacetUI\GetTreeSuggestionsByFieldName
GetTreeSuggestionsByFieldName()
Retrieve facet UI data for tree metadata fields.
Definition: SearchFacetUI.php:222
SPTSearchEngine\GetResultFacets
static GetResultFacets($SearchResults, $User)
Generate a list of suggested additional search terms that can be used for faceted searching...
Definition: SPTSearchEngine.php:728
SearchFacetUI
SearchFacetUI supports the generation of a user interface for faceted search, by taking the search pa...
Definition: SearchFacetUI.php:15