Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 171
0.00% covered (danger)
0.00%
0 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
CargoMapsFormat
0.00% covered (danger)
0.00%
0 / 171
0.00% covered (danger)
0.00%
0 / 8
4422
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 allowedParameters
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 getScripts
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 getStyles
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getImageURL
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 getImageData
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 display
0.00% covered (danger)
0.00%
0 / 115
0.00% covered (danger)
0.00%
0 / 1
2162
 getMapPointValues
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
42
1<?php
2/**
3 * @author Yaron Koren
4 * @ingroup Cargo
5 */
6
7use MediaWiki\Html\Html;
8use MediaWiki\MediaWikiServices;
9use MediaWiki\Title\Title;
10
11class CargoMapsFormat extends CargoDisplayFormat {
12
13    public static $mappingService = "OpenLayers";
14    public static $mapNumber = 1;
15
16    public function __construct( $output ) {
17        global $wgCargoDefaultMapService;
18        parent::__construct( $output );
19        self::$mappingService = $wgCargoDefaultMapService;
20    }
21
22    public static function allowedParameters() {
23        return [
24            'height' => [ 'type' => 'int', 'label' => wfMessage( 'cargo-viewdata-heightparam' )->parse() ],
25            'width' => [ 'type' => 'int', 'label' => wfMessage( 'cargo-viewdata-widthparam' )->parse() ],
26            'icon' => [ 'type' => 'string' ],
27            'zoom' => [ 'type' => 'int' ],
28            'center' => [ 'type' => 'string' ],
29            'cluster' => [ 'type' => 'string' ]
30        ];
31    }
32
33    public static function getScripts() {
34        global $wgCargoDefaultMapService;
35        if ( $wgCargoDefaultMapService == 'Google Maps' ) {
36            return CargoGoogleMapsFormat::getScripts();
37        } elseif ( $wgCargoDefaultMapService == 'OpenLayers' ) {
38            return CargoOpenLayersFormat::getScripts();
39        } elseif ( $wgCargoDefaultMapService == 'Leaflet' ) {
40            return CargoLeafletFormat::getScripts();
41        } else {
42            return [];
43        }
44    }
45
46    public static function getStyles() {
47        global $wgCargoDefaultMapService;
48        if ( $wgCargoDefaultMapService == 'Leaflet' ) {
49            return CargoLeafletFormat::getStyles();
50        } else {
51            return [];
52        }
53    }
54
55    /**
56     * Based on the Maps extension's getFileUrl().
57     */
58    public static function getImageURL( $imageName ) {
59        $title = Title::makeTitle( NS_FILE, $imageName );
60
61        if ( $title == null || !$title->exists() ) {
62            return null;
63        }
64
65        $imagePage = new ImagePage( $title );
66        return $imagePage->getDisplayedFile()->getURL();
67    }
68
69    public function getImageData( $fileName ) {
70        if ( $fileName == '' ) {
71            return null;
72        }
73        $fileTitle = Title::makeTitleSafe( NS_FILE, $fileName );
74        if ( !$fileTitle->exists() ) {
75            throw new MWException( "Error: File \"$fileName\" does not exist on this wiki." );
76        }
77        $imagePage = new ImagePage( $fileTitle );
78        $file = $imagePage->getDisplayedFile();
79        $filePath = $file->getLocalRefPath();
80        [ $imageWidth, $imageHeight, $type, $attr ] = getimagesize( $filePath );
81        return [ $imageWidth, $imageHeight, $file->getUrl() ];
82    }
83
84    /**
85     * @param array $valuesTable
86     * @param array $formattedValuesTable
87     * @param array $fieldDescriptions
88     * @param array $displayParams
89     * @return string HTML
90     * @throws MWException
91     */
92    public function display( $valuesTable, $formattedValuesTable, $fieldDescriptions, $displayParams ) {
93        $coordinatesFields = [];
94        $latField = null;
95        $lonField = null;
96        $urlField = null;
97        foreach ( $fieldDescriptions as $field => $description ) {
98            if ( $description->mType == 'Coordinates' ) {
99                $coordinatesFields[] = $field;
100            }
101            if ( $field == 'lat' ) {
102                $latField = $field;
103            } elseif ( $field == 'lon' ) {
104                $lonField = $field;
105            } elseif ( $field == 'URL' ) {
106                $urlField = $field;
107            }
108        }
109
110        if ( count( $coordinatesFields ) == 0 && ( $latField == null || $lonField == null ) ) {
111            throw new MWException( "Error: no fields of type \"Coordinates\" were specified in this "
112            . "query; cannot display in a map." );
113        }
114
115        // @TODO - should this check be higher up, i.e. for all
116        // formats?
117        if ( count( $formattedValuesTable ) == 0 ) {
118            throw new MWException( "No results found for this query; not displaying a map." );
119        }
120
121        // Add necessary JS scripts and CSS styles.
122        $scripts = $this->getScripts();
123        $scriptsHTML = '';
124        foreach ( $scripts as $script ) {
125            $scriptsHTML .= Html::linkedScript( $script );
126        }
127        $styles = $this->getStyles();
128        $stylesHTML = '';
129        foreach ( $styles as $style ) {
130            $stylesHTML .= Html::linkedStyle( $style );
131        }
132        $this->mOutput->addHeadItem( $scriptsHTML, $scriptsHTML );
133        $this->mOutput->addHeadItem( $stylesHTML, $stylesHTML );
134        $this->mOutput->addModules( [ 'ext.cargo.maps' ] );
135
136        // Construct the table of data we will display.
137        $valuesForMap = [];
138        foreach ( $formattedValuesTable as $i => $valuesRow ) {
139            $displayedValuesForRow = [];
140            foreach ( $valuesRow as $fieldName => $fieldValue ) {
141                if ( !array_key_exists( $fieldName, $fieldDescriptions ) ) {
142                    continue;
143                }
144                $fieldType = $fieldDescriptions[$fieldName]->mType;
145                // Don't display any values that are going to be included already.
146                if ( $fieldType == 'Coordinates' || $fieldType == 'Coordinates part' || $fieldName == $latField || $fieldName == $lonField || $fieldName == $urlField ) {
147                    continue;
148                }
149                if ( $fieldValue == '' ) {
150                    continue;
151                }
152                $displayedValuesForRow[$fieldName] = $fieldValue;
153            }
154            // There could potentially be more than one
155            // coordinate for this "row".
156            // @TODO - handle lists of coordinates as well.
157            foreach ( $coordinatesFields as $coordinatesField ) {
158                $coordinatesField = str_replace( ' ', '_', $coordinatesField );
159                $latValue = $valuesRow[$coordinatesField . '  lat'] ?? null;
160                $lonValue = $valuesRow[$coordinatesField . '  lon'] ?? null;
161                if ( $latValue != '' && $lonValue != '' ) {
162                    $nameValue = array_shift( $valuesTable[$i] );
163                    // @TODO - enforce the existence of a field
164                    // besides the coordinates field(s).
165                    $firstValue = array_shift( $displayedValuesForRow );
166                    $valuesForMap[] = self::getMapPointValues( $nameValue, $firstValue, $latValue, $lonValue, $displayedValuesForRow, $displayParams, $i );
167                }
168            }
169
170            if ( $latField !== null && $lonField !== null ) {
171                $latValue = $valuesRow[$latField] ?? null;
172                $lonValue = $valuesRow[$lonField] ?? null;
173                $urlValue = $valuesRow[$urlField] ?? null;
174                if ( $latValue != '' && $lonValue != '' ) {
175                    $nameValue = array_shift( $valuesTable[$i] );
176                    $titleValue = array_shift( $displayedValuesForRow );
177                    $urlUtils = MediaWikiServices::getInstance()->getUrlUtils();
178                    $hrefRegExp = '/^(' . $urlUtils->validProtocols() . ')[^\s]+$/';
179                    if ( $urlValue !== null && preg_match( $hrefRegExp, $urlValue ) ) {
180                        $titleValue = Html::element( 'a', [ 'href' => $urlValue ], $titleValue );
181                    }
182                    $valuesForMap[] = self::getMapPointValues( $nameValue, $titleValue, $latValue, $lonValue, $displayedValuesForRow, $displayParams, $i );
183                }
184            }
185        }
186
187        $service = self::$mappingService;
188        $jsonData = json_encode( $valuesForMap, JSON_NUMERIC_CHECK | JSON_HEX_TAG );
189        $divID = "mapCanvas" . self::$mapNumber++;
190
191        if ( $service == 'Leaflet' && array_key_exists( 'image', $displayParams ) ) {
192            $fileName = $displayParams['image'];
193            $imageData = $this->getImageData( $fileName );
194            if ( $imageData == null ) {
195                $fileName = null;
196            } else {
197                [ $imageWidth, $imageHeight, $imageURL ] = $imageData;
198            }
199        } else {
200            $fileName = null;
201        }
202
203        $height = CargoUtils::getCSSSize( $displayParams, 'height', null );
204        $width = CargoUtils::getCSSSize( $displayParams, 'width', null );
205
206        if ( $fileName !== null ) {
207            // Do some scaling of the image, if necessary.
208            if ( $height !== null && $width !== null ) {
209                // Reduce image if it doesn't fit into the
210                // assigned rectangle.
211                $heightRatio = (int)$height / $imageHeight;
212                $widthRatio = (int)$width / $imageWidth;
213                $smallerRatio = min( $heightRatio, $widthRatio );
214                if ( $smallerRatio < 1 ) {
215                    $imageHeight *= $smallerRatio;
216                    $imageWidth *= $smallerRatio;
217                }
218            } else {
219                // Reduce image if it's too big.
220                $maxDimension = max( $imageHeight, $imageWidth );
221                $maxAllowedSize = 1000;
222                if ( $maxDimension > $maxAllowedSize ) {
223                    $imageHeight *= $maxAllowedSize / $maxDimension;
224                    $imageWidth *= $maxAllowedSize / $maxDimension;
225                }
226                $height = $imageHeight . 'px';
227                $width = $imageWidth . 'px';
228            }
229        } else {
230            if ( $height == null ) {
231                $height = "400px";
232            }
233            if ( $width == null ) {
234                $width = "700px";
235            }
236        }
237
238        // The 'map data' element does double duty: it holds the full
239        // set of map data, as well as, in the tag attributes,
240        // settings related to the display, including the mapping
241        // service to use.
242        $mapDataAttrs = [
243            'class' => 'cargoMapData',
244            'style' => 'display: none',
245            'data-mapping-service' => $service
246        ];
247        if ( array_key_exists( 'zoom', $displayParams ) && $displayParams['zoom'] != '' ) {
248            $mapDataAttrs['data-zoom'] = $displayParams['zoom'];
249        }
250        if ( array_key_exists( 'center', $displayParams ) && $displayParams['center'] != '' ) {
251            $mapDataAttrs['data-center'] = $displayParams['center'];
252        }
253        if ( array_key_exists( 'cluster', $displayParams ) ) {
254            $mapDataAttrs['data-cluster'] = strtolower( $displayParams['cluster'] );
255        }
256        if ( $fileName !== null ) {
257            $mapDataAttrs['data-image-path'] = $imageURL;
258            $mapDataAttrs['data-height'] = $imageHeight;
259            $mapDataAttrs['data-width'] = $imageWidth;
260        }
261
262        $mapData = Html::element( 'span', $mapDataAttrs, $jsonData );
263
264        $mapCanvasAttrs = [
265            'class' => 'mapCanvas',
266            'style' => "height: $height; width: $width;",
267            'id' => $divID,
268        ];
269        $mapCanvas = Html::rawElement( 'div', $mapCanvasAttrs, $mapData );
270        return $mapCanvas;
271    }
272
273    public static function getMapPointValues( $nameValue, $titleValue, $latValue, $lonValue, $displayedValuesForRow, $displayParams, $rowNum ) {
274        $valuesForMapPoint = [
275            // 'name' has no formatting (like a link), while 'title' might.
276            'name' => $nameValue,
277            'title' => $titleValue,
278            'lat' => $latValue,
279            'lon' => $lonValue,
280            'otherValues' => $displayedValuesForRow
281        ];
282        if ( array_key_exists( 'icon', $displayParams ) ) {
283            $iconFileName = null;
284            if ( is_array( $displayParams['icon'] ) ) {
285                // Compound query.
286                if ( array_key_exists( $rowNum, $displayParams['icon'] ) ) {
287                    $iconFileName = $displayParams['icon'][$rowNum];
288                }
289            } else {
290                // Regular query.
291                $iconFileName = $displayParams['icon'];
292            }
293            if ( $iconFileName !== null ) {
294                $iconURL = self::getImageURL( $iconFileName );
295                if ( $iconURL !== null ) {
296                    $valuesForMapPoint['icon'] = $iconURL;
297                }
298            }
299        }
300
301        return $valuesForMapPoint;
302    }
303
304}