Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
85.37% covered (warning)
85.37%
70 / 82
42.86% covered (danger)
42.86%
3 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
MapFrameAttributeGenerator
85.37% covered (warning)
85.37%
70 / 82
42.86% covered (danger)
42.86%
3 / 7
24.66
0.00% covered (danger)
0.00%
0 / 1
 __construct
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
2.03
 getContainerClasses
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getThumbClasses
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 prepareAttrs
88.89% covered (warning)
88.89%
24 / 27
0.00% covered (danger)
0.00%
0 / 1
6.05
 prepareImgAttrs
92.00% covered (success)
92.00%
23 / 25
0.00% covered (danger)
0.00%
0 / 1
6.02
 getUrlAttrs
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 getSrcSet
33.33% covered (danger)
33.33%
3 / 9
0.00% covered (danger)
0.00%
0 / 1
12.41
1<?php
2
3namespace Kartographer\Tag;
4
5use Kartographer\Special\SpecialMap;
6use MediaWiki\Config\Config;
7use MediaWiki\Json\FormatJson;
8use MediaWiki\MainConfigNames;
9
10/**
11 * @license MIT
12 */
13class MapFrameAttributeGenerator {
14
15    private const ALIGN_CLASSES = [
16        'left' => 'floatleft',
17        'center' => 'center',
18        'right' => 'floatright',
19    ];
20
21    private const THUMB_ALIGN_CLASSES = [
22        'left' => 'tleft',
23        'center' => 'tnone center',
24        'right' => 'tright',
25        'none' => 'tnone',
26    ];
27
28    private MapTagArgumentValidator $args;
29    private Config $config;
30    public string $cssWidth;
31
32    public function __construct( MapTagArgumentValidator $args, Config $config ) {
33        $this->args = $args;
34        $this->config = $config;
35
36        if ( $this->args->width === 'full' ) {
37            $this->cssWidth = '100%';
38        } else {
39            $this->cssWidth = $this->args->width . 'px';
40        }
41    }
42
43    /**
44     * @return string[]
45     */
46    private function getContainerClasses(): array {
47        return [
48            'mw-kartographer-container',
49            ...( $this->args->width === 'full' ? [ 'mw-kartographer-full' ] : [] ),
50        ];
51    }
52
53    /**
54     * @return string[]
55     */
56    public function getThumbClasses(): array {
57        return [
58            ...$this->getContainerClasses(),
59            'thumb',
60            self::THUMB_ALIGN_CLASSES[$this->args->align],
61        ];
62    }
63
64    public function prepareAttrs(): array {
65        $attrs = [
66            // T359082: Temporarily disable dark mode unless we have a better idea
67            'class' => [ 'mw-kartographer-map', 'notheme' ],
68            // We need dimensions for when there is no img (editpreview or no staticmap)
69            // because an <img> element with permanent failing src has either:
70            // - intrinsic dimensions of 0x0, when alt=''
71            // - intrinsic dimensions of alt size
72            'style' => "width: $this->cssWidth; height: {$this->args->height}px;",
73            // Attributes starting with "data-mw" are banned from user content in Sanitizer;
74            // we add such an attribute here (by default empty) so that its presence can be
75            // checked later to guarantee that they were generated by Kartographer
76            // Warning: This attribute is also checked in Wikibase and should be modified there as
77            // well if necessary!
78            'data-mw-kartographer' => 'mapframe',
79            'data-style' => $this->args->mapStyle,
80            'data-width' => $this->args->width,
81            'data-height' => $this->args->height,
82        ];
83
84        if ( $this->args->zoom !== null ) {
85            $attrs['data-zoom'] = $this->args->zoom;
86        }
87
88        if ( $this->args->hasCoordinates() ) {
89            $attrs['data-lat'] = $this->args->lat;
90            $attrs['data-lon'] = $this->args->lon;
91        }
92
93        if ( $this->args->specifiedLangCode !== null ) {
94            $attrs['data-lang'] = $this->args->specifiedLangCode;
95        }
96
97        if ( $this->args->showGroups ) {
98            $attrs['data-overlays'] = FormatJson::encode( $this->args->showGroups, false,
99                FormatJson::ALL_OK );
100        }
101
102        $attrs['href'] = SpecialMap::link( $this->args->lat, $this->args->lon, $this->args->zoom,
103            $this->args->getLanguageCodeWithDefaultFallback() );
104
105        if ( $this->args->frameless ) {
106            $attrs['class'] = [
107                ...$attrs['class'],
108                ...$this->getContainerClasses(),
109                ...(array)( self::ALIGN_CLASSES[$this->args->align] ?? [] ),
110            ];
111        }
112
113        return $attrs;
114    }
115
116    /**
117     * @param bool $serverMayRenderOverlays If the map server should attempt to render GeoJSON
118     *  overlays via their group id
119     * @param string $pagetitle
120     * @param ?int $revisionId
121     * @return array
122     */
123    public function prepareImgAttrs( bool $serverMayRenderOverlays, string $pagetitle, ?int $revisionId ): array {
124        $mapServer = $this->config->get( 'KartographerMapServer' );
125        $fullWidth = $this->config->get( 'KartographerStaticFullWidth' );
126        $staticWidth = $this->args->width === 'full' ? $fullWidth : (int)$this->args->width;
127
128        $imgUrlParams = [
129            'lang' => $this->args->getLanguageCodeWithDefaultFallback(),
130        ];
131        if ( $this->args->showGroups && $serverMayRenderOverlays ) {
132            // Groups are not available to the static map renderer
133            // before the page was saved, can only be applied via JS
134            $imgUrlParams += self::getUrlAttrs( $this->config, $pagetitle, $revisionId, $this->args->showGroups );
135        }
136
137        $imgUrl = "$mapServer/img/{$this->args->mapStyle}," .
138            ( $this->args->zoom ?? 'a' ) . ',' .
139            ( $this->args->lat ?? 'a' ) . ',' .
140            ( $this->args->lon ?? 'a' ) .
141            ",{$staticWidth}x{$this->args->height}";
142
143        $imgAttrs = [
144            'src' => "$imgUrl.png?" . wfArrayToCgi( $imgUrlParams ),
145            'width' => $staticWidth,
146            'height' => $this->args->height,
147            'decoding' => 'async',
148        ];
149
150        $srcSet = $this->getSrcSet( $imgUrl, $imgUrlParams );
151        if ( $srcSet ) {
152            $imgAttrs['srcset'] = $srcSet;
153        }
154
155        if ( $this->args->alt !== '' ) {
156            $imgAttrs['alt'] = $this->args->alt;
157        }
158
159        return $imgAttrs;
160    }
161
162    /**
163     * @param Config $config
164     * @param string $title
165     * @param int|null $revId
166     * @param array $groupId
167     * @return array
168     */
169    public static function getUrlAttrs( Config $config, string $title, ?int $revId, array $groupId ): array {
170        return [
171            'domain' => $config->get( 'KartographerMediaWikiInternalUrl' ) ??
172                $config->get( MainConfigNames::ServerName ),
173            'title' => $title,
174            'revid' => $revId,
175            'groups' => implode( ',', $groupId )
176        ];
177    }
178
179    private function getSrcSet( string $imgUrl, array $imgUrlParams = [] ): ?string {
180        $scales = $this->config->get( 'KartographerSrcsetScales' );
181        if ( !$scales || !$this->config->get( MainConfigNames::ResponsiveImages ) ) {
182            return null;
183        }
184
185        // For now only support 2x, not 1.5. Saves some bytes...
186        $scales = array_intersect( $scales, [ 2 ] );
187        $srcSets = [];
188        foreach ( $scales as $scale ) {
189            $scaledImgUrl = "$imgUrl@{$scale}x.png?" . wfArrayToCgi( $imgUrlParams );
190            $srcSets[] = "$scaledImgUrl {$scale}x";
191        }
192        return implode( ', ', $srcSets ) ?: null;
193    }
194}