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