• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

keplergl / kepler.gl / 30137028063

25 Jul 2026 12:45AM UTC coverage: 54.409% (+0.1%) from 54.295%
30137028063

push

github

web-flow
fix: attribution fixes and refactoring (#3550)

* fix: fix attribution issues

Signed-off-by: Ihor Dykhta <ihordykhta@Ihors-MacBook-Pro.local>

* fix: attribution fixes and refactoring

Signed-off-by: Ihor Dykhta <ihordykhta@Ihors-MacBook-Pro.local>

* follow up more

Signed-off-by: Ihor Dykhta <ihordykhta@Ihors-MacBook-Pro.local>

* follow up

Signed-off-by: Ihor Dykhta <ihordykhta@Ihors-MacBook-Pro.local>

---------

Signed-off-by: Ihor Dykhta <ihordykhta@Ihors-MacBook-Pro.local>
Co-authored-by: Ihor Dykhta <ihordykhta@Ihors-MacBook-Pro.local>

7879 of 17402 branches covered (45.28%)

Branch coverage included in aggregate %.

87 of 115 new or added lines in 4 files covered. (75.65%)

15872 of 26251 relevant lines covered (60.46%)

71.8 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

59.49
/src/utils/src/map-style-utils/mapbox-utils.ts
1
// SPDX-License-Identifier: MIT
2
// Copyright contributors to the kepler.gl project
3

4
// import {isMapboxURL, transformMapboxUrl} from 'maplibregl-mapbox-request-transformer';
5

6
import {MAP_LIB_OPTIONS, BaseMapLibraryType} from '@kepler.gl/constants';
7

8
/**
9
 * Determines whether a Map Style is using Mapbox Tiles
10
 * @param {any} mapStyle the mapStyle to check
11
 * @returns true if the style is using Mapbox tiles
12
 */
13
export function isStyleUsingMapboxTiles(mapStyle) {
14
  const sources = mapStyle.stylesheet?.sources || {};
4✔
15

16
  return Object.keys(sources).some(sourceId => {
4✔
17
    const {url, tiles} = sources[sourceId] || {};
5!
18

19
    if (url) {
5✔
20
      return url.toLowerCase().startsWith('mapbox://');
3✔
21
    }
22

23
    if (tiles) {
2✔
24
      return tiles.some(tileUrl => tileUrl.toLowerCase().startsWith('mapbox://'));
1✔
25
    }
26

27
    return false;
1✔
28
  });
29
}
30

31
export function isStyleUsingOpenStreetMapTiles(mapStyle: any) {
32
  const sources = mapStyle?.stylesheet?.sources || {};
×
33
  return Object.keys(sources).some(sourceId => {
×
34
    const {attribution} = sources[sourceId] || {};
×
35
    if (typeof attribution === 'string') {
×
36
      return attribution.includes('openstreetmap.org');
×
37
    }
38
    return false;
×
39
  });
40
}
41

42
/**
43
 * Minimal structural contract for the MapLibre/Mapbox map instance used by the
44
 * attribution helpers below. Only the members actually read are declared;
45
 * everything is optional because these helpers may be called before the map is
46
 * fully initialized (guarded by optional chaining + try/catch at runtime).
47
 *
48
 * `getSource` returns `any` on purpose: the concrete MapLibre/Mapbox source
49
 * union (GeoJSONSource, VectorSource, …) is not structurally assignable to a
50
 * `{attribution?}` shape, and we only ever read an optional `attribution`
51
 * string off it. Declaring the two getters still catches passing a non-map.
52
 */
53
export interface AttributionMapLike {
54
  getStyle?: () => {sources?: Record<string, unknown>} | undefined | null;
55
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
56
  getSource?: (id: string) => any;
57
}
58

59
/**
60
 * Checks resolved map sources for OpenStreetMap attribution.
61
 * CARTO basemaps don't have attribution in the raw style JSON;
62
 * it only appears after MapLibre resolves the TileJSON.
63
 * Uses map.getSource() to access the resolved source with TileJSON metadata.
64
 */
65
export function mapHasOpenStreetMapAttribution(map?: AttributionMapLike | null): boolean {
66
  try {
×
67
    const style = map?.getStyle?.();
×
68
    if (!style?.sources) return false;
×
69
    return Object.keys(style.sources).some(sourceId => {
×
NEW
70
      const source = map?.getSource?.(sourceId);
×
71
      const attribution = source?.attribution;
×
72
      if (typeof attribution === 'string') {
×
73
        return attribution.toLowerCase().includes('openstreetmap');
×
74
      }
75
      return false;
×
76
    });
77
  } catch {
78
    return false;
×
79
  }
80
}
81

82
/**
83
 * Collects the attribution strings declared by the resolved map sources.
84
 * Custom basemap styles (e.g. OpenFreeMap, OpenMapTiles) declare their own
85
 * attribution in the source's TileJSON, which only becomes available after
86
 * MapLibre resolves it. Unlike {@link mapHasOpenStreetMapAttribution}, this
87
 * preserves the full attribution text instead of collapsing it to a boolean,
88
 * so custom attributions are not lost.
89
 * @param map the resolved MapLibre/Mapbox map instance
90
 * @returns de-duplicated list of attribution HTML strings, order preserved
91
 */
92
export function getBaseMapAttributions(map?: AttributionMapLike | null): string[] {
93
  try {
7✔
94
    const style = map?.getStyle?.();
7✔
95
    if (!style?.sources) return [];
6✔
96
    const seen = new Set<string>();
4✔
97
    Object.keys(style.sources).forEach(sourceId => {
4✔
98
      const source = map?.getSource?.(sourceId);
8✔
99
      const attribution = source?.attribution;
8✔
100
      if (typeof attribution === 'string') {
8✔
101
        const trimmed = attribution.trim();
6✔
102
        if (trimmed) {
6✔
103
          seen.add(trimmed);
5✔
104
        }
105
      }
106
    });
107
    return [...seen];
4✔
108
  } catch {
109
    return [];
1✔
110
  }
111
}
112

113
/**
114
 * Transform mapbox protocol so can be used with maplibre
115
 * @param mapboxKey mapbox api key
116
 * @returns transformed url
117
 */
118
export const transformRequest = (
17✔
119
  _mapboxKey: string
120
): ((url: string, resourceType: string) => {url: string}) => {
121
  return (url: string, _resourceType: string) => {
45✔
122
    /*
123
    if (isMapboxURL(url)) {
124
      // ! TODO - use MapBox directly?
125
      return transformMapboxUrl(url, resourceType, mapboxKey);
126
    }
127
    */
128

129
    return {url};
×
130
  };
131
};
132

133
type StyleWithSources = {
134
  sources?: Record<string, any>;
135
};
136

137
export const getBaseMapLibrary = (baseMapStyle?: {
17✔
138
  url?: string | null;
139
  style?: any;
140
}): BaseMapLibraryType => {
141
  if (baseMapStyle) {
135✔
142
    if (baseMapStyle.url?.startsWith('mapbox://') || baseMapStyle.url?.includes('mapbox.com')) {
129!
143
      return MAP_LIB_OPTIONS.MAPBOX;
×
144
    }
145

146
    if ((baseMapStyle.style as StyleWithSources)?.sources?.['mapbox'])
129!
147
      return MAP_LIB_OPTIONS.MAPBOX;
×
148
  }
149

150
  return MAP_LIB_OPTIONS.MAPLIBRE;
135✔
151
};
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc