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