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->renderer->doc .= '<ul>'; 75 foreach ($this->result as $result) { 76 $this->renderTag($result); 77 } 78 $this->renderer->doc .= '</ul>'; 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 $weight = $this->getWeight($count, $this->min, $this->max); 117 $type = 'struct_' . strtolower($value->getColumn()->getType()->getClass()); 118 if ($value->isEmpty()) { 119 return; 120 } 121 122 $this->renderer->doc .= '<li><div class="li">'; 123 $this->renderer->doc .= "<div style='font-size:$weight%' data-count='$count' class='cloudtag $type'>"; 124 125 $this->renderer->doc .= $this->getTagLink($value, $weight); 126 $this->renderer->doc .= '</div>'; 127 $this->renderer->doc .= '</div></li>'; 128 } 129 130 /** 131 * @param Value $value 132 * @param int $weight 133 * @return string 134 */ 135 protected function getTagLink(Value $value, $weight) { 136 $type = $value->getColumn()->getType()->getClass(); 137 $schema = $this->data['schemas'][0][0]; 138 $col = $value->getColumn()->getLabel(); 139 140 global $ID; // todo replace with target 141 142 143 $tagValue = $value->getDisplayValue(); 144 if (empty($tagValue)) { 145 $tagValue = $value->getRawValue(); 146 } 147 if (is_array($tagValue)) { 148 $tagValue = $tagValue[0]; 149 } 150 $filter = "flt[$schema.$col*~]=" . urlencode($tagValue); 151 $linktext = $tagValue; 152 153 if ($type == 'Color') { 154 $url = wl($ID, $filter); 155 $style = "background-color:$tagValue;display:block;height:100%"; 156 return "<a href='$url' style='$style'></a>"; 157 } 158 if ($type == 'Media' && $value->getColumn()->getType()->getConfig()['mime'] == 'image/') { 159 $linktext = p_get_instructions("[[|{{{$tagValue}?$weight}}]]")[2][1][1]; 160 } 161 162 return $this->renderer->internallink("?$filter",$linktext, null, true); 163 } 164 165 /** 166 * This interpolates the weight between 70 and 150 based on $min, $max and $current 167 * 168 * @param int $current 169 * @param int $min 170 * @param int $max 171 * @return int 172 */ 173 protected function getWeight($current, $min, $max) { 174 if ($min == $max) { 175 return 100; 176 } 177 return round(($current - $min)/($max - $min) * 80 + 70); 178 } 179 180 /** 181 * Sort the list of results 182 */ 183 protected function sortResults() { 184 foreach ($this->result as &$result) { 185 if ($result['tag']->getColumn()->getType()->getClass() == 'Color') { 186 $result['sort'] = $this->getHue($result['tag']->getRawValue()); 187 } else { 188 $result['sort'] = $result['tag']->getDisplayValue(); 189 } 190 } 191 usort($this->result, function ($a, $b) { 192 if ($a['sort'] < $b['sort']) { 193 return -1; 194 } 195 if ($a['sort'] > $b['sort']) { 196 return 1; 197 } 198 return 0; 199 }); 200 } 201 202 /** 203 * Calculate the hue of a color to use it for sorting so we can sort similar colors together. 204 * 205 * @param string $color the color as #RRGGBB 206 * @return float|int 207 */ 208 protected function getHue($color) { 209 if (!preg_match('/^#[0-9A-F]{6}$/i', $color)) { 210 return 0; 211 } 212 213 $red = hexdec(substr($color, 1, 2)); 214 $green = hexdec(substr($color, 3, 2)); 215 $blue = hexdec(substr($color, 5, 2)); 216 217 $min = min([$red, $green, $blue]); 218 $max = max([$red, $green, $blue]); 219 220 if ($max == $red) { 221 $hue = ($green-$blue)/($max-$min); 222 } 223 if ($max == $green) { 224 $hue = 2 + ($blue-$red)/($max-$min); 225 } 226 if ($max == $blue) { 227 $hue = 4 + ($red-$green)/($max-$min); 228 } 229 $hue = $hue * 60; 230 if ($hue < 0) { 231 $hue += 360; 232 } 233 return $hue; 234 } 235} 236