Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
0.00% |
0 / 167 |
|
0.00% |
0 / 12 |
CRAP | |
0.00% |
0 / 1 |
| CargoExhibitFormat | |
0.00% |
0 / 167 |
|
0.00% |
0 / 12 |
2450 | |
0.00% |
0 / 1 |
| allowedParameters | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
2 | |||
| prependDot | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| createMap | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
20 | |||
| createTimeline | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
72 | |||
| createTabular | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
12 | |||
| createFacets | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
6 | |||
| createSearch | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
| createLens | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
30 | |||
| queryAndDisplay | |
0.00% |
0 / 52 |
|
0.00% |
0 / 1 |
156 | |||
| automateViews | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
12 | |||
| getCoordinatesFields | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
20 | |||
| getDateFields | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
30 | |||
| 1 | <?php |
| 2 | /** |
| 3 | * Adds Exhibit format to Cargo queries. |
| 4 | * |
| 5 | * @author @lmorillas |
| 6 | */ |
| 7 | |
| 8 | use MediaWiki\Html\Html; |
| 9 | |
| 10 | class CargoExhibitFormat extends CargoDeferredFormat { |
| 11 | /** @var array */ |
| 12 | private $displayParams; |
| 13 | /** @var array */ |
| 14 | private $views; |
| 15 | |
| 16 | public static function allowedParameters() { |
| 17 | return [ |
| 18 | 'view' => [ 'values' => [ 'map', 'tabular', 'timeline' ] ], |
| 19 | 'facets' => [ 'type' => 'string' ], |
| 20 | 'datalabel' => [ 'type' => 'string' ], |
| 21 | 'end' => [ 'type' => 'string' ], |
| 22 | 'color' => [ 'type' => 'string' ], |
| 23 | 'topunit' => [ 'values' => [ 'millisecond', 'second', 'minute', 'hour', 'day', 'week', 'month', 'year', 'decade', 'century', 'millennium' ] ], |
| 24 | 'toppx' => [ 'type' => 'int' ], |
| 25 | 'bottompx' => [ 'type' => 'int' ] |
| 26 | ]; |
| 27 | } |
| 28 | |
| 29 | /** |
| 30 | * @param string $p |
| 31 | * @return string |
| 32 | */ |
| 33 | private function prependDot( $p ) { |
| 34 | return '.' . trim( $p ); |
| 35 | } |
| 36 | |
| 37 | private function createMap( $sqlQueries ) { |
| 38 | $maps_script = '<link rel="exhibit-extension" href="//api.simile-widgets.org/exhibit/HEAD/extensions/map/map-extension.js"/>'; |
| 39 | $this->mOutput->addHeadItem( $maps_script, $maps_script ); |
| 40 | |
| 41 | if ( !array_key_exists( "latlng", $this->displayParams ) ) { |
| 42 | $coordFields = $this->getCoordinatesFields( $sqlQueries ); |
| 43 | if ( count( $coordFields ) > 0 ) { |
| 44 | $this->displayParams['latlng'] = $coordFields[0]; |
| 45 | } |
| 46 | } |
| 47 | |
| 48 | $attrs = [ |
| 49 | 'data-ex-role' => 'view', |
| 50 | 'data-ex-view-class' => "Map", |
| 51 | 'data-ex-latlng' => $this->prependDot( $this->displayParams['latlng'] ), |
| 52 | 'data-ex-autoposition' => "true", |
| 53 | ]; |
| 54 | |
| 55 | if ( array_key_exists( "color", $this->displayParams ) ) { |
| 56 | $attrs["data-ex-color-key"] = $this->prependDot( $this->displayParams['color'] ); |
| 57 | } |
| 58 | |
| 59 | return Html::element( 'div', $attrs ); |
| 60 | } |
| 61 | |
| 62 | private function createTimeline( $sqlQueries ) { |
| 63 | $timeline_script = '<link rel="exhibit-extension" href="//api.simile-widgets.org/exhibit/HEAD/extensions/time/time-extension.js"/>'; |
| 64 | $this->mOutput->addHeadItem( $timeline_script, $timeline_script ); |
| 65 | |
| 66 | // div |
| 67 | $attrs = []; |
| 68 | $attrs['data-ex-role'] = 'view'; |
| 69 | $attrs["data-ex-view-class"] = "Timeline"; |
| 70 | |
| 71 | if ( !array_key_exists( "start", $this->displayParams ) ) { |
| 72 | $dateFields = $this->getDateFields( $sqlQueries ); |
| 73 | if ( count( $dateFields ) > 0 ) { |
| 74 | $this->displayParams['start'] = $dateFields[0]; |
| 75 | } |
| 76 | } |
| 77 | |
| 78 | $attrs["data-ex-start"] = $this->prependDot( $this->displayParams['start'] ); |
| 79 | |
| 80 | if ( array_key_exists( "end", $this->displayParams ) ) { |
| 81 | $attrs["data-ex-end"] = $this->prependDot( $this->displayParams['end'] ); |
| 82 | } |
| 83 | if ( array_key_exists( "color", $this->displayParams ) ) { |
| 84 | $attrs["data-ex-color-key"] = $this->prependDot( $this->displayParams['color'] ); |
| 85 | } |
| 86 | if ( array_key_exists( "topunit", $this->displayParams ) ) { |
| 87 | $attrs["data-ex-top-band-unit"] = $this->displayParams['topunit']; |
| 88 | } |
| 89 | if ( array_key_exists( "toppx", $this->displayParams ) ) { |
| 90 | $attrs["data-ex-top-band-pixels-per-unit"] = $this->displayParams['toppx']; |
| 91 | } |
| 92 | if ( array_key_exists( "bottompx", $this->displayParams ) ) { |
| 93 | $attrs["data-ex-bottom-band-pixels-per-unit"] = $this->displayParams['bottompx']; |
| 94 | } |
| 95 | return Html::element( 'div', $attrs ); |
| 96 | } |
| 97 | |
| 98 | /** |
| 99 | * @param string[] $fieldList |
| 100 | * @return string HTML |
| 101 | */ |
| 102 | private function createTabular( $fieldList ) { |
| 103 | $columnsList = []; |
| 104 | foreach ( $fieldList as $field ) { |
| 105 | if ( strpos( $field, '__' ) == false ) { |
| 106 | $columnsList[] = $field; |
| 107 | } |
| 108 | } |
| 109 | |
| 110 | $attrs = [ |
| 111 | 'data-ex-role' => 'view', |
| 112 | 'data-ex-view-class' => 'Tabular', |
| 113 | 'data-ex-paginate' => "true", |
| 114 | 'data-ex-table-styler' => "tableStyler", |
| 115 | |
| 116 | 'data-ex-columns' => implode( ',', |
| 117 | array_map( "CargoExhibitFormat::prependDot", $columnsList ) ), |
| 118 | |
| 119 | 'data-ex-column-labels' => implode( ',', array_map( "ucfirst", $columnsList ) ) |
| 120 | ]; |
| 121 | |
| 122 | return Html::element( 'div', $attrs ); |
| 123 | } |
| 124 | |
| 125 | private function createFacets( $facets ) { |
| 126 | // Explode facets and create the div for each of them. |
| 127 | $text = $this->createSearch(); |
| 128 | foreach ( $facets as $f ) { |
| 129 | $attrs = [ |
| 130 | 'data-ex-role' => "facet", |
| 131 | 'data-ex-collapsible' => "true", |
| 132 | 'data-ex-expression' => '.' . $f, |
| 133 | 'data-ex-show-missing' => 'false', |
| 134 | 'data-ex-facet-label' => ucfirst( $f ), |
| 135 | 'style' => "float: left; width: 24%; margin: 0 1% 0 0;" |
| 136 | ]; |
| 137 | $text .= Html::element( 'div', $attrs ); |
| 138 | } |
| 139 | return Html::rawElement( 'div', [ "class" => "facets", "style" => "overflow: hidden; width: 100%;" ], $text ); |
| 140 | } |
| 141 | |
| 142 | /** |
| 143 | * @return string |
| 144 | */ |
| 145 | private function createSearch() { |
| 146 | $attrs = [ |
| 147 | 'data-ex-role' => "exhibit-facet", |
| 148 | 'data-ex-facet-class' => "TextSearch", |
| 149 | 'data-ex-facet-label' => wfMessage( 'search' )->text(), |
| 150 | 'style' => "float: left; width: 24%; margin: 0 1% 0 0;" |
| 151 | ]; |
| 152 | return Html::element( 'div', $attrs ); |
| 153 | } |
| 154 | |
| 155 | private function createLens( $fieldList ) { |
| 156 | $lensBody = '<caption><strong data-ex-content=".label"></strong></caption>'; |
| 157 | foreach ( $fieldList as $field ) { |
| 158 | if ( $field != "label" && strpos( $field, '__' ) === false && |
| 159 | strpos( $field, ' ' ) === false ) { |
| 160 | $th = "<strong>" . ucfirst( $field ) . "</strong>"; |
| 161 | $lensBody .= "<tr data-ex-if-exists=\".$field\"><td>$th</td><td data-ex-content=\".$field\"></td></tr>"; |
| 162 | } |
| 163 | } |
| 164 | $tableAttrs = [ |
| 165 | 'data-ex-role' => 'lens', |
| 166 | 'class' => 'cargoTable', |
| 167 | 'style' => "display: none; width: 100%;" |
| 168 | ]; |
| 169 | return Html::rawElement( 'table', $tableAttrs, $lensBody ); |
| 170 | } |
| 171 | |
| 172 | /** |
| 173 | * @param CargoSQLQuery[] $sqlQueries |
| 174 | * @param array $displayParams |
| 175 | * @param array|null $querySpecificParams |
| 176 | * @return string HTML |
| 177 | * @throws MWException |
| 178 | */ |
| 179 | public function queryAndDisplay( $sqlQueries, $displayParams, $querySpecificParams = null ) { |
| 180 | global $cgScriptPath; |
| 181 | |
| 182 | $this->mOutput->addModules( [ 'ext.cargo.exhibit' ] ); |
| 183 | $this->mOutput->addModuleStyles( [ 'ext.cargo.main' ] ); |
| 184 | |
| 185 | $exhibit_busy = $cgScriptPath . "/resources/images/loading.gif"; |
| 186 | // The "loading" message is just alt-text, so it doesn't really |
| 187 | // matter that it's hardcoded in English. |
| 188 | $preViewsText = '<img id="loading_exhibit" src="' . $exhibit_busy . '" alt="Loading Exhibit" style="display: none;" >'; |
| 189 | |
| 190 | $field_list = []; |
| 191 | foreach ( $sqlQueries as $sqlQuery ) { |
| 192 | foreach ( $sqlQuery->mAliasedFieldNames as $alias => $fieldName ) { |
| 193 | $field_list[] = $alias; |
| 194 | } |
| 195 | } |
| 196 | |
| 197 | $csv_properties = ''; |
| 198 | if ( !in_array( "label", $field_list ) ) { |
| 199 | // first field will be label! |
| 200 | $field_list[0] = 'label'; |
| 201 | $csv_properties = 'data-ex-properties="' . implode( ',', $field_list ) . '"'; |
| 202 | } |
| 203 | |
| 204 | $queryParams = $this->sqlQueriesToQueryParams( $sqlQueries ); |
| 205 | $queryParams['format'] = 'csv'; |
| 206 | |
| 207 | $ce = SpecialPage::getTitleFor( 'CargoExport' ); |
| 208 | $dataurl = htmlentities( $ce->getFullURL( $queryParams ) ); |
| 209 | |
| 210 | // Data imported as csv |
| 211 | $datalink = "<link href=\"$dataurl\" type=\"text/csv\" rel=\"exhibit/data\" data-ex-has-column-titles=\"true\" $csv_properties />"; |
| 212 | |
| 213 | $this->mOutput->addHeadItem( $datalink, $datalink ); |
| 214 | |
| 215 | $this->displayParams = $displayParams; |
| 216 | |
| 217 | // lens |
| 218 | $preViewsText .= $this->createLens( $field_list ); |
| 219 | |
| 220 | // Facets |
| 221 | if ( array_key_exists( 'facets', $displayParams ) ) { |
| 222 | $facets = array_map( 'trim', explode( ',', $displayParams['facets'] ) ); |
| 223 | $preViewsText .= $this->createFacets( $facets ); |
| 224 | } else { |
| 225 | $preViewsText .= $this->createFacets( array_slice( $field_list, 0, 3 ) ); |
| 226 | } |
| 227 | |
| 228 | if ( array_key_exists( 'datalabel', $displayParams ) ) { |
| 229 | $datalabel = trim( $displayParams['datalabel'] ); |
| 230 | // What is this used for? |
| 231 | $dataplural = $datalabel . 's'; |
| 232 | $data_label_link = <<<EOLABEL |
| 233 | <link href="#cargoExhibit" type="text/html" rel="exhibit/data" |
| 234 | data-ex-item-type="Item" data-ex-label="$datalabel" data-ex-plural-label="$dataplural" /> |
| 235 | EOLABEL; |
| 236 | $this->mOutput->addHeadItem( $data_label_link, $data_label_link ); |
| 237 | } |
| 238 | |
| 239 | // View |
| 240 | $this->views = []; |
| 241 | |
| 242 | if ( array_key_exists( 'view', $displayParams ) ) { |
| 243 | $this->views = array_map( 'ucfirst', array_map( 'trim', explode( ',', $displayParams['view'] ) ) ); |
| 244 | } else { |
| 245 | $this->automateViews( $sqlQueries ); |
| 246 | } |
| 247 | |
| 248 | $viewsText = ""; |
| 249 | foreach ( $this->views as $view ) { |
| 250 | switch ( $view ) { |
| 251 | case "Timeline": |
| 252 | $viewsText .= $this->createTimeline( $sqlQueries ); |
| 253 | break; |
| 254 | case "Map": |
| 255 | $viewsText .= $this->createMap( $sqlQueries ); |
| 256 | break; |
| 257 | case "Tabular": |
| 258 | $viewsText .= $this->createTabular( $field_list ); |
| 259 | } |
| 260 | } |
| 261 | |
| 262 | if ( count( $this->views ) > 1 ) { |
| 263 | $viewsText = Html::rawElement( 'div', |
| 264 | [ 'data-ex-role' => "viewPanel" ], |
| 265 | $viewsText ); |
| 266 | } |
| 267 | |
| 268 | return $preViewsText . '<div id="cargoExhibit">' . $viewsText . '</div>'; |
| 269 | } |
| 270 | |
| 271 | /** |
| 272 | * Initializes $this->views[] |
| 273 | */ |
| 274 | private function automateViews( $sqlQueries ) { |
| 275 | // map ? |
| 276 | $coordFields = $this->getCoordinatesFields( $sqlQueries ); |
| 277 | if ( count( $coordFields ) > 0 ) { |
| 278 | $this->views[] = 'Map'; |
| 279 | $this->displayParams['latlng'] = $coordFields[0]; |
| 280 | } |
| 281 | |
| 282 | // timeline ? |
| 283 | $dateFields = $this->getDateFields( $sqlQueries ); |
| 284 | if ( count( $dateFields ) > 0 ) { |
| 285 | $this->views[] = 'Timeline'; |
| 286 | $this->displayParams['start'] = $dateFields[0]; |
| 287 | } |
| 288 | |
| 289 | $this->views[] = 'Tabular'; |
| 290 | } |
| 291 | |
| 292 | private function getCoordinatesFields( $sqlQueries ) { |
| 293 | $coordinatesFields = []; |
| 294 | |
| 295 | foreach ( $sqlQueries as $query ) { |
| 296 | $fieldDescriptions = $query->mFieldDescriptions; |
| 297 | foreach ( $fieldDescriptions as $field => $description ) { |
| 298 | if ( $description->mType == 'Coordinates' ) { |
| 299 | $coordinatesFields[] = $field; |
| 300 | } |
| 301 | } |
| 302 | } |
| 303 | return $coordinatesFields; |
| 304 | } |
| 305 | |
| 306 | private function getDateFields( $sqlQueries ) { |
| 307 | $dateFields = []; |
| 308 | |
| 309 | foreach ( $sqlQueries as $query ) { |
| 310 | $fieldDescriptions = $query->mFieldDescriptions; |
| 311 | foreach ( $fieldDescriptions as $field => $description ) { |
| 312 | if ( $description->mType == 'Date' || $description->mType == 'Datetime' ) { |
| 313 | $dateFields[] = $field; |
| 314 | } |
| 315 | } |
| 316 | } |
| 317 | return $dateFields; |
| 318 | } |
| 319 | } |