Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
39.74% |
31 / 78 |
|
50.00% |
3 / 6 |
CRAP | |
0.00% |
0 / 1 |
SpecialMap | |
39.74% |
31 / 78 |
|
50.00% |
3 / 6 |
107.51 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
execute | |
0.00% |
0 / 43 |
|
0.00% |
0 / 1 |
12 | |||
parseSubpage | |
81.25% |
13 / 16 |
|
0.00% |
0 / 1 |
5.16 | |||
getWorldMapUrl | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
getWorldMapSrcset | |
88.89% |
8 / 9 |
|
0.00% |
0 / 1 |
5.03 | |||
link | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
5 |
1 | <?php |
2 | |
3 | namespace Kartographer\Special; |
4 | |
5 | use GeoData\Globe; |
6 | use Kartographer\CoordFormatter; |
7 | use MediaWiki\Html\Html; |
8 | use MediaWiki\MainConfigNames; |
9 | use MediaWiki\SpecialPage\SpecialPage; |
10 | use 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 | */ |
20 | class 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 | } |