Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 167
0.00% covered (danger)
0.00%
0 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
CargoExhibitFormat
0.00% covered (danger)
0.00%
0 / 167
0.00% covered (danger)
0.00%
0 / 12
2450
0.00% covered (danger)
0.00%
0 / 1
 allowedParameters
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
 prependDot
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 createMap
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
20
 createTimeline
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
72
 createTabular
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
12
 createFacets
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
6
 createSearch
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 createLens
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
30
 queryAndDisplay
0.00% covered (danger)
0.00%
0 / 52
0.00% covered (danger)
0.00%
0 / 1
156
 automateViews
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 getCoordinatesFields
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 getDateFields
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
30
1<?php
2/**
3 * Adds Exhibit format to Cargo queries.
4 *
5 * @author @lmorillas
6 */
7
8use MediaWiki\Html\Html;
9
10class 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"
234data-ex-item-type="Item" data-ex-label="$datalabel" data-ex-plural-label="$dataplural" />
235EOLABEL;
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}