Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
39.74% covered (danger)
39.74%
31 / 78
50.00% covered (danger)
50.00%
3 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialMap
39.74% covered (danger)
39.74%
31 / 78
50.00% covered (danger)
50.00%
3 / 6
107.51
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 / 43
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
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
5.03
 link
100.00% covered (success)
100.00%
6 / 6
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( 'div', [ 'id' => 'mw-specialMap-container', 'class' => 'thumb' ],
60                Html::rawElement( 'div', [ 'class' => 'thumbinner' ],
61                    Html::rawElement( 'div', [ 'id' => 'mw-specialMap-inner' ],
62                        Html::element( 'img', [
63                            'alt' => $this->msg( 'kartographer-specialmap-world' ),
64                            'height' => 256,
65                            'width' => 256,
66                            'src' => $this->getWorldMapUrl(),
67                            'srcset' => $this->getWorldMapSrcset()
68                        ] ) .
69                        $markerHtml .
70                        $attributions
71                    ) .
72                    Html::rawElement( 'div',
73                        [ 'id' => 'mw-specialMap-caption', 'class' => 'thumbcaption' ],
74                        Html::element( 'span', [ 'id' => 'mw-specialMap-icon' ] ) .
75                        Html::element( 'span', [ 'id' => 'mw-specialMap-coords' ], $coordText )
76                    )
77                )
78            )
79        );
80    }
81
82    /**
83     * Parses subpage parameter to this special page into zoom / lat /lon
84     *
85     * @param string|null $par
86     * @return array|null
87     */
88    private function parseSubpage( ?string $par ): ?array {
89        if ( !$par || !preg_match(
90            '#^(?<zoom>\d*)/(?<lat>-?\d+(\.\d+)?)/(?<lon>-?\d+(\.\d+)?)(/(?<lang>[a-zA-Z\d-]+))?$#',
91            $par,
92            $matches
93        ) ) {
94            return null;
95        }
96
97        if ( class_exists( Globe::class ) ) {
98            $globe = new Globe( 'earth' );
99            if ( !$globe->coordinatesAreValid( $matches['lat'], $matches['lon'] ) ) {
100                return null;
101            }
102        }
103
104        return [
105            'zoom' => (int)$matches['zoom'],
106            'lat' => (float)$matches['lat'],
107            'lon' => (float)$matches['lon'],
108            'lang' => $matches['lang'] ?? 'local',
109        ];
110    }
111
112    /**
113     * Return the image url for a world map
114     * @param string $factor HiDPI image factor (example: @2x)
115     * @return string
116     */
117    private function getWorldMapUrl( string $factor = '' ): string {
118        return $this->getConfig()->get( 'KartographerMapServer' ) . '/' .
119            $this->getConfig()->get( 'KartographerDfltStyle' ) .
120            '/0/0/0' . $factor . '.png';
121    }
122
123    /**
124     * Return srcset attribute value for world map image url
125     * @return string|null
126     */
127    private function getWorldMapSrcset(): ?string {
128        $scales = $this->getConfig()->get( 'KartographerSrcsetScales' );
129        if ( !$scales || !$this->getConfig()->get( MainConfigNames::ResponsiveImages ) ) {
130            return null;
131        }
132
133        // For now only support 2x, not 1.5. Saves some bytes...
134        $scales = array_intersect( $scales, [ 2 ] );
135        $srcSets = [];
136        foreach ( $scales as $scale ) {
137            $scaledImgUrl = $this->getWorldMapUrl( "@{$scale}x" );
138            $srcSets[] = "$scaledImgUrl {$scale}x";
139        }
140        return implode( ', ', $srcSets ) ?: null;
141    }
142
143    /**
144     * @param float|null $lat
145     * @param float|null $lon
146     * @param int|null $zoom
147     * @param string $lang Optional language code. Defaults to 'local'
148     * @return string|null
149     */
150    public static function link( ?float $lat, ?float $lon, ?int $zoom, $lang = 'local' ): ?string {
151        if ( $lat === null || $lon === null ) {
152            return null;
153        }
154
155        $subpage = (int)$zoom . '/' . $lat . '/' . $lon;
156        if ( $lang && $lang !== 'local' ) {
157            $subpage .= '/' . $lang;
158        }
159        return SpecialPage::getTitleFor( 'Map', $subpage )->getLocalURL();
160    }
161}