-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathfetch-search-results.php
811 lines (691 loc) · 30.7 KB
/
fetch-search-results.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
<?php
/*
* Return markup with a series of search results (fossil calibrations), based on the POSTed query
*
* TODO: Support different response types: JSON? others?
*/
// open and load site variables
require('../config.php');
// build search object from GET vars or other inputs (eg, a saved-query ID)
include('build-search-query.php');
// Quick test for non-empty string
function isNullOrEmptyString($str){
return (!isset($str) || ($str == null) || trim($str)==='');
}
// test for active filter (by name)
function filterIsActive( $fullFilterName ) {
global $search;
if (in_array($fullFilterName, $search['HiddenFilters'])) return false;
if (in_array($fullFilterName, $search['BlockedFilters'])) return false;
return true;
}
// we need consistent number formatting to get natural sorting to work
function sortableNumber( $number ) {
return number_format($number, 2, '.', '');
}
$responseType = $search['ResponseType']; // HTML | JSON | ??
/* TODO: page or limit results, eg,
* $search['ResultsRange'] = "1-10"
* $search['ResultsRange'] = "21-40"
*/
// keep a master list of results
$searchResults = null;
// gather smaller result sets along the way, use to filter $searchResults
$tempResults = null;
// whip up a comparison function to use when getting the intersection of two arrays
function compareCalibrationResults($cal_A, $cal_B) {
if ($cal_A['CalibrationID'] > $cal_B['CalibrationID']) {
return 1;
}
if ($cal_A['CalibrationID'] == $cal_B['CalibrationID']) {
return 0;
}
return -1;
}
// keep track of how many possible matches there are for each result (based on search tools used)
$possibleMatches = 0;
/* TODO: If the requested sort doesn't make sense given the search type(s), apply
* some simple rules to override it.
*/
$forcedSort = null;
// connect to mySQL server and select the Fossil Calibration database (using newer 'msqli' interface)
$mysqli = new mysqli($SITEINFO['servername'],$SITEINFO['UserName'], $SITEINFO['password'], 'FossilCalibration');
// if search is a total bust (no results), we'll clear all search widgets using Javascript
$clearFailedSearch = false;
/*
* Building top-level search logic here for now, possibly move this into stored procedure later..?
*/
$showDefaultSearch = true; // TODO: improve logic for this as more filters are implemented
// apply each included search type in turn, then weigh/consolidate its results?
// simple text search; compare to misc titles, text data, and taxa(?)
// TODO: if a name resolves to a taxon, should it become an implicit tip-taxa or clade search?
if (!empty($search['SimpleSearch'])) {
$showDefaultSearch = false;
// break text into tokens (split on commas or whitespace, but respected quoted phrases)
// see http://fr2.php.net/manual/en/function.preg-split.php#92632
$search_expression = $search['SimpleSearch']; // eg, "apple bear \"Tom Cruise\" or 'Mickey Mouse' another word";
$searchTerms = preg_split("/[\s,]*\\\"([^\\\"]+)\\\"[\s,]*|" . "[\s,]*'([^']+)'[\s,]*|" . "[\s,]+/", $search_expression, 0, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
?><div class="search-details">SIMPLE SEARCH TERMS:<br/>
<pre><?= htmlentities(print_r($searchTerms, TRUE)); ?></pre></div><?
/* TODO: IF a term resolves as a geological period, copy it to that filter?
IF geological-time filter is not already being used
IF geological-time filter is not already blocked
*/
/* TODO: IF a term resolves to a taxon, copy it to tip-taxa?
IF tip-taxa filter is not already being used
IF tip-taxa filter is not already blocked
Copy the FIRST TWO matching terms found to taxa A and B, ignore others
$multitree_id_A = nameToMultitreeID($search['FilterByTipTaxa']['TaxonA']);
$multitree_id_B = nameToMultitreeID($search['FilterByTipTaxa']['TaxonB']);
*/
/* Search for each term (keeping tally for relevance score) in:
* > calibration node name
* > its publication description (full reference)
* > associated fossils (ID, taxon, collection?)
* > geological time?
* > implied tip-taxa search?
* > implied clade search?
* > phylogenetic publication (lcf.PhyloPub => publications)?
* > fossil publication (f.FossilPub => publications)?
* > fossil locality (f.LocalityID => localities)?
*/
$query="SELECT c.CalibrationID FROM calibrations AS c
LEFT OUTER JOIN publications AS p ON p.PublicationID = c.NodePub
LEFT OUTER JOIN Link_CalibrationFossil AS lcf ON lcf.CalibrationID = c.CalibrationID
LEFT OUTER JOIN fossils AS f ON f.FossilID = lcf.FossilID
WHERE (
c.NodeName LIKE CONCAT('%', ?, '%') OR
c.MinAgeExplanation LIKE CONCAT('%', ?, '%') OR
c.MaxAgeExplanation LIKE CONCAT('%', ?, '%') OR
p.ShortName LIKE CONCAT('%', ?, '%') OR
p.FullReference LIKE CONCAT('%', ?, '%') OR
p.DOI LIKE CONCAT('%', ?, '%') OR
lcf.Species LIKE CONCAT('%', ?, '%') OR
lcf.PhyJustification LIKE CONCAT('%', ?, '%') OR
f.CollectionAcro LIKE CONCAT('%', ?, '%') OR
f.CollectionNumber LIKE CONCAT('%', ?, '%')
)".
// non-admin users should only see *Published* calibrations
((isset($_SESSION['IS_ADMIN_USER']) && ($_SESSION['IS_ADMIN_USER'] == true)) ? '' :
" AND c.PublicationStatus = 4"
);
?><div class="search-details">SIMPLE-SEARCH TEMPLATE:<br/><?= htmlentities(print_r($query, TRUE)); ?></div><?
// use mysqli prepared statement to prevent SQL injection
$stmt=mysqli_prepare($mysqli, $query) or die ('Error in preparing template: '.$query.'|'. mysqli_error($mysqli));
$termPosition = 0;
foreach($searchTerms as $term) {
$matching_calibration_ids = array();
$possibleMatches++;
$termPosition++;
$tempResults = array();
// bind paramter (several times)
mysqli_stmt_bind_param($stmt, "ssssssssss", $term, $term, $term, $term, $term, $term, $term, $term, $term, $term);
// execute query
mysqli_stmt_execute($stmt);
mysqli_stmt_bind_result($stmt, $stmtCalibrationID);
// TODO: sort/sift from all the results lists above
while(mysqli_stmt_fetch($stmt)) {
$matching_calibration_ids[] = $stmtCalibrationID;
}
if (count($matching_calibration_ids) > 0) {
addCalibrations( $tempResults, $matching_calibration_ids, Array('relationship' => "03-MATCHES-TERM-$termPosition", 'relevance' => 1.0) );
}
if (is_array($searchResults)) {
$searchResults = array_uintersect( $searchResults, $tempResults, 'compareCalibrationResults' );
} else {
$searchResults = $tempResults;
}
}
// close statement (prepare for next time)
mysqli_stmt_close($stmt);
}
// tip-taxon search, using one or two taxa...
if (filterIsActive('FilterByTipTaxa')) {
if (empty($search['FilterByTipTaxa']['TaxonA']) && empty($search['FilterByTipTaxa']['TaxonB'])) {
// no taxa specified, bail out now
} else if (!empty($search['FilterByTipTaxa']['TaxonA']) && !empty($search['FilterByTipTaxa']['TaxonB'])) {
?><div class="search-details">2 TAXA SUBMITTED</div><?
// both taxa were specified...
$showDefaultSearch = false;
$possibleMatches += 2;
$tempResults = array();
?><div class="search-details">Starting result count: <?= count($searchResults) ?></div><?
/*
* Check for associated calibrations ("direct hits" and "near misses") based on related multitree IDs
*/
// resolve taxon multitree IDs
$multitree_id_A = nameToMultitreeID($search['FilterByTipTaxa']['TaxonA']);
$multitree_id_B = nameToMultitreeID($search['FilterByTipTaxa']['TaxonB']);
// check MRCA (common ancestor)
$multitree_id_MRCA = getMultitreeIDForMRCA( $multitree_id_A, $multitree_id_B );
?><div class="search-details">MRCA: <?= $multitree_id_MRCA ?> <? if (empty($multitree_id_MRCA)) { ?>EMPTY<? } ?> <? if ($multitree_id_MRCA == null) { ?>NULL<? } ?></div><?
// NOTE that if no MRCA was found, we still pass a one-item array to addAssociatedCalibrations()
addAssociatedCalibrations( $tempResults, Array($multitree_id_MRCA), Array('relationship' => '10-COMMON-ANCESTOR', 'relevance' => 1.0) );
?><div class="search-details">Temp result count: <?= count($tempResults) ?></div><?
// check director ancestors of A or B (includes the tip taxa)
$multitree_id_ancestors_A = getAllMultitreeAncestors( $multitree_id_A );
?><div class="search-details">ANCESTORS-A: <?= implode(", ", $multitree_id_ancestors_A) ?></div><?
addAssociatedCalibrations( $tempResults, $multitree_id_ancestors_A, Array('relationship' => '09-ANCESTOR-A', 'relevance' => 1.0) );
?><div class="search-details">Temp result count: <?= count($tempResults) ?></div><?
$multitree_id_ancestors_B = getAllMultitreeAncestors( $multitree_id_B );
?><div class="search-details">ANCESTORS-B: <?= implode(", ", $multitree_id_ancestors_B) ?></div><?
addAssociatedCalibrations( $tempResults, $multitree_id_ancestors_B, Array('relationship' => '08-ANCESTOR-B', 'relevance' => 1.0) );
?><div class="search-details">Temp result count: <?= count($tempResults) ?></div><?
// TODO: check all within clade of MRCA
// addAssociatedCalibrations( $tempResults, $multitree_id_clade_members, Array('relationship' => '04-MRCA-CLADE', 'relevance' => 0.25) );
// TODO: check all neighbors of MRCA
// addAssociatedCalibrations( $tempResults, $multitree_id_mrca_neighbors, Array('relationship' => '06-MRCA-NEIGHBOR', 'relevance' => 0.1) );
// TODO: check all neighbors of direct ancestors of A or B
// addAssociatedCalibrations( $tempResults, $multitree_id_ancestor_neighbors, Array('relationship' => '05-ANCESTOR-NEIGHBOR', 'relevance' => 0.1) );
if (is_array($searchResults)) {
$searchResults = array_uintersect( $searchResults, $tempResults, 'compareCalibrationResults' );
} else {
$searchResults = $tempResults;
}
} else {
?><div class="search-details">1 TAXON SUBMITTED</div><?
// just one taxon was specified
$showDefaultSearch = false;
$possibleMatches++;
$tempResults = array();
$specifiedTaxon = empty($search['FilterByTipTaxa']['TaxonA']) ? 'B' : 'A';
/*
* Check for associated calibrations ("direct hits" and "near misses") based on related multitree IDs
*/
// resolve taxon multitree ID
$multitree_id = nameToMultitreeID($search['FilterByTipTaxa']['Taxon'.$specifiedTaxon]);
// check its direct ancestors (includes the tip taxon)
$multitree_id_ancestors = getAllMultitreeAncestors( $multitree_id );
?><div class="search-details">ANCESTORS-<?= $specifiedTaxon ?>: <?= implode(", ", $multitree_id_ancestors) ?></div><?
addAssociatedCalibrations( $tempResults, $multitree_id_ancestors, Array('relationship' => ($specifiedTaxon == 'A' ? '09-ANCESTOR-A' : '08-ANCESTOR-B'), 'relevance' => 1.0) );
// TODO: check all neighbors of direct ancestors
// addAssociatedCalibrations( $tempResults, $multitree_id_ancestor_neighbors, Array('relationship' => '05-ANCESTOR-NEIGHBOR', 'relevance' => 0.2) );
if (is_array($searchResults)) {
$searchResults = array_uintersect( $searchResults, $tempResults, 'compareCalibrationResults' );
} else {
$searchResults = $tempResults;
}
}
}
// search within a named clade
if (filterIsActive('FilterByClade')) {
if (empty($search['FilterByClade'])) {
// no clade specified, bail out now
} else {
?><div class="search-details">CLADE SUBMITTED: <?= htmlspecialchars($search['FilterByClade']) ?></div><?
// search within this clade
$showDefaultSearch = false;
$possibleMatches++;
$tempResults = array();
/*
* Check for associated calibrations ("direct hits" and "near misses") based on related multitree IDs
*
* REMINDER: Prior versions of this file used a different approach, checking all clade members(!). This was
* painfully slow, esp. for large clades like Eukaryota, but it might contain some lessons if the logic above
* starts to crawl with many calibrations added.
*/
// resolve clade multitree ID
$clade_root_multitree_id = nameToMultitreeID($search['FilterByClade']);
// grab calibrations using our pre-built fast index
$matching_calibration_ids = getAllCalibrationsInClade($clade_root_multitree_id);
addCalibrations( $tempResults, $matching_calibration_ids, Array('relationship' => '07-CLADE-MEMBER', 'relevance' => 1.0) );
if (is_array($searchResults)) {
$searchResults = array_uintersect( $searchResults, $tempResults, 'compareCalibrationResults' );
} else {
$searchResults = $tempResults;
}
}
}
// filtering results by minimum and/or maximum age
if (filterIsActive('FilterByAge')) {
if (empty($search['FilterByAge']['MinAge']) && empty($search['FilterByAge']['MaxAge'])) {
// no ages specified, bail out now
} else if (!empty($search['FilterByAge']['MinAge']) && !empty($search['FilterByAge']['MaxAge'])) {
?><div class="search-details">MIN AND MAX AGES SUBMITTED</div><?
// search within this clade
$showDefaultSearch = false;
$possibleMatches++;
$tempResults = array();
/*
* Check for calibrations within the specified age ranage. NOTE that we should check
* both age bounds, as a sanity check in case only one was entered ("blank" ranges will appear as 0).
*/
$matching_calibration_ids = array();
$query="SELECT CalibrationID FROM calibrations WHERE
(MinAge >= ? OR MinAge = 0) AND (MaxAge >= ? OR MaxAge = 0)
AND (MinAge <= ? OR MinAge = 0) AND (MaxAge <= ? OR MaxAge = 0)
".
// non-admin users should only see *Published* calibrations
((isset($_SESSION['IS_ADMIN_USER']) && ($_SESSION['IS_ADMIN_USER'] == true)) ? '' :
" AND PublicationStatus = 4"
);
?><div class="search-details"><?= $query ?></div><?
$stmt=mysqli_prepare($mysqli, $query) or die ('Error in preparing template: '.$query.'|'. mysqli_error($mysqli));
mysqli_stmt_bind_param($stmt, "iiii", $search['FilterByAge']['MinAge'], $search['FilterByAge']['MinAge'], $search['FilterByAge']['MaxAge'], $search['FilterByAge']['MaxAge']);
mysqli_stmt_execute($stmt);
mysqli_stmt_bind_result($stmt, $stmtCalibrationID);
// sort/sift from all the results lists above
while(mysqli_stmt_fetch($stmt)) {
$matching_calibration_ids[] = $stmtCalibrationID;
}
if (count($matching_calibration_ids) > 0) {
addCalibrations( $tempResults, $matching_calibration_ids, Array('relationship' => '02-MATCHES-AGE', 'relevance' => 1.0) );
}
if (is_array($searchResults)) {
$searchResults = array_uintersect( $searchResults, $tempResults, 'compareCalibrationResults' );
} else {
$searchResults = $tempResults;
}
} else {
// just one age was specified
$showDefaultSearch = false;
$possibleMatches++;
$tempResults = array();
$specifiedAge = empty($search['FilterByAge']['MinAge']) ? 'MaxAge' : 'MinAge';
?><div class="search-details">1 AGE SUBMITTED (<?= $specifiedAge ?>)</div><?
/*
* Check for calibrations that are newer (or older) than the age specified. NOTE that we should check
* both age bounds, as a sanity check in case only one was entered ("blank" ranges will appear as 0).
*/
$matching_calibration_ids = array();
if ($specifiedAge == 'MinAge') {
$query="SELECT CalibrationID FROM calibrations".
" WHERE (MinAge >= ? OR MinAge = 0) AND (MaxAge >= ? OR MaxAge = 0)".
// non-admin users should only see *Published* calibrations
((isset($_SESSION['IS_ADMIN_USER']) && ($_SESSION['IS_ADMIN_USER'] == true)) ? '' :
" AND PublicationStatus = 4"
);
$stmt=mysqli_prepare($mysqli, $query) or die ('Error in preparing template: '.$query.'|'. mysqli_error($mysqli));
mysqli_stmt_bind_param($stmt, "ii", $search['FilterByAge']['MinAge'], $search['FilterByAge']['MinAge']);
} else {
$query="SELECT CalibrationID FROM calibrations".
" WHERE (MinAge <= ? OR MinAge = 0) AND (MaxAge <= ? OR MaxAge = 0)".
// non-admin users should only see *Published* calibrations
((isset($_SESSION['IS_ADMIN_USER']) && ($_SESSION['IS_ADMIN_USER'] == true)) ? '' :
" AND PublicationStatus = 4"
);
$stmt=mysqli_prepare($mysqli, $query) or die ('Error in preparing template: '.$query.'|'. mysqli_error($mysqli));
mysqli_stmt_bind_param($stmt, "ii", $search['FilterByAge']['MaxAge'], $search['FilterByAge']['MaxAge']);
}
mysqli_stmt_execute($stmt);
?><div class="search-details"><?= $query ?></div><?
mysqli_stmt_bind_result($stmt, $stmtCalibrationID);
// sort/sift from all the results lists above
while(mysqli_stmt_fetch($stmt)) {
$matching_calibration_ids[] = $stmtCalibrationID;
}
if (count($matching_calibration_ids) > 0) {
addCalibrations( $tempResults, $matching_calibration_ids, Array('relationship' => '02-MATCHES-AGE', 'relevance' => 1.0) );
}
if (is_array($searchResults)) {
$searchResults = array_uintersect( $searchResults, $tempResults, 'compareCalibrationResults' );
} else {
$searchResults = $tempResults;
}
}
}
// filtering results by geological time
if (filterIsActive('FilterByGeologicalTime')) {
if (empty($search['FilterByGeologicalTime'])) {
// no time specified, bail out now
} else {
?><div class="search-details">GEOLOGICAL TIME SUBMITTED</div><?
// search within this period
$showDefaultSearch = false;
$possibleMatches++;
$tempResults = array();
/*
* Check for calibrations from the specified time period (or a more specific time)
* EXAMPLE: Searching for 'Quaternary,Holocene,' will match 'Quaternary,,'
* EXAMPLE: Searching for 'Quaternary,Holocene,' will match 'Quaternary,Holocene,'
* EXAMPLE: Searching for 'Quaternary,Holocene,Modern' will NOT match 'Quaternary,Holocene,' [SHOULD IT?]
* EXAMPLE: Searching for 'Neogene,Miocene,' will NOT match 'Neogene,Pliocene,'
* EXAMPLE: Searching for 'Neogene,Miocene,Langhian' will NOT match 'Neogene,Miocen,Tortonian'
*/
$matching_calibration_ids = array();
$query="SELECT CalibrationID FROM Link_CalibrationFossil WHERE FossilID IN
(SELECT FossilID FROM fossils WHERE LocalityID IN
(SELECT LocalityID FROM localities WHERE GeolTime IN
(SELECT GeolTimeID FROM geoltime WHERE CONCAT_WS(',', Period,Epoch,Age) LIKE CONCAT(?, '%'))))".
// non-admin users should only see *Published* calibrations
((isset($_SESSION['IS_ADMIN_USER']) && ($_SESSION['IS_ADMIN_USER'] == true)) ? '' :
" AND CalibrationID IN (SELECT CalibrationID FROM calibrations WHERE PublicationStatus = 4)"
);
?><div class="search-details"><?= $query ?></div><?
$stmt=mysqli_prepare($mysqli, $query) or die ('Error in preparing template: '.$query.'|'. mysqli_error($mysqli));
mysqli_stmt_bind_param($stmt, "s", $search['FilterByGeologicalTime']);
mysqli_stmt_execute($stmt);
mysqli_stmt_bind_result($stmt, $stmtCalibrationID);
// sort/sift from all the results lists above
while(mysqli_stmt_fetch($stmt)) {
$matching_calibration_ids[] = $stmtCalibrationID;
}
if (count($matching_calibration_ids) > 0) {
addCalibrations( $tempResults, $matching_calibration_ids, Array('relationship' => '01-MATCHES-GEOTIME', 'relevance' => 1.0) );
}
/*
* TODO: Give "partial credit" (0.5 relevance) for calibrations that match using a more broad geo-time (eg, Quaternary or Holocene, vs. Modern)?
*/
if (is_array($searchResults)) {
$searchResults = array_uintersect( $searchResults, $tempResults, 'compareCalibrationResults' );
} else {
$searchResults = $tempResults;
}
}
}
// IF no search tools were active and loaded, return the n results most recently added
if ($showDefaultSearch) {
?><div class="search-details">SHOWING DEFAULT SEARCH</div><?
$searchResults = array();
$matching_calibration_ids = array();
$query="SELECT c.CalibrationID FROM calibrations AS c".
// non-admin users should only see *Published* calibrations
((isset($_SESSION['IS_ADMIN_USER']) && ($_SESSION['IS_ADMIN_USER'] == true)) ? '' :
" WHERE c.PublicationStatus = 4"
)
." ORDER BY DateCreated DESC
LIMIT 10";
$result=mysqli_query($mysqli, $query) or die ('Error in query: '.$query.'|'. mysqli_error($mysqli));
while($row=mysqli_fetch_assoc($result)) {
$matching_calibration_ids[] = $row['CalibrationID'];
}
if (count($matching_calibration_ids) > 0) {
addCalibrations( $searchResults, $matching_calibration_ids, Array('relationship' => '00-NONE', 'relevance' => 0.0) );
}
}
// return these results in the requested format
if ($responseType == 'JSON') {
echo json_encode($searchResults);
return;
}
/* ?><h3>FINAL: <?= count($searchResults) ?> results</h3><? */
function getRelationshipFromResult( $result, $relType ) {
foreach($result['qualifiers'] as $qual) {
/*?><pre class="search-details" style="color: red;">getRelationshipFromResult: <?= htmlentities(print_r($qual, TRUE)); ?> results</pre><?*/
if ($qual['relationship'] == $relType) {
return $qual;
}
}
return null;
}
// still here? then build HTML markup for the results
if (count($searchResults) == 0) {
// clear all search text and filters in the new page
$clearFailedSearch = true;
?>
<p style="color: #c44; font-style: italic;">No matching calibrations found. Please try another search.</p>
<p style="color: #c44; font-style: italic;">
<b>Note: We've cleared any search text above and reset all the search filters at left.</b>
</p>
<? $usingCladisticFilters = (filterIsActive('FilterByTipTaxa') && !(empty($search['FilterByTipTaxa']['TaxonA']) && empty($search['FilterByTipTaxa']['TaxonB'])))
|| (filterIsActive('FilterByClade') && !(empty($search['FilterByClade'])));
if (!$usingCladisticFilters) { // ie, the only search was on "simple text" ?>
<p style="color: #c44; font-style: italic;">REMINDER: To search by <b>clade</b> or <b>taxa</b>, use the filters at left.</p>
<? }
} else {
// Each result may contain multiple qualifiers (for "hits" on
// ancestory, age, etc), but it can only be listed once. Choose
// the displayed relationship and relevance to show for each
// as $displayedRelationship, $displayedRelevance
foreach($searchResults as &$result) // by reference!
{
/* Choose relationship and relevance to display.
* NOTE that the hidden identifier for all relationships includes a numeric
* prefix for sorting purposes, based on totally subjective "importance".
12-DIRECT-MATCH [same as an entered taxon]
11-MRCA
10-COMMON-ANCESTOR
09-ANCESTOR-A
08-ANCESTOR-B
07-CLADE-MEMBER
06-MRCA-NEIGHBOR
05-ANCESTOR-NEIGHBOR
04-MRCA-CLADE
03-MATCHES-TERM-{#}
02-MATCHES-AGE
01-MATCHES-GEOTIME
00-NONE
*/
switch(count($result['qualifiers'])) {
case 0:
// this should never happen
$result['displayedRelationship'] = '00-NONE';
break;
case 1:
// simple result, copy values directly
$result['displayedRelationship'] = $result['qualifiers'][0]['relationship'];
break;
default:
// complex result, with more than one qualifier
/* Choose a relationship (and relevance score) based on rules of
precedence/interest, ie, some kinds of result are more interesting
than others:
1. TODO: "direct hits" on entered taxa [tip or clade search]
2. MRCA or any common ancestor [tip only]
3. ancestor of one taxon [tip only]
4. clade member [clade only]
5. TODO: neighboring calibrations [tip or clade]
6. no relationship [other search]
*/
if (getRelationshipFromResult($result, '09-ANCESTOR-A') && getRelationshipFromResult($result, '08-ANCESTOR-B')) {
// bump this to show as common ancestor
$result['displayedRelationship'] = '10-COMMON-ANCESTOR';
} else if (getRelationshipFromResult($result, '09-ANCESTOR-A')) {
$result['displayedRelationship'] = '09-ANCESTOR-A';
} else if (getRelationshipFromResult($result, '08-ANCESTOR-B')) {
$result['displayedRelationship'] = '08-ANCESTOR-B';
} else if (getRelationshipFromResult($result, '07-CLADE-MEMBER')) {
$result['displayedRelationship'] = '07-CLADE-MEMBER';
// TODO: add other remaining relationship types, in precedence shown above
} else {
// relevance is a weighted average, or highlighted score
$result['displayedRelationship'] = '00-NONE';
$result['displayedRelevance'] = sortableNumber(0);
}
}
// choose relevance by examining the search and weighting matches against the search tools used
if (count($result['qualifiers']) == 0) {
$result['displayedRelevance'] = sortableNumber(0.0);
} else {
/* Average the relevance for all qualifiers on this result, including zeroes for any
* missing matches (based on $search properties). This gives us an overall relevance
* score that should look about right.
*/
/* ?><pre class="search-details" style="color: red;">possibleMatches: <?= $possibleMatches ?></pre><? */
$relevanceScores = array();
foreach ($result['qualifiers'] as $qual) {
$relevanceScores[] = $qual['relevance'];
}
while(count($relevanceScores) < $possibleMatches) {
$relevanceScores[] = 0.0;
}
$result['displayedRelevance'] = sortableNumber(array_sum($relevanceScores) / count ($relevanceScores));
?><pre class="search-details" style="color: red;"><b>'<?= $result['NodeName'] ?>'</b>:<br/> displayedRelevance: <?= array_sum($relevanceScores) ?> / <?= count ($relevanceScores) ?> = <?= $result['displayedRelevance'] ?></pre><?
/* ?><pre class="search-details" style="color: red;"> scores: <?== htmlentities(print_r($relevanceScores, TRUE)); ?></pre><? */
}
}
unset($result); // IMPORTANT: because PHP is "special" and has bound $result to a reference above...
/* TEST of sort order for floating-point scores:
?><pre class="search-details" style="color: red;">strnatcmp(0, 1.0) = <?= strnatcmp(0, 1.0) ?></pre><?
?><pre class="search-details" style="color: red;">strnatcmp(0.5, 1.0) = <?= strnatcmp(0.5, 1.0) ?></pre><?
?><pre class="search-details" style="color: red;">strnatcmp(0.5, 0.25) = <?= strnatcmp(0.5, 0.25) ?></pre><?
?><pre class="search-details" style="color: red;">strnatcmp(0.5, 0.2) = <?= strnatcmp(0.5, 0.2) ?></pre><?
?><pre class="search-details" style="color: red;">strnatcmp(0.75, 0.25) = <?= strnatcmp(0.75, 0.25) ?></pre><?
?><pre class="search-details" style="color: red;">strnatcmp(1, 0.25) = <?= strnatcmp(1, 0.25) ?></pre><?
*/
// Do any final sorting for display, using visible (consolidated) relationship and relevance
switch($search['SortResultsBy']) {
case 'RELEVANCE_DESC':
$searchResults = columnSort($searchResults, array(
'displayedRelevance', 'desc',
'DateCreated', 'desc'
//'displayedRelationship', 'desc',
//'MinAge', 'desc'
));
break;
case 'RELATIONSHIP':
$searchResults = columnSort($searchResults, array(
'displayedRelationship', 'desc',
'displayedRelevance', 'desc',
'DateCreated', 'desc',
'MinAge', 'desc'
));
break;
case 'DATE_ADDED_DESC':
$searchResults = columnSort($searchResults, array(
'DateCreated', 'desc',
'displayedRelevance', 'desc',
'displayedRelationship', 'desc',
'MinAge', 'desc'
));
break;
case 'CALIBRATED_AGE_ASC':
$searchResults = columnSort($searchResults, array(
'MinAge', 'desc',
'displayedRelevance', 'desc',
'displayedRelationship', 'desc',
'DateCreated', 'desc'
));
break;
}
// Display the sorted list
foreach($searchResults as $result)
{
// print hidden diagnostic info
?>
<pre class="search-details" style="color: green;">
<?= htmlentities(print_r($result, TRUE)); ?>
</pre>
<?
$calibrationDisplayURL = "/Show_Calibration.php?CalibrationID=". $result['CalibrationID'];
// fetch detailed properties from this result
$displayedRelationship = $result['displayedRelationship'];
$displayedRelevance = $result['displayedRelevance'];
$minAge = floatval($result['MinAge']);
$maxAge = floatval($result['MaxAge']);
// PHP's floats are imprecise, so we should define what constitutes equality here
$epsilon = 0.0001;
?>
<div class="search-result">
<table class="qualifiers" border="0">
<tr>
<td width="30">
<? if ($displayedRelationship) {
// choose an appropriate "qualifier" icon for this result
$icon = null;
$label = null;
switch($displayedRelationship) {
// case '12-DIRECT-MATCH':
case '11-MRCA':
case '10-COMMON-ANCESTOR':
$icon = 'result-mrca.jpg'; // nearest common ancestor
$label = 'Common ancestor of A and B';
break;
case '09-ANCESTOR-A':
//$icon = 'result-ancestor1.jpg';
$icon = 'result-ancestor2.jpg';
$label = 'Ancestor of A';
break;
case '08-ANCESTOR-B':
//$icon = 'result-ancestor1.jpg';
$icon = 'result-ancestor2.jpg';
$label = 'Ancestor of B';
break;
case '07-CLADE-MEMBER':
$icon = 'result-member.jpg';
$label = 'Clade member';
break;
// case '06-MRCA-NEIGHBOR':
// case '05-ANCESTOR-NEIGHBOR':
case '04-MRCA-CLADE':
$icon = 'result-member.jpg';
$label = 'Member of ancestor clade';
break;
// see below for '03-MATCHES-TERM-{#}' (regex)
case '02-MATCHES-AGE':
$icon = 'result-neutral.jpg';
$label = 'Matches age filter';
break;
case '01-MATCHES-GEOTIME':
$icon = 'result-neutral.jpg';
$label = 'Matches geological time';
break;
case '00-NONE':
$icon = 'result-neutral.jpg';
$label = 'No clear relationship';
break;
default:
// TODO: add regex for '03-MATCHES-TERM-{#}'
// TODO: match other icons
//$icon = 'result-tip.jpg'; // tip taxon
$icon = 'result-neutral.jpg';
if (preg_match('/03-MATCHES-TERM-(\d+)/', $displayedRelationship, $matches)) {
$label = 'Matches search term '. $matches[1];
} else {
$label = $displayedRelationship;
}
}
?>
<img class="qualifier-icon" title="<?= $label ?>" src="/images/<?= $icon ?>" alt="<?= $label ?>" />
<?
} else { ?>
<? } ?>
</td>
<!--
<td width="*" title="Relevance based on all filters used">
<? if ($displayedRelevance) { ?>
<?= intval($displayedRelevance * 100) ?>% match
<? } else { ?>
<? } ?>
</td>
-->
<td width="100" title="Calibrated age range">
<? if(abs($minAge-$maxAge) < $epsilon) { ?>
<?= $minAge ?> Ma
<? } else if ($minAge && $maxAge) { ?>
<?= $minAge ?>–<?= $maxAge ?> Ma
<? } else if ($minAge) { ?>
> <?= $minAge ?> Ma
<? } else if ($maxAge){ ?>
< <?= $maxAge ?> Ma
<? } else { ?>
<? } ?>
</td>
<td width="120" title="Date entered into database">
Added <?= date("M d, Y", strtotime($result['DateCreated'])) ?>
</td>
</tr>
</table>
<a class="calibration-link" href="<?= $calibrationDisplayURL ?>">
<span class="name"><?= $result['NodeName'] ?></span>
<span class="citation">– from <?= $result['ShortName'] ?></span>
</a>
<br/>
<? // if there's an image mapped to this publication, show it
if ($result['image']) { ?>
<div class="optional-thumbnail" style="height: 60px;">
<a href="<?= $calibrationDisplayURL ?>">
<img src="/publication_image.php?id=<?= $result['PublicationID'] ?>" style="height: 60px;"
alt="<?= $result['image_caption'] ?>" title="<?= $result['image_caption'] ?>"
/></a>
</div>
<? } ?>
<div class="details">
<?= $result['FullReference'] ?>
<a class="more" style="display: block; text-align: right;" href="<?= $calibrationDisplayURL ?>">more »</a>
</div>
</div>
<? }
}
if (count($searchResults) > 10) // TODO?
{ ?>
<div style="text-align: right; border-top: 1px solid #ddd; font-size: 0.9em; padding-top: 2px;">
<a href="#">Show more results like this</a>
</div>
<? }
return;
?>