diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index b016d356ad82f..af88844a1d836 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -36,13 +36,15 @@ The file must use the WGS84 coordinate reference system and only include polygon If the file is hosted on a separate domain from Kibana, the server needs to be CORS-enabled so Kibana can download the file. The url field also serves as a unique identifier for the file. Each layer can contain multiple fields to indicate what properties from the geojson features you want to expose. -The field.description is the human readable text that is shown in the Region Map visualization's field menu. +The field.description is the human readable text that is shown in the Region Map visualization's field menu. +An optional attribution value can be added as well. The following example shows a valid regionmap configuration. regionmap: layers: - name: "Departments of France" url: "http://my.cors.enabled.server.org/france_departements.geojson" + attribution: "INRAP" fields: - name: "department" description: "Full department name" diff --git a/src/core_plugins/region_map/public/choropleth_layer.js b/src/core_plugins/region_map/public/choropleth_layer.js index bd9b42c065018..9c36b42fa0a0b 100644 --- a/src/core_plugins/region_map/public/choropleth_layer.js +++ b/src/core_plugins/region_map/public/choropleth_layer.js @@ -7,14 +7,14 @@ import { truncatedColorMaps } from 'ui/vislib/components/color/truncated_colorma export default class ChoroplethLayer extends KibanaMapLayer { - constructor(geojsonUrl) { + constructor(geojsonUrl, attribution) { super(); - this._metrics = null; this._joinField = null; this._colorRamp = truncatedColorMaps[Object.keys(truncatedColorMaps)[0]]; this._tooltipFormatter = () => ''; + this._attribution = attribution; this._geojsonUrl = geojsonUrl; this._leafletLayer = L.geoJson(null, { @@ -162,9 +162,7 @@ export default class ChoroplethLayer extends KibanaMapLayer { jqueryDiv.append(label); }); - } - } diff --git a/src/core_plugins/region_map/public/region_map_controller.js b/src/core_plugins/region_map/public/region_map_controller.js index 0245f51f551ec..42bd4df53447d 100644 --- a/src/core_plugins/region_map/public/region_map_controller.js +++ b/src/core_plugins/region_map/public/region_map_controller.js @@ -54,7 +54,7 @@ module.controller('KbnRegionMapController', function ($scope, $element, Private, return; } - updateChoroplethLayer($scope.vis.params.selectedLayer.url); + updateChoroplethLayer($scope.vis.params.selectedLayer.url, $scope.vis.params.selectedLayer.attribution); choroplethLayer.setMetrics(results, metricsAgg); setTooltipFormatter(); @@ -74,7 +74,7 @@ module.controller('KbnRegionMapController', function ($scope, $element, Private, return; } - updateChoroplethLayer(visParams.selectedLayer.url); + updateChoroplethLayer(visParams.selectedLayer.url, visParams.selectedLayer.attribution); choroplethLayer.setJoinField(visParams.selectedJoinField.name); choroplethLayer.setColorRamp(truncatedColorMaps[visParams.colorSchema]); setTooltipFormatter(); @@ -109,7 +109,7 @@ module.controller('KbnRegionMapController', function ($scope, $element, Private, } } - function updateChoroplethLayer(url) { + function updateChoroplethLayer(url, attribution) { if (choroplethLayer && choroplethLayer.equalsGeoJsonUrl(url)) {//no need to recreate the layer return; @@ -118,7 +118,7 @@ module.controller('KbnRegionMapController', function ($scope, $element, Private, const previousMetrics = choroplethLayer ? choroplethLayer.getMetrics() : null; const previousMetricsAgg = choroplethLayer ? choroplethLayer.getMetricsAgg() : null; - choroplethLayer = new ChoroplethLayer(url); + choroplethLayer = new ChoroplethLayer(url, attribution); if (previousMetrics && previousMetricsAgg) { choroplethLayer.setMetrics(previousMetrics, previousMetricsAgg); } diff --git a/src/server/config/schema.js b/src/server/config/schema.js index 2d8309f93685f..b4966470dc3cf 100644 --- a/src/server/config/schema.js +++ b/src/server/config/schema.js @@ -188,6 +188,7 @@ module.exports = () => Joi.object({ layers: Joi.array().items(Joi.object({ url: Joi.string(), type: Joi.string(), + attribution: Joi.string(), name: Joi.string(), fields: Joi.array().items(Joi.object({ name: Joi.string(), diff --git a/src/ui/public/vis_maps/__tests__/kibana_map.js b/src/ui/public/vis_maps/__tests__/kibana_map.js index 5d12b9d79602e..08c971ee8353b 100644 --- a/src/ui/public/vis_maps/__tests__/kibana_map.js +++ b/src/ui/public/vis_maps/__tests__/kibana_map.js @@ -1,5 +1,7 @@ import expect from 'expect.js'; -import { KibanaMap } from 'ui/vis_maps/kibana_map'; +import { KibanaMap } from '../kibana_map'; +import { KibanaMapLayer } from '../kibana_map_layer'; +import L from 'leaflet'; describe('kibana_map tests', function () { @@ -66,10 +68,53 @@ describe('kibana_map tests', function () { expect(bounds.top_left.lon).to.equal(-180); }); - }); + describe('KibanaMap - attributions', function () { + + + beforeEach(async function () { + setupDOM(); + kibanaMap = new KibanaMap(domNode, { + minZoom: 1, + maxZoom: 10, + center: [0, 0], + zoom: 0 + }); + }); + + afterEach(function () { + kibanaMap.destroy(); + teardownDOM(); + }); + + function makeMockLayer(attribution) { + const layer = new KibanaMapLayer(); + layer._attribution = attribution; + layer._leafletLayer = L.geoJson(null); + return layer; + } + + it('should update attributions correctly', function () { + kibanaMap.addLayer(makeMockLayer('foo|bar')); + expect(domNode.querySelectorAll('.leaflet-control-attribution')[0].innerHTML).to.equal('foo, bar'); + + kibanaMap.addLayer(makeMockLayer('bar')); + expect(domNode.querySelectorAll('.leaflet-control-attribution')[0].innerHTML).to.equal('foo, bar'); + + const layer = makeMockLayer('bar,stool'); + kibanaMap.addLayer(layer); + expect(domNode.querySelectorAll('.leaflet-control-attribution')[0].innerHTML).to.equal('foo, bar, stool'); + + kibanaMap.removeLayer(layer); + expect(domNode.querySelectorAll('.leaflet-control-attribution')[0].innerHTML).to.equal('foo, bar'); + + + }); + + }); + describe('KibanaMap - baseLayer', function () { beforeEach(async function () { diff --git a/src/ui/public/vis_maps/kibana_map.js b/src/ui/public/vis_maps/kibana_map.js index 699422cd826cf..cf012d1d54c19 100644 --- a/src/ui/public/vis_maps/kibana_map.js +++ b/src/ui/public/vis_maps/kibana_map.js @@ -105,6 +105,7 @@ export class KibanaMap extends EventEmitter { }; this._leafletMap = L.map(containerNode, leafletOptions); + this._leafletMap.attributionControl.setPrefix(''); this._leafletMap.scrollWheelZoom.disable(); const worldBounds = L.latLngBounds(L.latLng(-90, -180), L.latLng(90, 180)); this._leafletMap.setMaxBounds(worldBounds); @@ -236,19 +237,49 @@ export class KibanaMap extends EventEmitter { this._layers.push(kibanaLayer); kibanaLayer.addToLeafletMap(this._leafletMap); this.emit('layers:update'); + + this._addAttributions(kibanaLayer.getAttributions()); } - removeLayer(layer) { - const index = this._layers.indexOf(layer); + removeLayer(kibanaLayer) { + + if (!kibanaLayer) { + return; + } + + this._removeAttributions(kibanaLayer.getAttributions()); + const index = this._layers.indexOf(kibanaLayer); if (index >= 0) { this._layers.splice(index, 1); - layer.removeFromLeafletMap(this._leafletMap); + kibanaLayer.removeFromLeafletMap(this._leafletMap); } this._listeners.forEach(listener => { - if (listener.layer === layer) { + if (listener.layer === kibanaLayer) { listener.layer.removeListener(listener.name, listener.handle); } }); + + //must readd all attributions, because we might have removed dupes + this._layers.forEach((layer) => this._addAttributions(layer.getAttributions())); + if (this._baseLayerSettings) { + this._addAttributions(this._baseLayerSettings.options.attribution); + } + } + + + _addAttributions(attribution) { + const attributions = getAttributionArray(attribution); + attributions.forEach((attribution) => { + this._leafletMap.attributionControl.removeAttribution(attribution);//this ensures we do not add duplicates + this._leafletMap.attributionControl.addAttribution(attribution); + }); + } + + _removeAttributions(attribution) { + const attributions = getAttributionArray(attribution); + attributions.forEach((attribution) => { + this._leafletMap.attributionControl.removeAttribution(attribution);//this ensures we do not add duplicates + }); } destroy() { @@ -422,15 +453,18 @@ export class KibanaMap extends EventEmitter { return; } - this._baseLayerSettings = settings; + if (settings === null) { if (this._leafletBaseLayer && this._leafletMap) { + this._removeAttributions(this._baseLayerSettings.options.attribution); this._leafletMap.removeLayer(this._leafletBaseLayer); this._leafletBaseLayer = null; + this._baseLayerSettings = null; } return; } + this._baseLayerSettings = settings; if (this._leafletBaseLayer) { this._leafletMap.removeLayer(this._leafletBaseLayer); this._leafletBaseLayer = null; @@ -443,15 +477,22 @@ export class KibanaMap extends EventEmitter { baseLayer = this._getTMSBaseLayer((settings.options)); } - baseLayer.on('tileload', () => this._updateDesaturation()); - baseLayer.on('load', () => { this.emit('baseLayer:loaded');}); - baseLayer.on('loading', () => {this.emit('baseLayer:loading');}); - - this._leafletBaseLayer = baseLayer; - this._leafletBaseLayer.addTo(this._leafletMap); - this._leafletBaseLayer.bringToBack(); - if (settings.options.minZoom > this._leafletMap.getZoom()) { - this._leafletMap.setZoom(settings.options.minZoom); + if (baseLayer) { + baseLayer.on('tileload', () => this._updateDesaturation()); + baseLayer.on('load', () => { + this.emit('baseLayer:loaded'); + }); + baseLayer.on('loading', () => { + this.emit('baseLayer:loading'); + }); + + this._leafletBaseLayer = baseLayer; + this._leafletBaseLayer.addTo(this._leafletMap); + this._leafletBaseLayer.bringToBack(); + if (settings.options.minZoom > this._leafletMap.getZoom()) { + this._leafletMap.setZoom(settings.options.minZoom); + } + this._addAttributions(settings.options.attribution); } this.resize(); @@ -487,16 +528,14 @@ export class KibanaMap extends EventEmitter { return L.tileLayer(options.url, { minZoom: options.minZoom, maxZoom: options.maxZoom, - subdomains: options.subdomains || [], - attribution: options.attribution + subdomains: options.subdomains || [] }); } _getWMSBaseLayer(options) { return L.tileLayer.wms(options.url, { - attribution: options.attribution, - format: options.format, - layers: options.layers, + format: options.format || '', + layers: options.layers || '', minZoom: options.minZoom, maxZoom: options.maxZoom, styles: options.styles, @@ -550,3 +589,12 @@ export class KibanaMap extends EventEmitter { } +function getAttributionArray(attribution) { + const attributionString = attribution || ''; + let attributions = attributionString.split('|'); + if (attributions.length === 1) {//temp work-around due to inconsistency in manifests of how attributions are delimited + attributions = attributions[0].split(','); + } + return attributions; +} + diff --git a/src/ui/public/vis_maps/kibana_map_layer.js b/src/ui/public/vis_maps/kibana_map_layer.js index 19a957cda4960..b32c330737599 100644 --- a/src/ui/public/vis_maps/kibana_map_layer.js +++ b/src/ui/public/vis_maps/kibana_map_layer.js @@ -27,6 +27,10 @@ export class KibanaMapLayer extends EventEmitter { movePointer() { } + + getAttributions() { + return this._attribution; + } } diff --git a/src/ui/public/vis_maps/lib/service_settings.js b/src/ui/public/vis_maps/lib/service_settings.js index 9983a705cbba4..c0e8428d96c45 100644 --- a/src/ui/public/vis_maps/lib/service_settings.js +++ b/src/ui/public/vis_maps/lib/service_settings.js @@ -69,6 +69,7 @@ uiModules.get('kibana') const layers = manifest.data.layers.filter(layer => layer.format === 'geojson'); layers.forEach((layer) => { layer.url = this._extendUrlWithParams(layer.url); + layer.attribution = $sanitize(marked(layer.attribution)); }); return layers; });