[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:
parent
a82def12e3
commit
2cc2229f1b
|
@ -2944,8 +2944,7 @@ main {
|
|||
/* 1 */
|
||||
padding: 10px;
|
||||
height: 40px;
|
||||
background-color: #ffffff;
|
||||
border: 1px solid #D3DAE6; }
|
||||
background-color: #ffffff; }
|
||||
|
||||
.kuiToolBarFooterSection {
|
||||
display: -webkit-box;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -34,6 +34,10 @@ export class TMSService {
|
|||
return this._emsClient.sanitizeMarkdown(this._config.attribution);
|
||||
}
|
||||
|
||||
getMarkdownAttribution() {
|
||||
return this._config.attribution;
|
||||
}
|
||||
|
||||
getMinZoom() {
|
||||
return this._config.minZoom;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 };
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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 : '';
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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;
|
|
@ -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'
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
});
|
|
@ -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';
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
};
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue