Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
41.77% |
33 / 79 |
|
66.67% |
4 / 6 |
CRAP | |
0.00% |
0 / 1 |
| SpecialMap | |
41.77% |
33 / 79 |
|
66.67% |
4 / 6 |
98.97 | |
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 | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
5 | |||
| link | |
100.00% |
7 / 7 |
|
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' )->text(), |
| 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 | $zoom ??= 0; |
| 155 | |
| 156 | $subpage = "$zoom/$lat/$lon"; |
| 157 | if ( $lang && $lang !== 'local' ) { |
| 158 | $subpage .= "/$lang"; |
| 159 | } |
| 160 | return SpecialPage::getTitleFor( 'Map', $subpage )->getLocalURL(); |
| 161 | } |
| 162 | } |