Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
42.86% covered (danger)
42.86%
33 / 77
66.67% covered (warning)
66.67%
4 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialMap
42.86% covered (danger)
42.86%
33 / 77
66.67% covered (warning)
66.67%
4 / 6
94.64
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 execute
0.00% covered (danger)
0.00%
0 / 41
0.00% covered (danger)
0.00%
0 / 1
12
 parseSubpage
81.25% covered (warning)
81.25%
13 / 16
0.00% covered (danger)
0.00%
0 / 1
5.16
 getWorldMapUrl
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getWorldMapSrcset
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
5
 link
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
5
1<?php
2
3namespace Kartographer\Special;
4
5use GeoData\Globe;
6use Kartographer\CoordFormatter;
7use MediaWiki\Html\Html;
8use MediaWiki\MainConfigNames;
9use MediaWiki\SpecialPage\SpecialPage;
10use MediaWiki\SpecialPage\UnlistedSpecialPage;
11
12/**
13 * Special page that works as a fallback destination for non-JS users
14 * who click on map links. It displays a world map with a dot for the given location.
15 * URL format: Special:Map/<zoom>/<lat>/<lon>
16 * Zoom isn't used anywhere yet.
17 *
18 * @license MIT
19 */
20class SpecialMap extends UnlistedSpecialPage {
21
22    /**
23     * @param string $name
24     */
25    public function __construct( $name = 'Map' ) {
26        parent::__construct( $name );
27    }
28
29    /** @inheritDoc */
30    public function execute( $par ) {
31        $this->setHeaders();
32        $output = $this->getOutput();
33        $output->addModuleStyles( 'ext.kartographer.specialMap' );
34        $mapServer = $this->getConfig()->get( 'KartographerMapServer' );
35        if ( $mapServer !== null ) {
36            $output->getCSP()->addDefaultSrc( $mapServer );
37        }
38
39        $coord = $this->parseSubpage( $par );
40        if ( !$coord ) {
41            $coordText = $this->msg( 'kartographer-specialmap-invalid-coordinates' )->text();
42            $markerHtml = '';
43        } else {
44            [ 'lat' => $lat, 'lon' => $lon ] = $coord;
45            $coordText = ( new CoordFormatter( $lat, $lon ) )->format( $this->getLanguage() );
46            [ $x, $y ] = EPSG3857::latLonToPoint( [ $lat, $lon ] );
47            $markerHtml = Html::element( 'div',
48                [
49                    'id' => 'mw-specialMap-marker',
50                    'style' => "left:{$x}px; top:{$y}px;"
51                ]
52            );
53        }
54
55        $attributions = Html::rawElement( 'div', [ 'id' => 'mw-specialMap-attributions' ],
56            $this->msg( 'kartographer-attribution' )->parse() );
57
58        $this->getOutput()->addHTML(
59            Html::rawElement( 'figure', [ 'id' => 'mw-specialMap-container' ],
60                Html::rawElement( 'div', [ 'id' => 'mw-specialMap-inner' ],
61                    Html::element( 'img', [
62                        'alt' => $this->msg( 'kartographer-specialmap-world' )->text(),
63                        'height' => 256,
64                        'width' => 256,
65                        'src' => $this->getWorldMapUrl(),
66                        'srcset' => $this->getWorldMapSrcset()
67                    ] ) .
68                    $markerHtml .
69                    $attributions
70                ) .
71                Html::rawElement( 'figcaption',
72                    [ 'id' => 'mw-specialMap-caption' ],
73                    Html::element( 'span', [ 'id' => 'mw-specialMap-icon' ] ) .
74                    Html::element( 'span', [ 'id' => 'mw-specialMap-coords' ], $coordText )
75                )
76            )
77        );
78    }
79
80    /**
81     * Parses subpage parameter to this special page into zoom / lat /lon
82     *
83     * @param string|null $par
84     * @return array|null
85     */
86    private function parseSubpage( ?string $par ): ?array {
87        if ( !$par || !preg_match(
88            '#^(?<zoom>\d*)/(?<lat>-?\d+(\.\d+)?)/(?<lon>-?\d+(\.\d+)?)(/(?<lang>[a-zA-Z\d-]+))?$#',
89            $par,
90            $matches
91        ) ) {
92            return null;
93        }
94
95        if ( class_exists( Globe::class ) ) {
96            $globe = new Globe( 'earth' );
97            if ( !$globe->coordinatesAreValid( $matches['lat'], $matches['lon'] ) ) {
98                return null;
99            }
100        }
101
102        return [
103            'zoom' => (int)$matches['zoom'],
104            'lat' => (float)$matches['lat'],
105            'lon' => (float)$matches['lon'],
106            'lang' => $matches['lang'] ?? 'local',
107        ];
108    }
109
110    /**
111     * Return the image url for a world map
112     * @param string $factor HiDPI image factor (example: @2x)
113     * @return string
114     */
115    private function getWorldMapUrl( string $factor = '' ): string {
116        return $this->getConfig()->get( 'KartographerMapServer' ) . '/' .
117            $this->getConfig()->get( 'KartographerDfltStyle' ) .
118            "/0/0/0$factor.png";
119    }
120
121    /**
122     * Return srcset attribute value for world map image url
123     * @return string|null
124     */
125    private function getWorldMapSrcset(): ?string {
126        $scales = $this->getConfig()->get( 'KartographerSrcsetScales' );
127        if ( !$scales || !$this->getConfig()->get( MainConfigNames::ResponsiveImages ) ) {
128            return null;
129        }
130
131        // For now only support 2x, not 1.5. Saves some bytes...
132        $scales = array_intersect( $scales, [ 2 ] );
133        $srcSets = [];
134        foreach ( $scales as $scale ) {
135            $scaledImgUrl = $this->getWorldMapUrl( "@{$scale}x" );
136            $srcSets[] = "$scaledImgUrl {$scale}x";
137        }
138        return implode( ', ', $srcSets ) ?: null;
139    }
140
141    /**
142     * @param float|null $lat
143     * @param float|null $lon
144     * @param int|null $zoom
145     * @param string $lang Optional language code. Defaults to 'local'
146     * @return string|null
147     */
148    public static function link(
149        ?float $lat,
150        ?float $lon,
151        ?int $zoom,
152        string $lang = 'local',
153    ): ?string {
154        if ( $lat === null || $lon === null ) {
155            return null;
156        }
157        $zoom ??= 0;
158
159        $subpage = "$zoom/$lat/$lon";
160        if ( $lang && $lang !== 'local' ) {
161            $subpage .= "/$lang";
162        }
163        return SpecialPage::getTitleFor( 'Map', $subpage )->getLocalURL();
164    }
165}