1<?php
2
3/**
4 *
5 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
6 * @author     Andreas Gohr <andi@splitbrain.org>
7 */
8
9/**
10 * Class syntax_plugin_data_cloud
11 */
12class syntax_plugin_data_cloud extends syntax_plugin_data_table
13{
14    /**
15     * will hold the data helper plugin
16     * @var $dthlp helper_plugin_data
17     */
18    public $dthlp;
19
20    /**
21     * Constructor. Load helper plugin
22     */
23    public function __construct()
24    {
25        $this->dthlp = plugin_load('helper', 'data');
26        if (!$this->dthlp) {
27            msg('Loading the data helper failed. Make sure the data plugin is installed.', -1);
28        }
29    }
30
31    /**
32     * What kind of syntax are we?
33     */
34    public function getType()
35    {
36        return 'substition';
37    }
38
39    /**
40     * What about paragraphs?
41     */
42    public function getPType()
43    {
44        return 'block';
45    }
46
47    /**
48     * Where to sort in?
49     */
50    public function getSort()
51    {
52        return 155;
53    }
54
55    /**
56     * Connect pattern to lexer
57     *
58     * @param $mode
59     */
60    public function connectTo($mode)
61    {
62        $this->Lexer->addSpecialPattern(
63            '----+ *datacloud(?: [ a-zA-Z0-9_]*)?-+\n.*?\n----+',
64            $mode,
65            'plugin_data_cloud'
66        );
67    }
68
69    /**
70     * Builds the SQL query from the given data
71     *
72     * @param array &$data instruction by handler
73     * @return bool|string SQL query or false
74     */
75    public function buildSQL(&$data)
76    {
77        $ckey = array_keys($data['cols']);
78        $ckey = $ckey[0];
79
80        $from = ' ';
81        $where = ' ';
82        $pagesjoin = '';
83        $tables = [];
84
85        $sqlite = $this->dthlp->getDB();
86        if (!$sqlite) return false;
87
88        $fields = ['pageid' => 'page', 'class' => 'class', 'title' => 'title'];
89        // prepare filters (no request filters - we set them ourselves)
90        if (is_array($data['filter']) && count($data['filter'])) {
91            $cnt = 0;
92
93            foreach ($data['filter'] as $filter) {
94                //Note: value is already escaped
95                $col = $filter['key'];
96                $closecompare = ($filter['compare'] == 'IN(' ? ')' : '');
97
98                if (preg_match('/^%(\w+)%$/', $col, $m) && isset($fields[$m[1]])) {
99                    $where .= " " . $filter['logic'] . " pages." . $fields[$m[1]] .
100                        " " . $filter['compare'] . " " . $filter['value'] . $closecompare;
101                    $pagesjoin = ' LEFT JOIN pages ON pages.pid = data.pid';
102                } else {
103                    // filter by hidden column?
104                    if (!$tables[$col]) {
105                        $tables[$col] = 'T' . (++$cnt);
106                        $from .= ' LEFT JOIN data AS ' . $tables[$col] . ' ON ' . $tables[$col] . '.pid = data.pid';
107                        $from .= ' AND ' . $tables[$col] . ".key = " . $sqlite->getPdo()->quote($col);
108                    }
109
110                    $where .= ' ' . $filter['logic'] . ' ' . $tables[$col] . '.value ' . $filter['compare'] .
111                        " " . $filter['value'] . $closecompare;
112                }
113            }
114        }
115
116        // build query
117        $sql = "SELECT data.value AS value, COUNT(data.pid) AS cnt
118                  FROM data $from $pagesjoin
119                 WHERE data.key = " . $sqlite->getPdo()->quote($ckey) . "
120                 $where
121              GROUP BY data.value";
122        if (isset($data['min'])) {
123            $sql .= ' HAVING cnt >= ' . $data['min'];
124        }
125        $sql .= ' ORDER BY cnt DESC';
126        if ($data['limit']) {
127            $sql .= ' LIMIT ' . $data['limit'];
128        }
129
130        return $sql;
131    }
132
133    protected $before_item = '<ul class="dataplugin_cloud %s">';
134    protected $after_item = '</ul>';
135    protected $before_val = '<li class="cl%s">';
136    protected $after_val = '</li>';
137
138    /**
139     * Create output or save the data
140     *
141     * @param $format
142     * @param Doku_Renderer $renderer
143     * @param $data
144     * @return bool
145     */
146    public function render($format, Doku_Renderer $renderer, $data)
147    {
148        global $ID;
149
150        if ($format != 'xhtml') return false;
151        if (is_null($data)) return false;
152        if (!$this->dthlp->ready()) return false;
153        $renderer->info['cache'] = false;
154
155        $sqlite = $this->dthlp->getDB();
156        if (!$sqlite) return false;
157
158        $ckey = array_keys($data['cols']);
159        $ckey = $ckey[0];
160
161        if (!isset($data['page'])) {
162            $data['page'] = $ID;
163        }
164
165        $this->dthlp->replacePlaceholdersInSQL($data);
166
167        // build cloud data
168        $rows = $sqlite->queryAll($data['sql']);
169        $min = 0;
170        $max = 0;
171        $tags = [];
172        foreach ($rows as $row) {
173            if (!$max) {
174                $max = $row['cnt'];
175            }
176            $min = $row['cnt'];
177            $tags[$row['value']]['cnt'] = $row['cnt'];
178            $tags[$row['value']]['value'] = $row['value'];
179        }
180        $this->cloudWeight($tags, $min, $max, 5);
181
182        // output cloud
183        $renderer->doc .= sprintf($this->before_item, hsc($data['classes']));
184        foreach ($tags as $tag) {
185            $tagLabelText = hsc($tag['value']);
186            if ($data['summarize'] == 1) {
187                $tagLabelText .= '<sub>(' . $tag['cnt'] . ')</sub>';
188            }
189
190            $renderer->doc .= sprintf($this->before_val, $tag['lvl']);
191            $renderer->doc .= '<a href="' .
192                wl($data['page'], $this->dthlp->getTagUrlparam($data['cols'][$ckey], $tag['value'])) .
193                '" title="' . sprintf($this->getLang('tagfilter'), hsc($tag['value'])) .
194                '" class="wikilink1">' . $tagLabelText . '</a>';
195            $renderer->doc .= $this->after_val;
196        }
197        $renderer->doc .= $this->after_item;
198        return true;
199    }
200
201    /**
202     * Create a weighted tag distribution
203     *
204     * @param $tags array ref The tags to weight ( tag => count)
205     * @param $min int      The lowest count of a single tag
206     * @param $max int      The highest count of a single tag
207     * @param $levels int   The number of levels you want. A 5 gives levels 0 to 4.
208     */
209    protected function cloudWeight(&$tags, $min, $max, $levels)
210    {
211        $levels--;
212
213        // calculate tresholds
214        $tresholds = [];
215        for ($i = 0; $i <= $levels; $i++) {
216            $tresholds[$i] = ($max - $min + 1) ** ($i / $levels) + $min - 1;
217        }
218
219        // assign weights
220        foreach ($tags as $tag) {
221            foreach ($tresholds as $tresh => $val) {
222                if ($tag['cnt'] <= $val) {
223                    $tags[$tag['value']]['lvl'] = $tresh;
224                    break;
225                }
226                $tags[$tag['value']]['lvl'] = $levels;
227            }
228        }
229
230        // sort
231        ksort($tags);
232    }
233}
234