[Maps] add attribution for EMS sources / design improvements (#28310)

- Add attribution from EMS sources to map.
- Design improvements in layout of lon/lat readout and attribution
This commit is contained in:
Thomas Neirynck 2019-01-14 14:14:20 -05:00 committed by GitHub
parent a82def12e3
commit 2cc2229f1b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 290 additions and 57 deletions

View file

@ -2944,8 +2944,7 @@ main {
/* 1 */
padding: 10px;
height: 40px;
background-color: #ffffff;
border: 1px solid #D3DAE6; }
background-color: #ffffff; }
.kuiToolBarFooterSection {
display: -webkit-box;

View file

@ -119,7 +119,7 @@ export class EMSClientV66 {
if (!i18nObject) {
return '';
}
return i18nObject[this._language] ? i18nObject[this._language] : i18nObject[DEFAULT_LANGUAGE];
return i18nObject[this._language] ? i18nObject[this._language] : i18nObject[DEFAULT_LANGUAGE];
}
/**
@ -139,9 +139,7 @@ export class EMSClientV66 {
}
throw new Error(`Unable to retrieve manifest from ${manifestUrl}: ${e.message}`);
} finally {
return result
? await result.json()
: null;
return result ? await result.json() : null;
}
}

View file

@ -27,6 +27,17 @@ export class FileLayer {
this._emsClient = emsClient;
}
getAttributions() {
const attributions = this._config.attribution.map(attribution => {
const url = this._emsClient.getValueInLanguage(attribution.url);
const label = this._emsClient.getValueInLanguage(attribution.label);
return {
url: url,
label: label
};
});
return attributions;
}
getHTMLAttribution() {
const attributions = this._config.attribution.map(attribution => {

View file

@ -34,6 +34,10 @@ export class TMSService {
return this._emsClient.sanitizeMarkdown(this._config.attribution);
}
getMarkdownAttribution() {
return this._config.attribution;
}
getMinZoom() {
return this._config.minZoom;
}

View file

@ -43,14 +43,18 @@ map-listing, .gisListingPage {
.gisWidgetOverlay {
position: absolute;
z-index: $euiZLevel1;
min-width: 17rem;
max-width: 24rem;
top: $euiSizeM;
right: $euiSizeM;
bottom: $euiSizeM;
// left: $euiSizeM;
pointer-events: none; /* 1 */
}
.gisWidgetOverlay__rightSide {
min-width: 17rem;
max-width: 24rem;
}
.gisWidgetControl {
max-height: 100%;
overflow: hidden;
@ -70,6 +74,21 @@ map-listing, .gisListingPage {
}
}
.gisAttributionControl {
padding: 0 $euiSizeXS;
}
.gisViewControl__coordinates {
padding: $euiSizeXS $euiSizeS;
justify-content: center;
pointer-events: none;
}
.gisViewControl__gotoButton {
min-width: 0;
pointer-events: all; /* 1 */
}
.gisWidgetControl__tocHolder {
@include euiScrollBar;
overflow-y: auto;

View file

@ -12,7 +12,7 @@ import {
mapDestroyed,
setMouseCoordinates,
clearMouseCoordinates,
clearGoto,
clearGoto
} from '../../../actions/store_actions';
import { getLayerList, getMapReady, getGoto } from "../../../selectors/map_selectors";
@ -20,7 +20,7 @@ function mapStateToProps(state = {}) {
return {
isMapReady: getMapReady(state),
layerList: getLayerList(state),
goto: getGoto(state),
goto: getGoto(state)
};
}

View file

@ -10,6 +10,7 @@ import mapboxgl from 'mapbox-gl';
export async function createMbMapInstance(node, initialView) {
return new Promise((resolve) => {
const options = {
attributionControl: false,
container: node,
style: {
version: 8,

View file

@ -174,7 +174,7 @@ export class MBMapContainer extends React.Component {
lng: goto.lon,
lat: goto.lat
});
}
};
_syncMbMapWithLayerList = () => {
const {
@ -190,7 +190,7 @@ export class MBMapContainer extends React.Component {
layer.syncLayerWithMB(this._mbMap);
});
syncLayerOrder(this._mbMap, layerList);
}
};
_syncMbMapWithInspector = () => {
if (!this.props.isMapReady) {
@ -206,7 +206,7 @@ export class MBMapContainer extends React.Component {
stats,
style: this._mbMap.getStyle(),
});
}
};
render() {
// do not debounce syncing zoom and center

View file

@ -0,0 +1,22 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { connect } from 'react-redux';
import { AttributionControl } from './view';
import { getLayerList } from "../../../selectors/map_selectors";
function mapStateToProps(state = {}) {
return {
layerList: getLayerList(state)
};
}
function mapDispatchToProps() {
return {};
}
const connectedViewControl = connect(mapStateToProps, mapDispatchToProps)(AttributionControl);
export { connectedViewControl as AttributionControl };

View file

@ -0,0 +1,88 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { Fragment } from 'react';
import _ from 'lodash';
import {
EuiText,
EuiPanel,
EuiLink,
} from '@elastic/eui';
export class AttributionControl extends React.Component {
constructor() {
super();
this.state = {
uniqueAttributions: []
};
}
componentDidMount() {
this._isMounted = true;
this._syncMbMapWithAttribution();
}
componentWillUnmount() {
this._isMounted = false;
}
componentDidUpdate() {
this._syncMbMapWithAttribution();
}
_syncMbMapWithAttribution = async () => {
const attributionPromises = this.props.layerList.map(layer => {
return layer.getAttributions();
});
const attributions = await Promise.all(attributionPromises);
if (!this._isMounted) {
return;
}
const uniqueAttributions = [];
for (let i = 0; i < attributions.length; i++) {
for (let j = 0; j < attributions[i].length; j++) {
const testAttr = attributions[i][j];
const attr = uniqueAttributions.find((added) => {
return (added.url === testAttr.url && added.label === testAttr.label);
});
if (!attr) {
uniqueAttributions.push(testAttr);
}
}
}
if (!_.isEqual(this.state.uniqueAttributions, uniqueAttributions)) {
this.setState({ uniqueAttributions });
}
};
_renderAttributions() {
return this.state.uniqueAttributions.map((attribution, index) => {
return (
<Fragment key={index}>
<EuiLink color="subdued" href={attribution.url} target="_blank">{attribution.label}</EuiLink>
{index < (this.state.uniqueAttributions.length - 1) && ', '}
</Fragment>
);
});
}
render() {
if (this.state.uniqueAttributions.length === 0) {
return null;
}
return (
<EuiPanel className="gisWidgetControl gisAttributionControl" paddingSize="none" grow={false}>
<EuiText color="subdued" size="xs">
<small>{this._renderAttributions()}</small>
</EuiText>
</EuiPanel>
);
}
}

View file

@ -4,12 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { Fragment } from 'react';
import React from 'react';
import {
EuiFlexGroup,
EuiFlexItem,
EuiPanel,
EuiButtonEmpty,
EuiButton,
EuiPopover,
EuiText,
} from '@elastic/eui';
@ -26,15 +26,17 @@ export function ViewControl({ isSetViewOpen, closeSetView, openSetView, mouseCoo
};
const setView = (
<EuiPopover
anchorPosition="upRight"
button={(
<EuiButtonEmpty
flush="right"
size="xs"
<EuiButton
className="gisViewControl__gotoButton"
fill
size="s"
onClick={toggleSetViewVisibility}
data-test-subj="toggleSetViewVisibilityButton"
>
Goto
</EuiButtonEmpty>)}
</EuiButton>)}
isOpen={isSetViewOpen}
closePopover={closeSetView}
>
@ -44,39 +46,30 @@ export function ViewControl({ isSetViewOpen, closeSetView, openSetView, mouseCoo
function renderMouseCoordinates() {
return (
<Fragment>
<EuiFlexItem grow={false}>
<EuiText size="xs">
<p>
<strong>lat:</strong> {mouseCoordinates && mouseCoordinates.lat}
</p>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="xs">
<p>
<strong>long:</strong> {mouseCoordinates && mouseCoordinates.lon}
</p>
</EuiText>
</EuiFlexItem>
</Fragment>
<EuiPanel className="gisWidgetControl gisViewControl__coordinates" paddingSize="none">
<EuiText size="xs">
<p>
<strong>lat:</strong> {mouseCoordinates && mouseCoordinates.lat},{' '}
<strong>lon:</strong> {mouseCoordinates && mouseCoordinates.lon}
</p>
</EuiText>
</EuiPanel>
);
}
return (
<EuiPanel className="gisWidgetControl" hasShadow paddingSize="s">
<EuiFlexGroup
justifyContent="spaceBetween"
alignItems="center"
gutterSize="s"
>
<EuiFlexGroup
justifyContent="spaceBetween"
gutterSize="s"
responsive={false}
>
<EuiFlexItem>
{mouseCoordinates && renderMouseCoordinates()}
</EuiFlexItem>
{renderMouseCoordinates()}
<EuiFlexItem grow={false}>
{setView}
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
<EuiFlexItem grow={false}>
{setView}
</EuiFlexItem>
</EuiFlexGroup>
);
}

View file

@ -11,19 +11,28 @@ import {
} from '@elastic/eui';
import { LayerControl } from './layer_control';
import { ViewControl } from './view_control';
import { AttributionControl } from './attribution_control';
export function WidgetOverlay() {
return (
<EuiFlexGroup
className="gisWidgetOverlay"
direction="column"
justifyContent="spaceBetween"
>
<EuiFlexGroup className="gisWidgetOverlay" responsive={false} direction="column" alignItems="flexEnd" gutterSize="s">
<EuiFlexItem>
<LayerControl/>
<EuiFlexGroup
className="gisWidgetOverlay__rightSide"
direction="column"
justifyContent="spaceBetween"
responsive={false}
>
<EuiFlexItem>
<LayerControl/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<ViewControl/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<ViewControl/>
<AttributionControl/>
</EuiFlexItem>
</EuiFlexGroup>
);

View file

@ -64,6 +64,10 @@ export class ALayer {
return (await this._source.getDisplayName()) || `Layer ${this._descriptor.id}`;
}
async getAttributions() {
return await this._source.getAttributions();
}
getLabel() {
return this._descriptor.label ? this._descriptor.label : '';
}

View file

@ -100,6 +100,13 @@ export class EMSFileSource extends VectorSource {
const fileSource = this._emsFiles.find((source => source.id === this._descriptor.id));
return fileSource.name;
}
async getAttributions() {
const fileSource = this._emsFiles.find((source => source.id === this._descriptor.id));
return fileSource.attributions;
}
async getStringFields() {
//todo: use map/service-settings instead.
const fileSource = this._emsFiles.find((source => source.id === this._descriptor.id));

View file

@ -4,8 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { Fragment } from 'react';
import { TMSSource } from './tms_source';
import { TileLayer } from '../tile_layer';
import { TMSSource } from '../tms_source';
import { TileLayer } from '../../tile_layer';
import {
EuiText,
EuiSelect,
@ -107,6 +107,22 @@ export class EMSTMSSource extends TMSSource {
return this._descriptor.id;
}
async getAttributions() {
const service = this._getTMSOptions();
const attributions = service.attributionMarkdown.split('|');
return attributions.map((attribution) => {
attribution = attribution.trim();
//this assumes attribution is plain markdown link
const extractLink = /\[(.*)\]\((.*)\)/;
const result = extractLink.exec(attribution);
return {
label: result ? result[1] : null,
url: result ? result[2] : null
};
});
}
getUrlTemplate() {
const service = this._getTMSOptions();
return service.url;

View file

@ -0,0 +1,44 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import {
EMSTMSSource,
} from './ems_tms_source';
describe('EMSTMSSource', () => {
it('should get attribution from markdown (tiles v2 legacy format)', async () => {
const emsTmsSource = new EMSTMSSource({
id: 'road_map'
}, {
emsTmsServices: [
{
id: 'road_map',
attributionMarkdown: '[foobar](http://foobar.org) | [foobaz](http://foobaz.org)'
}, {
id: 'satellite',
attributionMarkdown: '[satellite](http://satellite.org)'
}
]
});
const attributions = await emsTmsSource.getAttributions();
expect(attributions).toEqual([
{
label: 'foobar',
url: 'http://foobar.org'
}, {
label: 'foobaz',
url: 'http://foobaz.org'
}
]);
});
});

View file

@ -0,0 +1,7 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { EMSTMSSource } from './ems_tms_source';

View file

@ -43,6 +43,15 @@ export class ASource {
return '';
}
/**
* return attribution for this layer as array of objects with url and label property.
* e.g. [{ url: 'example.com', label: 'foobar' }]
* @return {Promise<null>}
*/
async getAttributions() {
return [];
}
isFieldAware() {
return false;
}

View file

@ -94,6 +94,7 @@ export function initRoutes(server, licenseUid) {
id: fileLayer.getId(),
created_at: fileLayer.getCreatedAt(),
attribution: fileLayer.getHTMLAttribution(),
attributions: fileLayer.getAttributions(),
fields: fileLayer.getFieldsInLanguage(),
url: fileLayer.getDefaultFormatUrl(),
format: format, //legacy: format and meta are split up
@ -108,6 +109,7 @@ export function initRoutes(server, licenseUid) {
minZoom: tmsService.getMinZoom(),
maxZoom: tmsService.getMaxZoom(),
attribution: tmsService.getHTMLAttribution(),
attributionMarkdown: tmsService.getMarkdownAttribution(),
url: tmsService.getUrlTemplate()
};
});