Skip to content

Commit f096bc9

Browse files
authored
fix: updates to attribution logic for tiled layers (#3375)
* fix: updates to attribution logic for tiled layers Signed-off-by: Ihor Dykhta <dikhta.igor@gmail.com> * lint Signed-off-by: Ihor Dykhta <dikhta.igor@gmail.com> * fix attribution selector Signed-off-by: Ihor Dykhta <dikhta.igor@gmail.com> * enabled > Signed-off-by: Ihor Dykhta <dikhta.igor@gmail.com> * fix version Signed-off-by: Ihor Dykhta <dikhta.igor@gmail.com> --------- Signed-off-by: Ihor Dykhta <dikhta.igor@gmail.com>
1 parent b0fc760 commit f096bc9

13 files changed

Lines changed: 429 additions & 141 deletions

File tree

src/components/src/kepler-gl.tsx

Lines changed: 92 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -162,39 +162,43 @@ export const mapStateSelector = createSelector(
162162
}
163163
);
164164

165-
export const mapFieldsSelector = (props: KeplerGLProps, index = 0) => ({
166-
getMapboxRef: props.getMapboxRef,
167-
mapboxApiAccessToken: props.mapboxApiAccessToken,
168-
mapboxApiUrl: props.mapboxApiUrl ? props.mapboxApiUrl : DEFAULT_KEPLER_GL_PROPS.mapboxApiUrl,
169-
mapState: mapStateSelector(props, index),
170-
datasetAttributions: attributionSelector(props).sources,
171-
mapStyle: props.mapStyle,
172-
onDeckInitialized: props.onDeckInitialized,
173-
onViewStateChange: props.onViewStateChange,
174-
onMouseMove: props.onMouseMove,
175-
deckGlProps: props.deckGlProps,
176-
uiStateActions: props.uiStateActions,
177-
visStateActions: props.visStateActions,
178-
mapStateActions: props.mapStateActions,
179-
180-
// visState
181-
visState: props.visState,
182-
183-
// uiState
184-
activeSidePanel: props.uiState.activeSidePanel,
185-
mapControls: props.uiState.mapControls,
186-
readOnly: props.uiState.readOnly,
187-
locale: props.uiState.locale,
188-
isLoadingIndicatorVisible: Number(props.visState.loadingIndicatorValue) > 0,
189-
sidePanelWidth: props.sidePanelWidth ? props.sidePanelWidth : DEFAULT_KEPLER_GL_PROPS.width,
190-
191-
// mapStyle
192-
topMapContainerProps: props.topMapContainerProps,
193-
bottomMapContainerProps: props.bottomMapContainerProps,
194-
195-
// transformRequest for Mapbox basemaps
196-
transformRequest: props.transformRequest
197-
});
165+
export const mapFieldsSelector = (props: KeplerGLProps, index = 0) => {
166+
const attribution = attributionSelector(props);
167+
return {
168+
getMapboxRef: props.getMapboxRef,
169+
mapboxApiAccessToken: props.mapboxApiAccessToken,
170+
mapboxApiUrl: props.mapboxApiUrl ? props.mapboxApiUrl : DEFAULT_KEPLER_GL_PROPS.mapboxApiUrl,
171+
mapState: mapStateSelector(props, index),
172+
datasetAttributions: attribution.sources,
173+
attributionLogos: attribution.logos,
174+
mapStyle: props.mapStyle,
175+
onDeckInitialized: props.onDeckInitialized,
176+
onViewStateChange: props.onViewStateChange,
177+
onMouseMove: props.onMouseMove,
178+
deckGlProps: props.deckGlProps,
179+
uiStateActions: props.uiStateActions,
180+
visStateActions: props.visStateActions,
181+
mapStateActions: props.mapStateActions,
182+
183+
// visState
184+
visState: props.visState,
185+
186+
// uiState
187+
activeSidePanel: props.uiState.activeSidePanel,
188+
mapControls: props.uiState.mapControls,
189+
readOnly: props.uiState.readOnly,
190+
locale: props.uiState.locale,
191+
isLoadingIndicatorVisible: Number(props.visState.loadingIndicatorValue) > 0,
192+
sidePanelWidth: props.sidePanelWidth ? props.sidePanelWidth : DEFAULT_KEPLER_GL_PROPS.width,
193+
194+
// mapStyle
195+
topMapContainerProps: props.topMapContainerProps,
196+
bottomMapContainerProps: props.bottomMapContainerProps,
197+
198+
// transformRequest for Mapbox basemaps
199+
transformRequest: props.transformRequest
200+
};
201+
};
198202

199203
export function getVisibleDatasets(datasets) {
200204
// We don't want Geocoder dataset to be present in SidePanel dataset list
@@ -343,16 +347,65 @@ export const datasetAttributionSelector = createSelector(
343347
);
344348

345349
/**
346-
* Deduplicated dataset and layer text attributions and logos.
347-
* Returns text attributions and logos to display.
350+
* Collect unique layer-level attributions from visible layers.
351+
* Builds a serialized cache key from attribution values so createSelector
352+
* can detect in-place mutations to layer.meta.
348353
*/
354+
const layerAttributionKeySelector = (state: any): string => {
355+
const layers: any[] = state.visState?.layers;
356+
if (!layers) return '';
357+
let key = '';
358+
for (const layer of layers) {
359+
if (layer.config?.isVisible && layer.meta?.attribution) {
360+
const a = layer.meta.attribution;
361+
key += `${a.title}\0${a.url}\0${a.logoUrl ?? ''}\0`;
362+
}
363+
}
364+
return key;
365+
};
366+
367+
export const layerAttributionSelector = createSelector(
368+
[(state: any) => state.visState?.layers, layerAttributionKeySelector],
369+
(layers: any[]): AttributionWithStyle[] => {
370+
const attributions: AttributionWithStyle[] = [];
371+
if (!layers) return attributions;
372+
for (const layer of layers) {
373+
if (layer.config?.isVisible && layer.meta?.attribution) {
374+
const attr = layer.meta.attribution;
375+
if (attr.title && !attributions.find(a => a.title === attr.title && a.url === attr.url)) {
376+
attributions.push(attr);
377+
}
378+
}
379+
}
380+
return attributions;
381+
}
382+
);
383+
349384
export const attributionSelector = createSelector(
350-
[datasetAttributionSelector],
351-
datasetAttributions => {
352-
// TODO collect attributions from layers, and merge with dataset attributions here
353-
const uniqueTextAttributions = datasetAttributions;
385+
[datasetAttributionSelector, layerAttributionSelector],
386+
(
387+
datasetAttributions: DatasetAttribution[],
388+
layerAttributions: AttributionWithStyle[]
389+
): {
390+
sources: DatasetAttribution[];
391+
logos: AttributionWithStyle[];
392+
} => {
393+
const uniqueTextAttributions: DatasetAttribution[] = [...datasetAttributions];
354394
const logos: AttributionWithStyle[] = [];
355395

396+
layerAttributions.forEach(layerAttribution => {
397+
if (
398+
!uniqueTextAttributions.find(
399+
a => a.title === layerAttribution.title && a.url === layerAttribution.url
400+
)
401+
) {
402+
uniqueTextAttributions.push({title: layerAttribution.title, url: layerAttribution.url});
403+
}
404+
if (layerAttribution.logoUrl && !logos.find(a => a.logoUrl === layerAttribution.logoUrl)) {
405+
logos.push(layerAttribution);
406+
}
407+
});
408+
356409
return {sources: uniqueTextAttributions, logos};
357410
}
358411
);

src/components/src/map-container.tsx

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
AggregatedBin
3636
} from '@kepler.gl/layers';
3737
import {
38+
AttributionWithStyle,
3839
DatasetAttribution,
3940
MapState,
4041
MapControls,
@@ -303,6 +304,46 @@ export const Attribution: React.FC<AttributionProps> = ({
303304
return memoizedComponents;
304305
};
305306

307+
const StyledAttributionLogoContainer = styled.div`
308+
position: absolute;
309+
bottom: 4px;
310+
left: 4px;
311+
z-index: 1;
312+
display: flex;
313+
align-items: flex-end;
314+
gap: 4px;
315+
pointer-events: auto;
316+
`;
317+
318+
const StyledLogoLink = styled.a<{$enabled: boolean}>`
319+
cursor: ${props => (props.$enabled ? 'pointer' : 'default')};
320+
display: flex;
321+
align-items: flex-end;
322+
`;
323+
324+
type AttributionLogosProps = {
325+
logos: AttributionWithStyle[];
326+
};
327+
328+
export const AttributionLogos: React.FC<AttributionLogosProps> = ({logos}) => {
329+
if (!logos?.length) return null;
330+
return (
331+
<StyledAttributionLogoContainer>
332+
{logos.map((logo, idx) => (
333+
<StyledLogoLink
334+
key={logo.logoUrl || idx}
335+
href={logo.url || undefined}
336+
{...(logo.url ? {target: '_blank', rel: 'noopener noreferrer'} : {})}
337+
$enabled={Boolean(logo.url)}
338+
style={logo.bottom ? {marginBottom: logo.bottom} : undefined}
339+
>
340+
<img src={logo.logoUrl} style={{height: logo.height || 12}} alt={logo.title} />
341+
</StyledLogoLink>
342+
))}
343+
</StyledAttributionLogoContainer>
344+
);
345+
};
346+
306347
MapContainerFactory.deps = [MapPopoverFactory, MapControlFactory, EditorFactory];
307348

308349
type MapboxStyle = string | object | undefined;
@@ -348,6 +389,7 @@ export interface MapContainerProps {
348389
transformRequest?: (url: string, resourceType?: string) => {url: string};
349390

350391
datasetAttributions?: DatasetAttribution[];
392+
attributionLogos?: AttributionWithStyle[];
351393

352394
generateMapboxLayers?: typeof generateMapboxLayers;
353395
generateDeckGLLayers?: typeof computeDeckLayers;
@@ -1098,6 +1140,7 @@ export default function MapContainerFactory(
10981140
topMapContainerProps,
10991141
theme,
11001142
datasetAttributions = [],
1143+
attributionLogos = [],
11011144
containerId = 0,
11021145
isLoadingIndicatorVisible,
11031146
activeSidePanel,
@@ -1242,6 +1285,9 @@ export default function MapContainerFactory(
12421285
baseMapLibraryConfig={baseMapLibraryConfig}
12431286
/>
12441287
) : null}
1288+
{this.props.primary ? (
1289+
<AttributionLogos logos={attributionLogos} />
1290+
) : null}
12451291
</>
12461292
);
12471293
}

src/components/src/modals/tilesets-modals/load-data-footer.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ const LoadDataFooterContainer = styled.div.attrs({
3232
display: flex;
3333
justify-content: space-between;
3434
padding: 21px 21px 0px 72px;
35-
margin: 24px -72px 0;
35+
margin: 0 -72px 0;
3636
align-items: center;
3737
`;
3838

src/components/src/modals/tilesets-modals/load-tileset.tsx

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,6 @@ const MetaInnerContainer = styled.div<MetaInnerContainerProps>`
7272
max-width: 600px;
7373
`;
7474

75-
const StyledHeaderMessage = styled.div`
76-
color: ${props => props.theme.textColorLT};
77-
font-size: 14px;
78-
`;
79-
8075
type LoadTilesetTabProps = {
8176
meta: {[key: string]: any};
8277
isAddingDatasets: boolean;
@@ -156,8 +151,6 @@ function LoadTilesetTabFactory() {
156151
<LoadTilesetTabContainer>
157152
<Container>
158153
<div>
159-
<StyledHeaderMessage>Tileset Type</StyledHeaderMessage>
160-
161154
<TilesetTypeContainer className="tileset-type">
162155
{tileTypes.map((tileType, index) => (
163156
<TilesetIcon

src/components/src/modals/tilesets-modals/tileset-raster-form.tsx

Lines changed: 33 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -389,35 +389,39 @@ const RasterTileForm: React.FC<RasterTileFormProps> = ({setResponse}) => {
389389
</TilesetInputDescription>
390390
</div>
391391
)}
392-
<div>
393-
<TilesetInputDescription>For example, try a public raster tileset:</TilesetInputDescription>
394-
<ExampleUrlsContainer>
395-
<ExampleTabs>
396-
{RASTER_TILE_EXAMPLES.map((ex, i) => (
397-
<ExampleTab
398-
key={`${ex.label}-${ex.name}`}
399-
active={exampleTab === i}
400-
onClick={() => {
401-
setExampleTab(i);
402-
onExampleClick(ex.url, ex.name);
403-
}}
404-
>
405-
{ex.name}
406-
</ExampleTab>
407-
))}
408-
</ExampleTabs>
409-
<ExampleUrl
410-
onClick={() =>
411-
onExampleClick(
412-
RASTER_TILE_EXAMPLES[exampleTab].url,
413-
RASTER_TILE_EXAMPLES[exampleTab].name
414-
)
415-
}
416-
>
417-
{RASTER_TILE_EXAMPLES[exampleTab].url}
418-
</ExampleUrl>
419-
</ExampleUrlsContainer>
420-
</div>
392+
{getApplicationConfig().showInlineTilesetExamples && (
393+
<div>
394+
<TilesetInputDescription>
395+
For example, try a public raster tileset:
396+
</TilesetInputDescription>
397+
<ExampleUrlsContainer>
398+
<ExampleTabs>
399+
{RASTER_TILE_EXAMPLES.map((ex, i) => (
400+
<ExampleTab
401+
key={`${ex.label}-${ex.name}`}
402+
active={exampleTab === i}
403+
onClick={() => {
404+
setExampleTab(i);
405+
onExampleClick(ex.url, ex.name);
406+
}}
407+
>
408+
{ex.name}
409+
</ExampleTab>
410+
))}
411+
</ExampleTabs>
412+
<ExampleUrl
413+
onClick={() =>
414+
onExampleClick(
415+
RASTER_TILE_EXAMPLES[exampleTab].url,
416+
RASTER_TILE_EXAMPLES[exampleTab].name
417+
)
418+
}
419+
>
420+
{RASTER_TILE_EXAMPLES[exampleTab].url}
421+
</ExampleUrl>
422+
</ExampleUrlsContainer>
423+
</div>
424+
)}
421425
</TilesetInputContainer>
422426
);
423427
};

src/components/src/modals/tilesets-modals/tileset-tile3d-form.tsx

Lines changed: 29 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import styled from 'styled-components';
66

77
import {validateUrl} from '@kepler.gl/common-utils';
88
import {DatasetType, TILE3D_PROVIDERS, Tile3DDatasetMetadata} from '@kepler.gl/constants';
9+
import {getApplicationConfig} from '@kepler.gl/utils';
910

1011
import {MetaResponse, DatasetCreationAttributes} from './common';
1112
import {InputLight} from '../../common';
@@ -365,32 +366,34 @@ const TilesetTile3DForm: React.FC<Tile3DFormProps> = ({setResponse}) => {
365366
onChange={onAccessTokenChange}
366367
/>
367368
</div>
368-
<div>
369-
<TilesetInputDescription>For example, try a public 3D tileset:</TilesetInputDescription>
370-
<ExampleUrlsContainer>
371-
<ExampleTabs>
372-
{TILE3D_EXAMPLES.map((ex, i) => (
373-
<ExampleTab
374-
key={ex.label}
375-
active={exampleTab === i}
376-
onClick={() => {
377-
setExampleTab(i);
378-
onExampleClick(ex.url, ex.name);
379-
}}
380-
>
381-
{ex.label}
382-
</ExampleTab>
383-
))}
384-
</ExampleTabs>
385-
<ExampleUrl
386-
onClick={() =>
387-
onExampleClick(TILE3D_EXAMPLES[exampleTab].url, TILE3D_EXAMPLES[exampleTab].name)
388-
}
389-
>
390-
{TILE3D_EXAMPLES[exampleTab].url}
391-
</ExampleUrl>
392-
</ExampleUrlsContainer>
393-
</div>
369+
{getApplicationConfig().showInlineTilesetExamples && (
370+
<div>
371+
<TilesetInputDescription>For example, try a public 3D tileset:</TilesetInputDescription>
372+
<ExampleUrlsContainer>
373+
<ExampleTabs>
374+
{TILE3D_EXAMPLES.map((ex, i) => (
375+
<ExampleTab
376+
key={ex.label}
377+
active={exampleTab === i}
378+
onClick={() => {
379+
setExampleTab(i);
380+
onExampleClick(ex.url, ex.name);
381+
}}
382+
>
383+
{ex.label}
384+
</ExampleTab>
385+
))}
386+
</ExampleTabs>
387+
<ExampleUrl
388+
onClick={() =>
389+
onExampleClick(TILE3D_EXAMPLES[exampleTab].url, TILE3D_EXAMPLES[exampleTab].name)
390+
}
391+
>
392+
{TILE3D_EXAMPLES[exampleTab].url}
393+
</ExampleUrl>
394+
</ExampleUrlsContainer>
395+
</div>
396+
)}
394397
</TilesetInputContainer>
395398
);
396399
};

0 commit comments

Comments
 (0)