Skip to content

Commit

Permalink
Merge pull request #230 from cosmocode/feature/cloudAggregation
Browse files Browse the repository at this point in the history
Add cloud aggregation similar to data-plugin's datacloud
  • Loading branch information
splitbrain committed Dec 8, 2016
2 parents 761e541 + 68b45d6 commit cde2661
Show file tree
Hide file tree
Showing 13 changed files with 578 additions and 1 deletion.
1 change: 1 addition & 0 deletions _test/ConfigParser.test.php
Expand Up @@ -78,6 +78,7 @@ public function test_simple() {
)
),
'csv' => true,
'target' => ''
);

$this->assertEquals($expected_config, $actual_config);
Expand Down
10 changes: 10 additions & 0 deletions _test/Type_Page.test.php
Expand Up @@ -108,6 +108,16 @@ public function test_search() {
$result[0][3]->getValue()
);

// if there is no title in the database display the pageid
$this->assertEquals(
array(
'DokuWiki Overview',
'DokuWiki Foobar Syntax',
'wiki:welcome'
),
$result[0][3]->getDisplayValue()
);

// search single with title
$single = clone $search;
$single->addFilter('singletitle', 'Overview', '*~', 'AND');
Expand Down
2 changes: 2 additions & 0 deletions lang/en/lang.php
Expand Up @@ -80,6 +80,8 @@
$lang['Exception No data saved'] = 'No data saved';
$lang['Exception no sqlite'] = 'The struct plugin requires the sqlite plugin. Please install and enable it.';

$lang['Warning: no filters for cloud'] = 'Filters are not supported for struct clouds.';

$lang['sort'] = 'Sort by this column';
$lang['next'] = 'Next page';
$lang['prev'] = 'Previous page';
Expand Down
193 changes: 193 additions & 0 deletions meta/AggregationCloud.php
@@ -0,0 +1,193 @@
<?php

namespace dokuwiki\plugin\struct\meta;

class AggregationCloud {

/**
* @var string the page id of the page this is rendered to
*/
protected $id;

/**
* @var string the Type of renderer used
*/
protected $mode;

/**
* @var \Doku_Renderer the DokuWiki renderer used to create the output
*/
protected $renderer;

/**
* @var SearchConfig the configured search - gives access to columns etc.
*/
protected $searchConfig;

/**
* @var Column[] the list of columns to be displayed
*/
protected $columns;

/**
* @var Value[][] the search result
*/
protected $result;

/**
* @var int number of all results
*/
protected $resultCount;

/**
* Initialize the Aggregation renderer and executes the search
*
* You need to call @see render() on the resulting object.
*
* @param string $id
* @param string $mode
* @param \Doku_Renderer $renderer
* @param SearchConfig $searchConfig
*/
public function __construct($id, $mode, \Doku_Renderer $renderer, SearchCloud $searchConfig) {
$this->id = $id;
$this->mode = $mode;
$this->renderer = $renderer;
$this->searchConfig = $searchConfig;
$this->data = $searchConfig->getConf();
$this->columns = $searchConfig->getColumns();
$this->result = $this->searchConfig->execute();
$this->resultCount = $this->searchConfig->getCount();

$this->max = $this->result[0]['count'];
$this->min = end($this->result)['count'];
}

/**
* Create the cloud on the renderer
*/
public function render() {

$this->sortResults();

$this->startScope();
$this->startList();
foreach ($this->result as $result) {
$this->renderTag($result);
}
$this->finishList();
$this->finishScope();
return;
}

/**
* Adds additional info to document and renderer in XHTML mode
*
* @see finishScope()
*/
protected function startScope() {
// wrapping div
if($this->mode != 'xhtml') return;
$this->renderer->doc .= "<div class=\"structcloud\">";
}

/**
* Closes the table and anything opened in startScope()
*
* @see startScope()
*/
protected function finishScope() {
// wrapping div
if($this->mode != 'xhtml') return;
$this->renderer->doc .= '</div>';
}

/**
* Render a tag of the cloud
*
* @param ['tag' => Value, 'count' => int] $result
*/
protected function renderTag($result) {
/**
* @var Value $value
*/
$value = $result['tag'];
$count = $result['count'];
if ($value->isEmpty()) {
return;
}

$type = strtolower($value->getColumn()->getType()->getClass());
$weight = $this->getWeight($count, $this->min, $this->max);

if (!empty($this->data['target'])) {
$target = $this->data['target'];
} else {
global $INFO;
$target = $INFO['id'];
}

$tagValue = $value->getDisplayValue();
if (is_array($tagValue)) {
$tagValue = $tagValue[0];
}
$key = $value->getColumn()->getFullQualifiedLabel() . '*~';
$filter = SearchConfigParameters::$PARAM_FILTER . "[$key]=" . urlencode($tagValue);

$this->renderer->listitem_open(1);
$this->renderer->listcontent_open();

if($this->mode == 'xhtml') {
$this->renderer->doc .= "<div style='font-size:$weight%' data-count='$count' class='cloudtag struct_$type'>";
}

$value->renderAsTagCloudLink($this->renderer, $this->mode, $target, $filter, $weight);

if($this->mode == 'xhtml') {
$this->renderer->doc .= '</div>';
}

$this->renderer->listcontent_close();
$this->renderer->listitem_close();
}

/**
* This interpolates the weight between 70 and 150 based on $min, $max and $current
*
* @param int $current
* @param int $min
* @param int $max
* @return int
*/
protected function getWeight($current, $min, $max) {
if ($min == $max) {
return 100;
}
return round(($current - $min)/($max - $min) * 80 + 70);
}

/**
* Sort the list of results
*/
protected function sortResults() {
usort($this->result, function ($a, $b) {
$asort = $a['tag']->getColumn()->getType()->getSortString($a['tag']);
$bsort = $b['tag']->getColumn()->getType()->getSortString($b['tag']);
if ($asort < $bsort) {
return -1;
}
if ($asort > $bsort) {
return 1;
}
return 0;
});
}

protected function startList() {
$this->renderer->listu_open();
}

protected function finishList() {
$this->renderer->listu_close();
}
}
5 changes: 5 additions & 0 deletions meta/ConfigParser.php
Expand Up @@ -29,6 +29,7 @@ public function __construct($lines) {
'summarize' => false,
'rownumbers' => false,
'sepbyheaders' => false,
'target' => '',
'headers' => array(),
'widths' => array(),
'filter' => array(),
Expand Down Expand Up @@ -108,6 +109,10 @@ public function __construct($lines) {
case 'csv':
$this->config['csv'] = (bool) $val;
break;
case 'target':
case 'page':
$this->config['target'] = cleanID($val);
break;
default:
throw new StructException("unknown option '%s'", hsc($key));
}
Expand Down
108 changes: 108 additions & 0 deletions meta/SearchCloud.php
@@ -0,0 +1,108 @@
<?php

namespace dokuwiki\plugin\struct\meta;


/**
* Class SearchCloud
*
* The same as @see SearchConfig, but executed a search that is not pid-focused
*
* @package dokuwiki\plugin\struct\meta
*/
class SearchCloud extends SearchConfig {

protected $limit = '';


/**
* Transform the set search parameters into a statement
*
* @return array ($sql, $opts) The SQL and parameters to execute
*/
public function getSQL() {
if(!$this->columns) throw new StructException('nocolname');

$QB = new QueryBuilder();
reset($this->schemas);
$schema = current($this->schemas);
$datatable = 'data_' . $schema->getTable();
if(!$schema->isLookup()) {
$QB->addTable('schema_assignments');
$QB->filters()->whereAnd("$datatable.pid = schema_assignments.pid");
$QB->filters()->whereAnd("schema_assignments.tbl = '{$schema->getTable()}'");
$QB->filters()->whereAnd("schema_assignments.assigned = 1");
$QB->filters()->whereAnd("GETACCESSLEVEL($datatable.pid) > 0");
$QB->filters()->whereAnd("PAGEEXISTS($datatable.pid) = 1");
}
$QB->addTable($datatable);
$QB->filters()->whereAnd("$datatable.latest = 1");

$col = $this->columns[0];
if($col->isMulti()) {
$multitable = "multi_{$col->getTable()}";
$MN = $QB->generateTableAlias('M');

$QB->addLeftJoin(
$datatable,
$multitable,
$MN,
"$datatable.pid = $MN.pid AND
$datatable.rev = $MN.rev AND
$MN.colref = {$col->getColref()}"
);

$col->getType()->select($QB, $MN, 'value', 'tag');
$colname = $MN . '.value';
} else {
$col->getType()->select($QB, $datatable, $col->getColName(), 'tag');
$colname = $datatable . '.' . $col->getColName();
}
$QB->addSelectStatement("COUNT($colname)", 'count');
$QB->addGroupByStatement('tag');
$QB->addOrderBy('count DESC');

list($sql, $opts) = $QB->getSQL();
return [$sql . $this->limit, $opts];
}

/**
* We do not have pagination in clouds, so we can work with a limit within SQL
*
* @param int $limit
*/
public function setLimit($limit) {
$this->limit = " LIMIT $limit";
}

/**
* Execute this search and return the result
*
* The result is a two dimensional array of Value()s.
*
* @return Value[][]
*/
public function execute() {
list($sql, $opts) = $this->getSQL();

/** @var \PDOStatement $res */
$res = $this->sqlite->query($sql, $opts);
if($res === false) throw new StructException("SQL execution failed for\n\n$sql");

$result = [];
$rows = $this->sqlite->res2arr($res);

foreach ($rows as $row) {
if (!empty($this->config['min']) && $this->config['min'] > $row['count']) {
break;
}

$row['tag'] = new Value($this->columns[0], $row['tag']);
$result[] = $row;
}

$this->sqlite->res_close($res);
$this->count = count($result);
return $result;
}
}
13 changes: 13 additions & 0 deletions meta/Value.php
Expand Up @@ -154,6 +154,19 @@ public function render(\Doku_Renderer $R, $mode) {
return true;
}

/**
* Render this value as a tag-link in a struct cloud
*
* @param \Doku_Renderer $R
* @param string $mode
* @param string $page
* @param string $filterQuery
* @param int $weight
*/
public function renderAsTagCloudLink(\Doku_Renderer $R, $mode, $page, $filterQuery, $weight) {
$this->column->getType()->renderTagCloudLink($this->value, $R, $mode, $page, $filterQuery, $weight);
}

/**
* Return the value editor for this value field
*
Expand Down

0 comments on commit cde2661

Please sign in to comment.