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