]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - lib/wikilens/RatingsDb.php
Remove commented code
[SourceForge/phpwiki.git] / lib / wikilens / RatingsDb.php
1 <?php
2
3 /*
4  * @author:  Dan Frankowski (wikilens group manager), Reini Urban (as plugin)
5  *
6  * TODO:
7  * - fix RATING_STORAGE = WIKIPAGE (dba, file)
8  * - fix smart caching
9  * - finish mysuggest.c (external engine with data from mysql)
10  * - add the various show modes (esp. TopN queries in PHP)
11  */
12 /*
13  CREATE TABLE rating (
14         dimension INT(4) NOT NULL,
15         raterpage INT(11) NOT NULL,
16         rateepage INT(11) NOT NULL,
17         ratingvalue FLOAT NOT NULL,
18         rateeversion INT(11) NOT NULL,
19         isPrivate ENUM('yes','no'),
20         tstamp TIMESTAMP(14) NOT NULL,
21         PRIMARY KEY (dimension, raterpage, rateepage)
22  );
23 */
24
25 // For other than SQL backends. dba + adodb SQL ratings are allowed but deprecated.
26 // We will probably drop this hack.
27 if (!defined('RATING_STORAGE'))
28     // for DATABASE_TYPE=dba and forced RATING_STORAGE=SQL we must use ADODB,
29     // but this is problematic.
30     define('RATING_STORAGE', $GLOBALS['request']->_dbi->_backend->isSQL() ? 'SQL' : 'WIKIPAGE');
31 //define('RATING_STORAGE','WIKIPAGE');   // not fully supported yet
32
33 // leave undefined for internal, slow php engine.
34 //if (!defined('RATING_EXTERNAL'))
35 //    define('RATING_EXTERNAL',PHPWIKI_DIR . 'suggest.exe');
36
37 // Dimensions
38 if (!defined('EXPLICIT_RATINGS_DIMENSION'))
39     define('EXPLICIT_RATINGS_DIMENSION', 0);
40 if (!defined('LIST_ITEMS_DIMENSION'))
41     define('LIST_ITEMS_DIMENSION', 1);
42 if (!defined('LIST_OWNER_DIMENSION'))
43     define('LIST_OWNER_DIMENSION', 2);
44 if (!defined('LIST_TYPE_DIMENSION'))
45     define('LIST_TYPE_DIMENSION', 3);
46
47 //TODO: split class into SQL and metadata backends
48 class RatingsDb extends WikiDB
49 {
50
51     function RatingsDb()
52     {
53         global $request;
54         $this->_dbi = &$request->_dbi;
55         $this->_backend = &$this->_dbi->_backend;
56         $this->dimension = null;
57         if (RATING_STORAGE == 'SQL') {
58             if (isa($this->_backend, 'WikiDB_backend_PearDB')) {
59                 $this->_sqlbackend = &$this->_backend;
60                 $this->dbtype = "PearDB";
61             } elseif (isa($this->_backend, 'WikiDB_backend_ADODOB')) {
62                 $this->_sqlbackend = &$this->_backend;
63                 $this->dbtype = "ADODB";
64             } else {
65                 include_once 'lib/WikiDB/backend/ADODB.php';
66                 // It is not possible to decouple a ref from the source again. (4.3.11)
67                 // It replaced the main request backend. So we don't initialize _sqlbackend before.
68                 //$this->_sqlbackend = clone($this->_backend);
69                 $this->_sqlbackend = new WikiDB_backend_ADODB($GLOBALS['DBParams']);
70                 $this->dbtype = "ADODB";
71             }
72             $this->iter_class = "WikiDB_backend_" . $this->dbtype . "_generic_iter";
73
74             extract($this->_sqlbackend->_table_names);
75             if (empty($rating_tbl)) {
76                 $rating_tbl = (!empty($GLOBALS['DBParams']['prefix'])
77                     ? $GLOBALS['DBParams']['prefix'] : '') . 'rating';
78                 $this->_sqlbackend->_table_names['rating_tbl'] = $rating_tbl;
79             }
80         } else {
81             $this->iter_class = "WikiDB_Array_PageIterator";
82         }
83     }
84
85     // this is a singleton.  It ensures there is only 1 ratingsDB.
86     function & getTheRatingsDb()
87     {
88         static $_theRatingsDb;
89
90         if (!isset($_theRatingsDb)) {
91             $_theRatingsDb = new RatingsDb();
92         }
93         //echo "rating db is $_theRatingsDb";
94         return $_theRatingsDb;
95     }
96
97
98 /// *************************************************************************************
99 // FIXME
100 // from Reini Urban's RateIt plugin
101     function addRating($rating, $userid, $pagename, $dimension)
102     {
103         if (RATING_STORAGE == 'SQL') {
104             $page = $this->_dbi->getPage($pagename);
105             $current = $page->getCurrentRevision();
106             $rateeversion = $current->getVersion();
107             $this->sql_rate($userid, $pagename, $rateeversion, $dimension, $rating);
108         } else {
109             $this->metadata_set_rating($userid, $pagename, $dimension, $rating);
110         }
111     }
112
113     function deleteRating($userid = null, $pagename = null, $dimension = null)
114     {
115         if (is_null($dimension)) $dimension = $this->dimension;
116         if (is_null($userid)) $userid = $this->userid;
117         if (is_null($pagename)) $pagename = $this->pagename;
118         if (RATING_STORAGE == 'SQL') {
119             $this->sql_delete_rating($userid, $pagename, $dimension);
120         } else {
121             $this->metadata_set_rating($userid, $pagename, $dimension, -1);
122         }
123     }
124
125     function getRating($userid = null, $pagename = null, $dimension = null)
126     {
127         if (RATING_STORAGE == 'SQL') {
128             $ratings_iter = $this->sql_get_rating($dimension, $userid, $pagename);
129             if ($rating = $ratings_iter->next() and isset($rating['ratingvalue'])) {
130                 return $rating['ratingvalue'];
131             } else
132                 return false;
133         } else {
134             return $this->metadata_get_rating($userid, $pagename, $dimension);
135         }
136     }
137
138     function getUsersRated($dimension = null, $orderby = null)
139     {
140         if (is_null($dimension)) $dimension = $this->dimension;
141         //if (is_null($userid))    $userid = $this->userid;
142         //if (is_null($pagename))  $pagename = $this->pagename;
143         if (RATING_STORAGE == 'SQL') {
144             $ratings_iter = $this->sql_get_users_rated($dimension, $orderby);
145             if ($rating = $ratings_iter->next() and isset($rating['ratingvalue'])) {
146                 return $rating['ratingvalue'];
147             } else
148                 return false;
149         } else {
150             return $this->metadata_get_users_rated($dimension, $orderby);
151         }
152     }
153
154     /**
155      * Get ratings.
156      *
157      * @param dimension  The rating dimension id.
158      *                   Example: 0
159      *                   [optional]
160      *                   If this is null (or left off), the search for ratings
161      *                   is not restricted by dimension.
162      *
163      * @param rater  The page id of the rater, i.e. page doing the rating.
164      *               This is a Wiki page id, often of a user page.
165      *               Example: "DanFr"
166      *               [optional]
167      *               If this is null (or left off), the search for ratings
168      *               is not restricted by rater.
169      *               TODO: Support an array
170      *
171      * @param ratee  The page id of the ratee, i.e. page being rated.
172      *               Example: "DudeWheresMyCar"
173      *               [optional]
174      *               If this is null (or left off), the search for ratings
175      *               is not restricted by ratee.
176      *
177      * @param orderby An order-by clause with fields and (optionally) ASC
178      *                or DESC.
179      *               Example: "ratingvalue DESC"
180      *               [optional]
181      *               If this is null (or left off), the search for ratings
182      *               has no guaranteed order
183      *
184      * @param pageinfo The type of page that has its info returned (i.e.,
185      *               'pagename', 'hits', and 'pagedata') in the rows.
186      *               Example: "rater"
187      *               [optional]
188      *               If this is null (or left off), the info returned
189      *               is for the 'ratee' page (i.e., thing being rated).
190      *
191      * @return DB iterator with results
192      */
193     function get_rating($dimension = null, $rater = null, $ratee = null,
194                         $orderby = null, $pageinfo = "ratee")
195     {
196         if (RATING_STORAGE == 'SQL') {
197             $ratings_iter = $this->sql_get_rating($dimension, $rater, $pagename);
198             if ($rating = $ratings_iter->next() and isset($rating['ratingvalue'])) {
199                 return $rating['ratingvalue'];
200             } else
201                 return false;
202         } else {
203             return $this->metadata_get_rating($rater, $pagename, $dimension);
204         }
205     }
206
207     /* UR: What is this for? NOT USED!
208        Maybe the list of users (ratees) who rated on this page.
209      */
210     function get_users_rated($dimension = null, $pagename = null, $orderby = null)
211     {
212         if (RATING_STORAGE == 'SQL') {
213             $ratings_iter = $this->sql_get_users_rated($dimension, $pagename, $orderby);
214             // iter as userid
215             $users = array();
216             while ($rating = $ratings_iter->next()) {
217                 $users[] = $rating['userid'];
218             }
219             return $users;
220         } else {
221             return $this->metadata_get_users_rated($dimension, $pagename, $orderby);
222         }
223     }
224
225     /**
226      * Like get_rating(), but return a WikiDB_PageIterator
227      * FIXME!
228      */
229     function get_rating_page($dimension = null, $rater = null, $ratee = null,
230                              $orderby = null, $pageinfo = "ratee")
231     {
232         if (RATING_STORAGE == 'SQL') {
233             return $this->sql_get_rating($dimension, $rater, $ratee, $orderby, $pageinfo);
234         } else {
235             // empty dummy iterator
236             $pages = array();
237             return new WikiDB_Array_PageIterator($pages);
238         }
239     }
240
241     /**
242      * Delete a rating.
243      *
244      * @param rater  The page id of the rater, i.e. page doing the rating.
245      *               This is a Wiki page id, often of a user page.
246      * @param ratee  The page id of the ratee, i.e. page being rated.
247      * @param dimension  The rating dimension id.
248      *
249      * @access public
250      *
251      * @return true upon success
252      */
253     function delete_rating($rater, $ratee, $dimension)
254     {
255         if (RATING_STORAGE == 'SQL') {
256             $this->sql_delete_rating($rater, $ratee, $dimension);
257         } else {
258             $this->metadata_set_rating($rater, $ratee, $dimension, -1);
259         }
260     }
261
262     /**
263      * Rate a page.
264      *
265      * @param rater  The page id of the rater, i.e. page doing the rating.
266      *               This is a Wiki page id, often of a user page.
267      * @param ratee  The page id of the ratee, i.e. page being rated.
268      * @param rateeversion  The version of the ratee page.
269      * @param dimension  The rating dimension id.
270      * @param rating The rating value (a float).
271      *
272      * @access public
273      *
274      * @return true upon success
275      */
276     function rate($rater, $ratee, $rateeversion, $dimension, $rating)
277     {
278         if (RATING_STORAGE == 'SQL') {
279             $page = $this->_dbi->getPage($pagename);
280             $current = $page->getCurrentRevision();
281             $rateeversion = $current->getVersion();
282             $this->sql_rate($userid, $pagename, $rateeversion, $dimension, $rating);
283         } else {
284             $this->metadata_set_rating($userid, $pagename, $dimension, $rating);
285         }
286     }
287
288     //function getUsersRated(){}
289
290 //*******************************************************************************
291     // TODO:
292     // Use wikilens/RatingsUser.php for the php methods.
293     //
294     // Old:
295     // Currently we have to call the "suggest" CGI
296     //   http://www-users.cs.umn.edu/~karypis/suggest/
297     // until we implement a simple recommendation engine.
298     // Note that "suggest" is only free for non-profit organizations.
299     // I am currently writing a binary CGI mysuggest using suggest, which loads
300     // data from mysql.
301     function getPrediction($userid = null, $pagename = null, $dimension = null)
302     {
303         if (is_null($dimension)) $dimension = $this->dimension;
304         if (is_null($userid)) $userid = $this->userid;
305         if (is_null($pagename)) $pagename = $this->pagename;
306
307         if (RATING_STORAGE == 'SQL') {
308             $dbh = &$this->_sqlbackend;
309             if (isset($pagename))
310                 $page = $dbh->_get_pageid($pagename);
311             else
312                 return 0;
313             if (isset($userid))
314                 $user = $dbh->_get_pageid($userid);
315             else
316                 return 0;
317         }
318         if (defined('RATING_EXTERNAL') and RATING_EXTERNAL) {
319             // how call mysuggest.exe? as CGI or natively
320             //$rating = HTML::Raw("<!--#include virtual=".RATING_ENGINE." -->");
321             $args = "-u$user -p$page -malpha"; // --top 10
322             if (isset($dimension))
323                 $args .= " -d$dimension";
324             $rating = passthru(RATING_EXTERNAL . " $args");
325         } else {
326             $rating = $this->php_prediction($userid, $pagename, $dimension);
327         }
328         return $rating;
329     }
330
331     /**
332      * Slow item-based recommendation engine, similar to suggest RType=2.
333      * Only the SUGGEST_EstimateAlpha part
334      * Take wikilens/RatingsUser.php for the php methods.
335      */
336     function php_prediction($userid = null, $pagename = null, $dimension = null)
337     {
338         if (is_null($dimension)) $dimension = $this->dimension;
339         if (is_null($userid)) $userid = $this->userid;
340         if (is_null($pagename)) $pagename = $this->pagename;
341         if (empty($this->buddies)) {
342             require_once 'lib/wikilens/RatingsUser.php';
343             require_once 'lib/wikilens/Buddy.php';
344             $user = RatingsUserFactory::getUser($userid);
345             $this->buddies = getBuddies($user, $GLOBALS['request']->_dbi);
346         }
347         return $user->knn_uu_predict($pagename, $this->buddies, $dimension);
348     }
349
350     function getNumUsers($pagename = null, $dimension = null)
351     {
352         if (is_null($dimension)) $dimension = $this->dimension;
353         if (is_null($pagename)) $pagename = $this->pagename;
354         if (RATING_STORAGE == 'SQL') {
355             $ratings_iter = $this->sql_get_rating($dimension, null, $pagename,
356                 null, "ratee");
357             return $ratings_iter->count();
358         } else {
359             if (!$pagename) return 0;
360             $page = $this->_dbi->getPage($pagename);
361             $data = $page->get('rating');
362             if (!empty($data[$dimension]))
363                 return count($data[$dimension]);
364             else
365                 return 0;
366         }
367     }
368
369     function getAvg($pagename = null, $dimension = null)
370     {
371         if (is_null($dimension)) $dimension = $this->dimension;
372         if (is_null($pagename)) $pagename = $this->pagename;
373         if (RATING_STORAGE == 'SQL') {
374             $dbi = &$this->_sqlbackend;
375             if (isset($pagename) || isset($dimension)) {
376                 $where = "WHERE";
377             }
378             if (isset($pagename)) {
379                 if (defined('FUSIONFORGE') and FUSIONFORGE) {
380                     $rateeid = $this->_sqlbackend->_get_pageid($pagename, true);
381                     $where .= " rateepage=$rateeid";
382                 } else {
383                     $raterid = $this->_sqlbackend->_get_pageid($pagename, true);
384                     $where .= " raterpage=$raterid";
385                 }
386             }
387             if (isset($dimension)) {
388                 if (isset($pagename)) $where .= " AND";
389                 $where .= " dimension=$dimension";
390             }
391             extract($dbi->_table_names);
392             if (defined('FUSIONFORGE') and FUSIONFORGE) {
393                 $query = "SELECT AVG(ratingvalue) as avg FROM $rating_tbl " . $where;
394             } else {
395                 $query = "SELECT AVG(ratingvalue) as avg FROM $rating_tbl r, $page_tbl p " . $where . " GROUP BY raterpage";
396             }
397             $result = $dbi->_dbh->query($query);
398             $iter = new $this->iter_class($this, $result);
399             $row = $iter->next();
400             return $row['avg'];
401         } else {
402             if (!$pagename) return 0;
403             $page = $this->_dbi->getPage($pagename);
404             $data = $page->get('rating');
405             if (!empty($data[$dimension]))
406                 // hash of userid => rating
407                 return array_sum(array_values($data[$dimension])) / count($data[$dimension]);
408             else
409                 return 0;
410         }
411     }
412
413 //*******************************************************************************
414
415     /**
416      * Get ratings.
417      *
418      * @param dimension  The rating dimension id.
419      *                   Example: 0
420      *                   [optional]
421      *                   If this is null (or left off), the search for ratings
422      *                   is not restricted by dimension.
423      *
424      * @param rater  The page id of the rater, i.e. page doing the rating.
425      *               This is a Wiki page id, often of a user page.
426      *               Example: "DanFr"
427      *               [optional]
428      *               If this is null (or left off), the search for ratings
429      *               is not restricted by rater.
430      *               TODO: Support an array
431      *
432      * @param ratee  The page id of the ratee, i.e. page being rated.
433      *               Example: "DudeWheresMyCar"
434      *               [optional]
435      *               If this is null (or left off), the search for ratings
436      *               is not restricted by ratee.
437      *               TODO: Support an array
438      *
439      * @param orderby An order-by clause with fields and (optionally) ASC
440      *                or DESC.
441      *               Example: "ratingvalue DESC"
442      *               [optional]
443      *               If this is null (or left off), the search for ratings
444      *               has no guaranteed order
445      *
446      * @param pageinfo The type of page that has its info returned (i.e.,
447      *               'pagename', 'hits', and 'pagedata') in the rows.
448      *               Example: "rater"
449      *               [optional]
450      *               If this is null (or left off), the info returned
451      *               is for the 'ratee' page (i.e., thing being rated).
452      *
453      * @return DB iterator with results
454      */
455     function sql_get_rating($dimension = null, $rater = null, $ratee = null,
456                             $orderby = null, $pageinfo = "ratee")
457     {
458         if (is_null($dimension)) $dimension = $this->dimension;
459         $result = $this->_sql_get_rating_result($dimension, $rater, $ratee, $orderby, $pageinfo);
460         return new $this->iter_class($this, $result);
461     }
462
463     function sql_get_users_rated($dimension = null, $pagename = null, $orderby = null)
464     {
465         if (is_null($dimension)) $dimension = $this->dimension;
466         $result = $this->_sql_get_rating_result($dimension, null, $pagename, $orderby, "rater");
467         return new $this->iter_class($this, $result);
468     }
469
470     // all users who rated this page resp if null all pages.. needed?
471     function metadata_get_users_rated($dimension = null, $pagename = null, $orderby = null)
472     {
473         if (is_null($dimension)) $dimension = $this->dimension;
474         $users = array();
475         if (!$pagename) {
476             // TODO: all pages?
477             return new WikiDB_Array_PageIterator($users);
478         }
479         $page = $this->_dbi->getPage($pagename);
480         $data = $page->get('rating');
481         if (!empty($data[$dimension])) {
482             //array($userid => (float)$rating);
483             return new WikiDB_Array_PageIterator(array_keys($data[$dimension]));
484         }
485         return new WikiDB_Array_PageIterator($users);
486     }
487
488     /**
489      * @access private
490      * @return result ressource, suitable to the iterator
491      */
492     function _sql_get_rating_result($dimension = null, $rater = null, $ratee = null,
493                                     $orderby = null, $pageinfo = "ratee")
494     {
495         // pageinfo must be 'rater' or 'ratee'
496         if (($pageinfo != "ratee") && ($pageinfo != "rater"))
497             return;
498         $dbi = &$this->_sqlbackend;
499         if (is_null($dbi))
500             return;
501         //$dbh = &$this->_dbi;
502         extract($dbi->_table_names);
503         $where = "WHERE r." . $pageinfo . "page = p.id";
504         if (isset($dimension)) {
505             $where .= " AND dimension=$dimension";
506         }
507         if (isset($rater)) {
508             $raterid = $dbi->_get_pageid($rater, true);
509             $where .= " AND raterpage=$raterid";
510         }
511         if (isset($ratee)) {
512             if (is_array($ratee)) {
513                 $where .= " AND (";
514                 for ($i = 0; $i < count($ratee); $i++) {
515                     $rateeid = $dbi->_get_pageid($ratee[$i], true);
516                     $where .= "rateepage=$rateeid";
517                     if ($i != (count($ratee) - 1)) {
518                         $where .= " OR ";
519                     }
520                 }
521                 $where .= ")";
522             } else {
523                 $rateeid = $dbi->_get_pageid($ratee, true);
524                 $where .= " AND rateepage=$rateeid";
525             }
526         }
527         $orderbyStr = "";
528         if (isset($orderby)) {
529             $orderbyStr = " ORDER BY " . $orderby;
530         }
531         if (isset($rater) or isset($ratee)) $what = '*';
532         // same as _get_users_rated_result()
533         else {
534             $what = 'DISTINCT p.pagename';
535             if ($pageinfo == 'rater')
536                 $what = 'DISTINCT p.pagename as userid';
537         }
538
539         $query = "SELECT $what"
540             . " FROM $rating_tbl r, $page_tbl p "
541             . $where
542             . $orderbyStr;
543         $result = $dbi->_dbh->query($query);
544         return $result;
545     }
546
547     /**
548      * Delete a rating.
549      *
550      * @param rater  The page id of the rater, i.e. page doing the rating.
551      *               This is a Wiki page id, often of a user page.
552      * @param ratee  The page id of the ratee, i.e. page being rated.
553      * @param dimension  The rating dimension id.
554      *
555      * @access public
556      *
557      * @return true upon success
558      */
559     function sql_delete_rating($rater, $ratee, $dimension)
560     {
561         //$dbh = &$this->_dbi;
562         $dbi = &$this->_sqlbackend;
563         extract($dbi->_table_names);
564
565         $dbi->lock();
566         $raterid = $dbi->_get_pageid($rater, true);
567         $rateeid = $dbi->_get_pageid($ratee, true);
568         $where = "WHERE raterpage=$raterid and rateepage=$rateeid";
569         if (isset($dimension)) {
570             $where .= " AND dimension=$dimension";
571         }
572         $dbi->_dbh->query("DELETE FROM $rating_tbl $where");
573         $dbi->unlock();
574         return true;
575     }
576
577     /**
578      * Rate a page.
579      *
580      * @param rater  The page id of the rater, i.e. page doing the rating.
581      *               This is a Wiki page id, often of a user page.
582      * @param ratee  The page id of the ratee, i.e. page being rated.
583      * @param rateeversion  The version of the ratee page.
584      * @param dimension  The rating dimension id.
585      * @param rating The rating value (a float).
586      *
587      * @access public
588      *
589      * @return true upon success
590      */
591     //               ($this->userid, $this->pagename, $page->version, $this->dimension, $rating);
592     function sql_rate($rater, $ratee, $rateeversion, $dimension, $rating)
593     {
594         $dbi = &$this->_sqlbackend;
595         extract($dbi->_table_names);
596         if (empty($rating_tbl))
597             $rating_tbl = $this->_dbi->getParam('prefix') . 'rating';
598
599         $dbi->lock();
600         $raterid = $dbi->_get_pageid($rater, true);
601         $rateeid = $dbi->_get_pageid($ratee, true);
602         assert($raterid);
603         assert($rateeid);
604         //mysql optimize: REPLACE if raterpage and rateepage are keys
605         $dbi->_dbh->query("DELETE from $rating_tbl WHERE dimension=$dimension AND raterpage=$raterid AND rateepage=$rateeid");
606         $where = "WHERE raterpage='$raterid' AND rateepage='$rateeid'";
607         $insert = "INSERT INTO $rating_tbl (dimension, raterpage, rateepage, ratingvalue, rateeversion)"
608             . " VALUES ('$dimension', $raterid, $rateeid, '$rating', '$rateeversion')";
609         $dbi->_dbh->query($insert);
610
611         $dbi->unlock();
612         return true;
613     }
614
615     function metadata_get_rating($userid, $pagename, $dimension)
616     {
617         if (!$pagename) return false;
618         $page = $this->_dbi->getPage($pagename);
619         $data = $page->get('rating');
620         if (!empty($data[$dimension][$userid]))
621             return (float)$data[$dimension][$userid];
622         else
623             return false;
624     }
625
626     function metadata_set_rating($userid, $pagename, $dimension, $rating = -1)
627     {
628         if (!$pagename) return false;
629         $page = $this->_dbi->getPage($pagename);
630         $data = $page->get('rating');
631         if ($rating == -1)
632             unset($data[$dimension][$userid]);
633         else {
634             if (empty($data[$dimension]))
635                 $data[$dimension] = array($userid => (float)$rating);
636             else
637                 $data[$dimension][$userid] = (float)$rating;
638         }
639         $page->set('rating', $data);
640     }
641
642 }
643
644 // Local Variables:
645 // mode: php
646 // tab-width: 8
647 // c-basic-offset: 4
648 // c-hanging-comment-ender-p: nil
649 // indent-tabs-mode: nil
650 // End: