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 # 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 $TreeFieldsToCheck = array();
92 # for each field in search parameters
93 foreach ($this->SearchParams->GetSearchStrings(TRUE)
94 as $FieldId => $Values)
95 {
96 # if the field does not exist, move to the next one
97 if (!$Schema->FieldExists($FieldId))
98 {
99 continue;
100 }
101
102 # if field is valid and viewable
103 $Field = $Schema->GetField($FieldId);
104 if (is_object($Field)
105 && ($Field->Status() === MetadataSchema::MDFSTAT_OK)
106 && $Field->UserCanView($User))
107 {
108 # for each value for field
109 $FieldName = $Field->Name();
110 foreach ($Values as $Value)
111 {
112 # for Tree fields
113 if ($Field->Type() == MetadataSchema::MDFTYPE_TREE)
114 {
115 # if value is not already in list
116 if (!isset($this->DisplayedValues[$Field->Name()][$Value]))
117 {
118 $TreeFieldsToCheck[$Field->Name()][]= $Value;
119 # allow removing the current search terms
120 $this->FieldsOpenByDefault[$FieldName] = TRUE;
121 $RemovalLink = $this->RemoveTermFromSearchURL(
122 $Field, $Value);
123 $this->TreeSuggestionsByFieldName[$FieldName][$Value][]
124 = array(
125 "Name" => $Value,
126 "RemoveLink" => $RemovalLink);
127 }
128 }
129 # for Option and Controlled Name fields
130 elseif ($Field->Type() == MetadataSchema::MDFTYPE_OPTION
131 || $Field->Type() == MetadataSchema::MDFTYPE_CONTROLLEDNAME)
132 {
133 # if this is not a "contains" search parameter
134 if ($Value[0] == "=")
135 {
136 # if value is not already in list
137 if (!isset($this->DisplayedValues[$Field->Name(
138 )][substr($Value, 1)]))
139 {
140 # note that this field should be open
141 $this->FieldsOpenByDefault[$FieldName] = TRUE;
142
143 # mark as a facet that can be removed
144 $RemovalLink = $this->RemoveTermFromSearchURL(
145 $Field, $Value);
146 $this->SuggestionsByFieldName[$Field->Name()][] = array(
147 "Name" => substr($Value, 1),
148 "RemoveLink" => $RemovalLink);
149 }
150 }
151 }
152 }
153 }
154 }
155
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)
159 {
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
163 # one up
164 if (count($Values) == 2 &&
165 preg_match('/^=(.*)$/', $Values[0], $FirstMatch) &&
166 preg_match('/^\\^(.*) -- $/', $Values[1], $SecondMatch) &&
167 $FirstMatch[1] == $SecondMatch[1] )
168 {
169 # erase the two separate remove links
170 unset ($this->TreeSuggestionsByFieldName[$FieldName][$Values[0]]);
171 unset ($this->TreeSuggestionsByFieldName[$FieldName][$Values[1]]);
172
173 # pull out our field
174 $Field = $Schema->GetField($FieldName);
175
176 # generate a remove link that covers both conditions
177 $RemovalLink = $this->RemoveTermFromSearchUrl(
178 $Field, array($Values[0], $Values[1]) );
179
180 # add that link in to our suggestions
181 $this->TreeSuggestionsByFieldName[$FieldName][$FirstMatch[1]][]=
182 array("Name" => $FirstMatch[1],
183 "RemoveLink" => $RemovalLink);
184 }
185 }
186
187 # within each field, sort the suggestions alphabetically
188 foreach ($this->TreeSuggestionsByFieldName as &$Suggestion)
189 {
190 uasort($Suggestion, array($this, "FacetSuggestionSortCallback"));
191 }
192 foreach ($this->SuggestionsByFieldName as &$Suggestion)
193 {
194 uasort($Suggestion, array($this, "FacetSuggestionSortCallback"));
195 }
196 }
197
202 public function GetSuggestionsByFieldName()
203 {
204 return $this->SuggestionsByFieldName;
205 }
206
211 public function GetTreeSuggestionsByFieldName()
212 {
213 return $this->TreeSuggestionsByFieldName;
214 }
215
220 public function GetFieldsOpenByDefault()
221 {
222 return $this->FieldsOpenByDefault;
223 }
224
225
226 # ---- PRIVATE INTERFACE -------------------------------------------------
227
228 private $BaseLink;
229 private $DisplayedValues;
230 private $FieldsOpenByDefault;
231 private $MaxFacetsPerField;
232 private $SearchParams;
233 private $SuggestionsByFieldName;
234 private $TreeSuggestionsByFieldName;
235
241 private function GenerateFacetsForTree($Field, $Suggestions)
242 {
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))
247 {
248 $this->FieldsOpenByDefault[$Field->Name()] = TRUE;
249 $ParentValue = $CurrentValues[0];
250
251 # if this was an exact match, rather than a 'contains'
252 if (strpos($ParentValue, "=") === 0)
253 {
254 # cut off the leading '='
255 $ParentValue = substr($ParentValue, 1);
256 }
257
258 $FacetsForThisTree = $this->GenerateFacetsForSelectedTree(
259 $Field, $Suggestions, $ParentValue);
260
261 # if we have facets to display, but not too many of them
262 if ((count($FacetsForThisTree) > 0)
263 && (count($FacetsForThisTree) <= $this->MaxFacetsPerField))
264 {
265 # and append the required 'remove current' link
266 $RemovalLink = $this->RemoveTermFromSearchURL($Field, $CurrentValues);
267 $FacetsForThisTree[] = array(
268 "Name" => $ParentValue,
269 "RemoveLink" => $RemovalLink);
270
271 # mark each value for this field displayed
272 foreach ($CurrentValues as $Val)
273 {
274 $this->DisplayedValues[$Field->Name()][$Val] = TRUE;
275 }
276
277 $this->TreeSuggestionsByFieldName[$Field->Name()] =
278 array($FacetsForThisTree);
279 }
280 }
281 else
282 {
283 # otherwise, list the toplevel options for this field
284 $FacetsForThisTree = $this->GenerateFacetsForUnselectedTree(
285 $Field, $Suggestions);
286
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))
291 {
292 $this->TreeSuggestionsByFieldName[$Field->Name()] =
293 $FacetsForThisTree;
294 }
295 }
296 }
297
306 private function GenerateFacetsForSelectedTree($Field, $Suggestions, $ParentValue)
307 {
308 $ParentSegments = explode(" -- ", $ParentValue);
309
310 # keep track of what has been shown
311 $DisplayedSegments = array();
312
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)
318 {
319 $ValueName = $ValueData["Name"];
320 $ValueCount = $ValueData["Count"];
321 $FieldSegments = explode(" -- ", $ValueName);
322
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)))
327 {
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);
332
333 # add the modified search URL to our list of facets for this field
334 $AdditionLink = $this->AddTermToSearchURL($Field, $ValueName);
335
336 $FacetsForThisTree[] = array(
337 "Name" => $LeafValue,
338 "AddLink" => $AdditionLink,
339 "Count" => $ValueCount);
340
341 # record value as having been displayed
342 $this->DisplayedValues[$FieldName][$LeafValue] = TRUE;
343 }
344 }
345
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)
350 {
351 $ValueName = $ValueData["Name"];
352 $ValueCount = $ValueData["Count"];
353
354 # get an array of the segments for this suggestion
355 $FieldSegments = explode(" -- ", $ValueName);
356
357 # skip segments that are along some other branch of this tree
358 if (!preg_match("/^".preg_quote($ParentValue." -- ", "/")
359 ."/", $ValueName))
360 {
361 continue;
362 }
363
364 # skip segments that aren't (grand)* children of the current parent.
365 if (count($FieldSegments) < count($ParentSegments))
366 {
367 continue;
368 }
369
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));
374
375 # reassemble this into a Classification string
376 $TargetName = implode(" -- ", $TargetSegments);
377
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
381 # now
382 if (!isset($DisplayedSegments[$TargetName]))
383 {
384 # as above, note that we've displayed this segment
385 # and add a modified search URL to our list of
386 # facets to display
387 $DisplayedSegments[$TargetName] = TRUE;
388 $LeafValue = array_pop($TargetSegments);
389 $AdditionLink = $this->AddTermToSearchURL($Field, $TargetName);
390 $FacetsForThisTree[] = array(
391 "Name" => $LeafValue,
392 "AddLink" => $AdditionLink);
393
394 # record value as having been displayed
395 $this->DisplayedValues[$FieldName][$LeafValue] = TRUE;
396 }
397 }
398
399 return $FacetsForThisTree;
400 }
401
409 private function GenerateFacetsForUnselectedTree($Field, $Suggestions)
410 {
411 $FacetsForThisTree = array();
412
413 # keep track of what has been already shown
414 $DisplayedSegments = array();
415
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 )
420 {
421 $ValueName = $ValueData["Name"];
422 $ValueCount = $ValueData["Count"];
423 $FieldSegments = explode(" -- ", $ValueName);
424
425 # if this is a top level field
426 if (count($FieldSegments) == 1)
427 {
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);
435
436 # record value as having been displayed
437 $this->DisplayedValues[$FieldName][$ValueName] = TRUE;
438 }
439 }
440
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)
449 {
450 $ValueName = $ValueData["Name"];
451 $FieldSegments = explode(" -- ", $ValueName);
452
453 # if none of our previous efforts have displayed this segment
454 if (!isset($DisplayedSegments[$FieldSegments[0]]))
455 {
456 # add it to our list
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"]);
464
465 # record value as having been displayed
466 $this->DisplayedValues[$FieldName][$FieldSegments[0]] = TRUE;
467 }
468 }
469
470 return $FacetsForThisTree;
471 }
472
479 private function GenerateFacetsForName($Field, $Suggestions)
480 {
481 # for option fields, bail when there are too many suggestions for this option
482 if (count($Suggestions) > $this->MaxFacetsPerField)
483 {
484 return;
485 }
486
487 # retrieve current search parameter values for field
488 $CurrentValues = $this->SearchParams->GetSearchStringsForField($Field);
489
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))
496 {
497 return;
498 }
499
500 # for each suggested value
501 $FieldName = $Field->Name();
502 foreach ($Suggestions as $ValueId => $ValueData)
503 {
504 $ValueName = $ValueData["Name"];
505
506 # if we have a current value that is selected for this search
507 if (in_array("=".$ValueName, $CurrentValues))
508 {
509 # note that this facet should be displayed and add a
510 # remove link for it
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"]);
518 }
519 else
520 {
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"]);
529 }
530
531 # record value as having been displayed
532 $this->DisplayedValues[$FieldName][$ValueName] = TRUE;
533 }
534 }
535
543 private function AddTermToSearchURL($Field, $Term)
544 {
545 # create our own copy of search parameters
546 $OurSearchParams = clone $this->SearchParams;
547
548 # if this is not a tree field type
549 if ($Field->Type() != MetadataSchema::MDFTYPE_TREE)
550 {
551 # retrieve subgroups from search parameters
552 $Subgroups = $OurSearchParams->GetSubgroups();
553
554 # find subgroup for this field
555 foreach ($Subgroups as $Group)
556 {
557 if (in_array($Field->Id(), $Group->GetFields()))
558 {
559 $Subgroup = $Group;
560 break;
561 }
562 }
563
564 # if subgroup found
565 if (isset($Subgroup))
566 {
567 # add term to subgroup
568 $Subgroup->AddParameter($Term, $Field);
569 }
570 else
571 {
572 # create new subgroup
573 $Subgroup = new SearchParameterSet();
574
575 # set logic for new subgroup
576 $Subgroup->Logic($Field->SearchGroupLogic());
577
578 # add term to subgroup
579 $Subgroup->AddParameter($Term, $Field);
580
581 # add subgroup to search parameters
582 $OurSearchParams->AddSet($Subgroup);
583 }
584 }
585 else
586 {
587 # add specified term to search parameters
588 $OurSearchParams->RemoveParameter(NULL, $Field);
589 $Subgroup = new SearchParameterSet();
590 $Subgroup->Logic("OR");
591 $Subgroup->AddParameter(array(
592 "=".$Term, "^".$Term." -- "), $Field);
593 $OurSearchParams->AddSet($Subgroup);
594 }
595
596 # build new URL with revised search parameters
597 $Url = implode("&amp;", array_filter(array(
598 $this->BaseLink, $OurSearchParams->UrlParameterString())));
599
600 # return new URL to caller
601 return $Url;
602 }
603
611 private function RemoveTermFromSearchURL($Field, $Terms)
612 {
613 # create our own copy of search parameters with specified parameter removed
614 $OurSearchParams = clone $this->SearchParams;
615
616 if ($Field->Type() == MetadataSchema::MDFTYPE_TREE)
617 {
618 # pull out the old setting
619 $OldSetting = $OurSearchParams->GetSearchStringsForField($Field);
620 $OurSearchParams->RemoveParameter($Terms, $Field);
621
622 # if we had an exact + prefix match for a specified level of the tree,
623 # move up one level
624 if (count($OldSetting) == 2 &&
625 preg_match('/^=(.*)$/', $OldSetting[0], $FirstMatch) &&
626 preg_match('/^\\^(.*) -- $/', $OldSetting[1], $SecondMatch) &&
627 $FirstMatch[1] == $SecondMatch[1] )
628 {
629 # explode out the segments
630 $FieldSegments = explode(" -- ", $FirstMatch[1]);
631
632 # remove the last one
633 array_pop($FieldSegments);
634
635 # if we have any segments left
636 if (count($FieldSegments))
637 {
638 # construct revised setting
639 $NewSetting = implode(" -- ", $FieldSegments);
640 $Subset = new SearchParameterSet();
641 $Subset->Logic("OR");
642 $Subset->AddParameter(
643 array("=".$NewSetting, "^".$NewSetting." -- "),
644 $Field);
645 $OurSearchParams->AddSet($Subset);
646 }
647 }
648 }
649 else
650 {
651 $OurSearchParams->RemoveParameter($Terms, $Field);
652 }
653
654 # build new URL with revised search parameters
655 $Url = implode("&amp;", array_filter(array(
656 $this->BaseLink, $OurSearchParams->UrlParameterString())));
657
658 # return new URL to caller
659 return $Url;
660 }
661
668 private function FacetSuggestionSortCallback($A, $B)
669 {
670 if (isset($A["Name"]))
671 {
672 return strcmp($A["Name"], $B["Name"]);
673 }
674 else
675 {
676 $CountA = count($A);
677 $CountB = count($B);
678 return ($CountA == $CountB) ? 0
679 : (($CountA < $CountB) ? -1 : 1);
680 }
681 }
682 }
SearchFacetUI\GetFieldsOpenByDefault
GetFieldsOpenByDefault()
Retrieve which fields should be initially open in facet UI.
Definition: SearchFacetUI.php:220
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:202
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:211
SPTSearchEngine\GetResultFacets
static GetResultFacets($SearchResults, $User)
Generate a list of suggested additional search terms that can be used for faceted searching...
Definition: SPTSearchEngine.php:703
SearchFacetUI
SearchFacetUI supports the generation of a user interface for faceted search, by taking the search pa...
Definition: SearchFacetUI.php:15