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 # 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
196 public function GetTreeSuggestionsByFieldName()
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 }
SearchFacetUI\GetFieldsOpenByDefault
GetFieldsOpenByDefault()
Retrieve which fields should be initially open in facet UI.
Definition: SearchFacetUI.php:205
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:187
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:196
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