xref: /plugin/struct/meta/AggregationCloud.php (revision c7dd6b6ae6b1d46da10628f8b099c209561c17b2)
1<?php
2
3namespace dokuwiki\plugin\struct\meta;
4
5class AggregationCloud {
6
7    /**
8     * @var string the page id of the page this is rendered to
9     */
10    protected $id;
11
12    /**
13     * @var string the Type of renderer used
14     */
15    protected $mode;
16
17    /**
18     * @var \Doku_Renderer the DokuWiki renderer used to create the output
19     */
20    protected $renderer;
21
22    /**
23     * @var SearchConfig the configured search - gives access to columns etc.
24     */
25    protected $searchConfig;
26
27    /**
28     * @var Column[] the list of columns to be displayed
29     */
30    protected $columns;
31
32    /**
33     * @var  Value[][] the search result
34     */
35    protected $result;
36
37    /**
38     * @var int number of all results
39     */
40    protected $resultCount;
41
42    /**
43     * Initialize the Aggregation renderer and executes the search
44     *
45     * You need to call @see render() on the resulting object.
46     *
47     * @param string $id
48     * @param string $mode
49     * @param \Doku_Renderer $renderer
50     * @param SearchConfig $searchConfig
51     */
52    public function __construct($id, $mode, \Doku_Renderer $renderer, SearchCloud $searchConfig) {
53        $this->id = $id;
54        $this->mode = $mode;
55        $this->renderer = $renderer;
56        $this->searchConfig = $searchConfig;
57        $this->data = $searchConfig->getConf();
58        $this->columns = $searchConfig->getColumns();
59        $this->result = $this->searchConfig->execute();
60        $this->resultCount = $this->searchConfig->getCount();
61
62        $this->max = $this->result[0]['count'];
63        $this->min = end($this->result)['count'];
64    }
65
66    /**
67     * Create the table on the renderer
68     */
69    public function render() {
70
71        $this->sortResults();
72
73        $this->startScope();
74        $this->startList();
75        foreach ($this->result as $result) {
76            $this->renderTag($result);
77        }
78        $this->finishList();
79        $this->finishScope();
80        return;
81    }
82
83    /**
84     * Adds additional info to document and renderer in XHTML mode
85     *
86     * @see finishScope()
87     */
88    protected function startScope() {
89        // wrapping div
90        if($this->mode != 'xhtml') return;
91        $this->renderer->doc .= "<div class=\"structcloud\">";
92    }
93
94    /**
95     * Closes the table and anything opened in startScope()
96     *
97     * @see startScope()
98     */
99    protected function finishScope() {
100        // wrapping div
101        if($this->mode != 'xhtml') return;
102        $this->renderer->doc .= '</div>';
103    }
104
105    /**
106     * Render a tag of the cloud
107     *
108     * @param ['tag' => Value, 'count' => int] $result
109     */
110    protected function renderTag($result) {
111        /**
112         * @var Value $value
113         */
114        $value = $result['tag'];
115        $count = $result['count'];
116        if ($value->isEmpty()) {
117            return;
118        }
119
120        $this->renderer->listitem_open(1);
121        $this->renderer->listcontent_open();
122
123        $this->renderTagLink($value, $count);
124        $this->renderer->listcontent_close();
125        $this->renderer->listitem_close();
126    }
127
128    /**
129     * @param Value $value
130     * @param int $count
131     */
132    protected function renderTagLink(Value $value, $count) {
133        $type = strtolower($value->getColumn()->getType()->getClass());
134        $weight = $this->getWeight($count, $this->min, $this->max);
135        $schema = $this->data['schemas'][0][0];
136        $col = $value->getColumn()->getLabel();
137
138        if (!empty($this->data['target'])) {
139            $target = $this->data['target'];
140        } else {
141            global $INFO;
142            $target = $INFO['id'];
143        }
144
145        $tagValue = $value->getDisplayValue();
146        if (empty($tagValue)) {
147            $tagValue = $value->getRawValue();
148        }
149        if (is_array($tagValue)) {
150            $tagValue = $tagValue[0];
151        }
152        $filter = "flt[$schema.$col*~]=" . urlencode($tagValue);
153        $linktext = $tagValue;
154
155
156        if($this->mode != 'xhtml') {
157            $this->renderer->internallink("$target?$filter",$linktext);
158            return;
159        }
160
161        $this->renderer->doc .= "<div style='font-size:$weight%' data-count='$count' class='cloudtag struct_$type'>";
162
163        if ($type == 'color') {
164            $url = wl($target, $filter);
165            $style = "background-color:$tagValue;display:block;height:100%";
166            $this->renderer->doc .=  "<a href='$url' style='$style'></a>";
167        } else {
168            if ($type == 'media' && $value->getColumn()->getType()->getConfig()['mime'] == 'image/') {
169                $linktext = p_get_instructions("[[|{{{$tagValue}?$weight}}]]")[2][1][1];
170            }
171
172            $this->renderer->internallink("$target?$filter", $linktext);
173        }
174        $this->renderer->doc .= '</div>';
175    }
176
177    /**
178     * This interpolates the weight between 70 and 150 based on $min, $max and $current
179     *
180     * @param int $current
181     * @param int $min
182     * @param int $max
183     * @return int
184     */
185    protected function getWeight($current, $min, $max) {
186        if ($min == $max) {
187            return 100;
188        }
189        return round(($current - $min)/($max - $min) * 80 + 70);
190    }
191
192    /**
193     * Sort the list of results
194     */
195    protected function sortResults() {
196        foreach ($this->result as &$result) {
197            if ($result['tag']->getColumn()->getType()->getClass() == 'Color') {
198                $result['sort'] = $this->getHue($result['tag']->getRawValue());
199            } else {
200                $result['sort'] = $result['tag']->getDisplayValue();
201            }
202        }
203        usort($this->result, function ($a, $b) {
204            if ($a['sort'] < $b['sort']) {
205                return -1;
206            }
207            if ($a['sort'] > $b['sort']) {
208                return 1;
209            }
210            return 0;
211        });
212    }
213
214    /**
215     * Calculate the hue of a color to use it for sorting so we can sort similar colors together.
216     *
217     * @param string $color the color as #RRGGBB
218     * @return float|int
219     */
220    protected function getHue($color) {
221        if (!preg_match('/^#[0-9A-F]{6}$/i', $color)) {
222            return 0;
223        }
224
225        $red   = hexdec(substr($color, 1, 2));
226        $green = hexdec(substr($color, 3, 2));
227        $blue  = hexdec(substr($color, 5, 2));
228
229        $min = min([$red, $green, $blue]);
230        $max = max([$red, $green, $blue]);
231
232        if ($max == $red) {
233            $hue = ($green-$blue)/($max-$min);
234        }
235        if ($max == $green) {
236            $hue = 2 + ($blue-$red)/($max-$min);
237        }
238        if ($max == $blue) {
239            $hue = 4 + ($red-$green)/($max-$min);
240        }
241        $hue = $hue * 60;
242        if ($hue < 0) {
243            $hue += 360;
244        }
245        return $hue;
246    }
247
248    protected function startList() {
249        $this->renderer->listu_open();
250    }
251
252    protected function finishList() {
253        $this->renderer->listu_close();
254    }
255}
256