mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Backport] Add Region Map Visualization (#12112)
- Users can now create choropleth maps. This requires configuring an inner join between the results of a term-aggregation and a reference vector layer. This vector layer needs to be in the GeoJson format. By default, Kibana uses vector layers serverd by a data service hosted by Elastic. Users can also bring in their own layers by adding configuration entries in the kibana.yml. These need to point to a CORS-enabled data service that accepts requests from the Kibana application. - For clarity, the tilemap is renamed to Coordinate Map. - A new manifest is published by Elastic. this includes metadata for the available tilemap services, as well as metadata for the available vector data layers. Required manual edits to resolve Conflicts: src/core_plugins/kibana/inject_vars.js
This commit is contained in:
parent
a110df05ab
commit
f6be1e4698
38 changed files with 1402 additions and 458 deletions
|
@ -129,10 +129,10 @@ make the differences stand out, starting the Y-axis at a value closer to the min
|
|||
|
||||
Save this chart with the name _Bar Example_.
|
||||
|
||||
Next, we're going to use a tile map chart to visualize geographic information in our log file sample data.
|
||||
Next, we're going to use a coordinate map chart to visualize geographic information in our log file sample data.
|
||||
|
||||
. Click *New*.
|
||||
. Select *Tile map*.
|
||||
. Select *Coordinate map*.
|
||||
. Select the `logstash-*` index pattern.
|
||||
. Set the time window for the events we're exploring:
|
||||
. Click the time picker in the Kibana toolbar.
|
||||
|
|
|
@ -48,7 +48,7 @@ increase request processing time.
|
|||
when necessary.
|
||||
`visualization:tileMap:maxPrecision`:: The maximum geoHash precision displayed on tile maps: 7 is high, 10 is very high,
|
||||
12 is the maximum. {es-ref}search-aggregations-bucket-geohashgrid-aggregation.html#_cell_dimensions_at_the_equator[Explanation of cell dimensions].
|
||||
`visualization:tileMap:WMSdefaults`:: Default properties for the WMS map server support in the tile map.
|
||||
`visualization:tileMap:WMSdefaults`:: Default properties for the WMS map server support in the coordinate map.
|
||||
`visualization:colorMapping`:: Maps values to specified colors within visualizations.
|
||||
`visualization:loadingDelay`:: Time to wait before dimming visualizations during query.
|
||||
`visualization:dimmingOpacity`:: When part of a visualization is highlighted, by hovering over it for example, ths is the opacity applied to the other elements. A higher number means other elements will be less opaque.
|
||||
|
|
|
@ -36,7 +36,7 @@ To create a visualization:
|
|||
<<metric-chart,Metric>>:: Display a single number.
|
||||
* *Maps*
|
||||
[horizontal]
|
||||
<<tilemap,Tile map>>:: Associate the results of an aggregation with geographic
|
||||
<<tilemap,Coordinate map>>:: Associate the results of an aggregation with geographic
|
||||
locations.
|
||||
* *Time Series*
|
||||
[horizontal]
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
[[tilemap]]
|
||||
== Tile Maps
|
||||
|
||||
A tile map displays a geographic area overlaid with circles keyed to the data determined by the buckets you specify.
|
||||
A coordinate map displays a geographic area overlaid with circles keyed to the data determined by the buckets you specify.
|
||||
|
||||
NOTE: By default, Kibana uses the https://www.elastic.co/elastic-tile-service[Elastic Tile Service]
|
||||
to display map tiles. To use other tile service providers, configure the <<tilemap-settings,tilemap settings>>
|
||||
|
@ -13,7 +13,7 @@ in `kibana.yml`.
|
|||
|
||||
===== Metrics
|
||||
|
||||
The default _metrics_ aggregation for a tile map is the *Count* aggregation. You can select any of the following
|
||||
The default _metrics_ aggregation for a coordinate map is the *Count* aggregation. You can select any of the following
|
||||
aggregations as the metrics aggregation:
|
||||
|
||||
*Count*:: The {es-ref}search-aggregations-metrics-valuecount-aggregation.html[_count_] aggregation returns a raw count of
|
||||
|
|
|
@ -14,7 +14,7 @@ export default function TileMapVisType(Private, getAppState, courier, config) {
|
|||
|
||||
return new MapsVisType({
|
||||
name: 'tile_map',
|
||||
title: 'Tile Map',
|
||||
title: 'Coordinate Map',
|
||||
image,
|
||||
description: 'Plot latitude and longitude coordinates on a map',
|
||||
category: VisType.CATEGORY.MAP,
|
||||
|
|
|
@ -43,6 +43,7 @@ module.exports = function (kibana) {
|
|||
'docViews'
|
||||
],
|
||||
injectVars: function (server) {
|
||||
|
||||
const serverConfig = server.config();
|
||||
|
||||
//DEPRECATED SETTINGS
|
||||
|
@ -51,14 +52,21 @@ module.exports = function (kibana) {
|
|||
const configuredUrl = server.config().get('tilemap.url');
|
||||
const isOverridden = typeof configuredUrl === 'string' && configuredUrl !== '';
|
||||
const tilemapConfig = serverConfig.get('tilemap');
|
||||
const regionmapsConfig = serverConfig.get('regionmap');
|
||||
const mapConfig = serverConfig.get('map');
|
||||
|
||||
|
||||
regionmapsConfig.layers = (regionmapsConfig.layers) ? regionmapsConfig.layers : [];
|
||||
|
||||
return {
|
||||
kbnDefaultAppId: serverConfig.get('kibana.defaultAppId'),
|
||||
regionmapsConfig: regionmapsConfig,
|
||||
mapConfig: mapConfig,
|
||||
tilemapsConfig: {
|
||||
deprecated: {
|
||||
isOverridden: isOverridden,
|
||||
config: tilemapConfig,
|
||||
},
|
||||
manifestServiceUrl: serverConfig.get('tilemap.manifestServiceUrl')
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
|
|
27
src/core_plugins/kibana/inject_vars.js
Normal file
27
src/core_plugins/kibana/inject_vars.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
export function injectVars(server) {
|
||||
const serverConfig = server.config();
|
||||
|
||||
//DEPRECATED SETTINGS
|
||||
//if the url is set, the old settings must be used.
|
||||
//keeping this logic for backward compatibilty.
|
||||
const configuredUrl = server.config().get('tilemap.url');
|
||||
const isOverridden = typeof configuredUrl === 'string' && configuredUrl !== '';
|
||||
const tilemapConfig = serverConfig.get('tilemap');
|
||||
const regionmapsConfig = serverConfig.get('regionmap');
|
||||
const mapConfig = serverConfig.get('map');
|
||||
|
||||
|
||||
regionmapsConfig.layers = (regionmapsConfig.layers) ? regionmapsConfig.layers : [];
|
||||
|
||||
return {
|
||||
kbnDefaultAppId: serverConfig.get('kibana.defaultAppId'),
|
||||
regionmapsConfig: regionmapsConfig,
|
||||
mapConfig: mapConfig,
|
||||
tilemapsConfig: {
|
||||
deprecated: {
|
||||
isOverridden: isOverridden,
|
||||
config: tilemapConfig,
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
9
src/core_plugins/region_map/index.js
Normal file
9
src/core_plugins/region_map/index.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
export default function (kibana) {
|
||||
|
||||
return new kibana.Plugin({
|
||||
uiExports: {
|
||||
visTypes: ['plugins/region_map/region_map_vis']
|
||||
}
|
||||
});
|
||||
|
||||
}
|
4
src/core_plugins/region_map/package.json
Normal file
4
src/core_plugins/region_map/package.json
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"name": "region_map",
|
||||
"version": "kibana"
|
||||
}
|
291
src/core_plugins/region_map/public/choropleth_layer.js
Normal file
291
src/core_plugins/region_map/public/choropleth_layer.js
Normal file
|
@ -0,0 +1,291 @@
|
|||
import $ from 'jquery';
|
||||
import L from 'leaflet';
|
||||
import _ from 'lodash';
|
||||
import d3 from 'd3';
|
||||
import { KibanaMapLayer } from 'ui/vis_maps/kibana_map_layer';
|
||||
import { truncatedColorMaps } from 'ui/vislib/components/color/truncated_colormaps';
|
||||
|
||||
export default class ChoroplethLayer extends KibanaMapLayer {
|
||||
|
||||
constructor(geojsonUrl) {
|
||||
super();
|
||||
|
||||
|
||||
this._metrics = null;
|
||||
this._joinField = null;
|
||||
this._colorRamp = truncatedColorMaps[Object.keys(truncatedColorMaps)[0]];
|
||||
this._tooltipFormatter = () => '';
|
||||
|
||||
this._geojsonUrl = geojsonUrl;
|
||||
this._leafletLayer = L.geoJson(null, {
|
||||
onEachFeature: (feature, layer) => {
|
||||
layer.on('click', () => {
|
||||
this.emit('select', feature.properties[this._joinField]);
|
||||
});
|
||||
let location = null;
|
||||
layer.on({
|
||||
mouseover: () => {
|
||||
const tooltipContents = this._tooltipFormatter(feature);
|
||||
if (!location) {
|
||||
const leafletGeojon = L.geoJson(feature);
|
||||
location = leafletGeojon.getBounds().getCenter();
|
||||
}
|
||||
|
||||
this.emit('showTooltip', {
|
||||
content: tooltipContents,
|
||||
position: location
|
||||
});
|
||||
},
|
||||
mouseout: () => {
|
||||
this.emit('hideTooltip');
|
||||
}
|
||||
});
|
||||
},
|
||||
style: emptyStyle
|
||||
});
|
||||
|
||||
this._loaded = false;
|
||||
this._error = false;
|
||||
$.ajax({
|
||||
dataType: 'json',
|
||||
url: geojsonUrl,
|
||||
success: (data) => {
|
||||
this._leafletLayer.addData(data);
|
||||
this._loaded = true;
|
||||
this._setStyle();
|
||||
},
|
||||
error: () => {
|
||||
this._loaded = true;
|
||||
this._error = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_setStyle() {
|
||||
if (this._error || (!this._loaded || !this._metrics || !this._joinField)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const styler = makeChoroplethStyler(this._metrics, this._colorRamp, this._joinField);
|
||||
this._leafletLayer.setStyle(styler.getLeafletStyleFunction);
|
||||
|
||||
if (this._metrics && this._metrics.length > 0) {
|
||||
const { min, max } = getMinMax(this._metrics);
|
||||
this._legendColors = getLegendColors(this._colorRamp);
|
||||
const quantizeDomain = (min !== max) ? [min, max] : d3.scale.quantize().domain();
|
||||
this._legendQuantizer = d3.scale.quantize().domain(quantizeDomain).range(this._legendColors);
|
||||
}
|
||||
this.emit('styleChanged', {
|
||||
mismatches: styler.getMismatches()
|
||||
});
|
||||
}
|
||||
|
||||
getMetrics() {
|
||||
return this._metrics;
|
||||
}
|
||||
|
||||
getMetricsAgg() {
|
||||
return this._metricsAgg;
|
||||
}
|
||||
|
||||
getUrl() {
|
||||
return this._geojsonUrl;
|
||||
}
|
||||
|
||||
setTooltipFormatter(tooltipFormatter, metricsAgg, fieldName) {
|
||||
this._tooltipFormatter = (geojsonFeature) => {
|
||||
if (!this._metrics) {
|
||||
return '';
|
||||
}
|
||||
const match = this._metrics.find((bucket) => {
|
||||
return bucket.term === geojsonFeature.properties[this._joinField];
|
||||
});
|
||||
return tooltipFormatter(metricsAgg, match, fieldName);
|
||||
};
|
||||
}
|
||||
|
||||
setJoinField(joinfield) {
|
||||
if (joinfield === this._joinField) {
|
||||
return;
|
||||
}
|
||||
this._joinField = joinfield;
|
||||
this._setStyle();
|
||||
}
|
||||
|
||||
|
||||
setMetrics(metrics, metricsAgg) {
|
||||
this._metrics = metrics;
|
||||
this._metricsAgg = metricsAgg;
|
||||
this._valueFormatter = this._metricsAgg.fieldFormatter();
|
||||
this._setStyle();
|
||||
}
|
||||
|
||||
setColorRamp(colorRamp) {
|
||||
if (_.isEqual(colorRamp, this._colorRamp)) {
|
||||
return;
|
||||
}
|
||||
this._colorRamp = colorRamp;
|
||||
this._setStyle();
|
||||
}
|
||||
|
||||
equalsGeoJsonUrl(geojsonUrl) {
|
||||
return this._geojsonUrl === geojsonUrl;
|
||||
}
|
||||
|
||||
appendLegendContents(jqueryDiv) {
|
||||
|
||||
|
||||
if (!this._legendColors || !this._legendQuantizer || !this._metricsAgg) {
|
||||
return;
|
||||
}
|
||||
|
||||
const titleText = this._metricsAgg.makeLabel();
|
||||
const $title = $('<div>').addClass('tilemap-legend-title').text(titleText);
|
||||
jqueryDiv.append($title);
|
||||
|
||||
this._legendColors.forEach((color) => {
|
||||
|
||||
const labelText = this._legendQuantizer
|
||||
.invertExtent(color)
|
||||
.map(this._valueFormatter)
|
||||
.join(' – ');
|
||||
|
||||
const label = $('<div>');
|
||||
const icon = $('<i>').css({
|
||||
background: color,
|
||||
'border-color': makeColorDarker(color)
|
||||
});
|
||||
|
||||
const text = $('<span>').text(labelText);
|
||||
label.append(icon);
|
||||
label.append(text);
|
||||
|
||||
jqueryDiv.append(label);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
function makeColorDarker(color) {
|
||||
const amount = 1.3;//magic number, carry over from earlier
|
||||
return d3.hcl(color).darker(amount).toString();
|
||||
}
|
||||
|
||||
|
||||
function getMinMax(data) {
|
||||
let min = data[0].value;
|
||||
let max = data[0].value;
|
||||
for (let i = 1; i < data.length; i += 1) {
|
||||
min = Math.min(data[i].value, min);
|
||||
max = Math.max(data[i].value, max);
|
||||
}
|
||||
return { min, max };
|
||||
}
|
||||
|
||||
|
||||
function makeChoroplethStyler(data, colorramp, joinField) {
|
||||
|
||||
|
||||
if (data.length === 0) {
|
||||
return {
|
||||
getLeafletStyleFunction: function () {
|
||||
return emptyStyle();
|
||||
},
|
||||
getMismatches: function () {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const { min, max } = getMinMax(data);
|
||||
const outstandingFeatures = data.slice();
|
||||
return {
|
||||
getLeafletStyleFunction: function (geojsonFeature) {
|
||||
let lastIndex = -1;
|
||||
const match = outstandingFeatures.find((bucket, index) => {
|
||||
lastIndex = index;
|
||||
if (typeof bucket.term === 'string' && typeof geojsonFeature.properties[joinField] === 'string') {
|
||||
return normalizeString(bucket.term) === normalizeString(geojsonFeature.properties[joinField]);
|
||||
} else {
|
||||
return bucket.term === geojsonFeature.properties[joinField];
|
||||
}
|
||||
});
|
||||
|
||||
if (!match) {
|
||||
return emptyStyle();
|
||||
}
|
||||
|
||||
outstandingFeatures.splice(lastIndex, 1);
|
||||
return {
|
||||
fillColor: getChoroplethColor(match.value, min, max, colorramp),
|
||||
weight: 2,
|
||||
opacity: 1,
|
||||
color: 'white',
|
||||
fillOpacity: 0.7
|
||||
};
|
||||
},
|
||||
/**
|
||||
* should not be called until getLeafletStyleFunction has been called
|
||||
* @return {Array}
|
||||
*/
|
||||
getMismatches: function () {
|
||||
return outstandingFeatures.map((bucket) => bucket.term);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
function normalizeString(string) {
|
||||
return string.trim().toLowerCase();
|
||||
}
|
||||
|
||||
|
||||
function getLegendColors(colorRamp) {
|
||||
const colors = [];
|
||||
colors[0] = getColor(colorRamp, 0);
|
||||
colors[1] = getColor(colorRamp, Math.floor(colorRamp.length * 1 / 4));
|
||||
colors[2] = getColor(colorRamp, Math.floor(colorRamp.length * 2 / 4));
|
||||
colors[3] = getColor(colorRamp, Math.floor(colorRamp.length * 3 / 4));
|
||||
colors[4] = getColor(colorRamp, colorRamp.length - 1);
|
||||
return colors;
|
||||
}
|
||||
|
||||
function getColor(colorRamp, i) {
|
||||
|
||||
if (!colorRamp[i]) {
|
||||
return getColor();
|
||||
}
|
||||
|
||||
const color = colorRamp[i][1];
|
||||
const red = Math.floor(color[0] * 255);
|
||||
const green = Math.floor(color[1] * 255);
|
||||
const blue = Math.floor(color[2] * 255);
|
||||
return `rgb(${red},${green},${blue})`;
|
||||
}
|
||||
|
||||
|
||||
function getChoroplethColor(value, min, max, colorRamp) {
|
||||
if (min === max) {
|
||||
return getColor(colorRamp, colorRamp.length - 1);
|
||||
}
|
||||
const fraction = (value - min) / (max - min);
|
||||
const index = Math.round(colorRamp.length * fraction) - 1;
|
||||
const i = Math.max(Math.min(colorRamp.length - 1, index), 0);
|
||||
|
||||
return getColor(colorRamp, i);
|
||||
}
|
||||
|
||||
const emptyStyleObject = {
|
||||
weight: 1,
|
||||
opacity: 0.6,
|
||||
color: 'rgb(200,200,200)',
|
||||
fillOpacity: 0
|
||||
};
|
||||
function emptyStyle() {
|
||||
return emptyStyleObject;
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="50" height="50" viewBox="0 0 50 50">
|
||||
<g fill-rule="evenodd">
|
||||
<polygon points="0 8.654 0 19.947 14.999 11.934 14.999 .541"/>
|
||||
<polygon points="0 22.147 0 49.038 14.999 41.827 14.999 14.135"/>
|
||||
<polygon points="34.999 26.511 34.999 49.458 49.999 41.346 49.999 18.027"/>
|
||||
<polygon points="49.999 0 34.999 8.114 34.999 24.28 49.999 15.796"/>
|
||||
<polygon points="16.999 26.298 32.999 33.655 32.999 8.173 16.999 .48"/>
|
||||
<polygon points="16.999 41.827 32.999 49.519 32.999 35.76 16.999 28.403"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 569 B |
7
src/core_plugins/region_map/public/region_map.less
Normal file
7
src/core_plugins/region_map/public/region_map.less
Normal file
|
@ -0,0 +1,7 @@
|
|||
.region-map-vis {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
<div ng-controller="KbnRegionMapController" class="region-map-vis">
|
||||
|
||||
</div>
|
145
src/core_plugins/region_map/public/region_map_controller.js
Normal file
145
src/core_plugins/region_map/public/region_map_controller.js
Normal file
|
@ -0,0 +1,145 @@
|
|||
import { uiModules } from 'ui/modules';
|
||||
import 'plugins/kbn_vislib_vis_types/controls/vislib_basic_options';
|
||||
import _ from 'lodash';
|
||||
import AggConfigResult from 'ui/vis/agg_config_result';
|
||||
import { KibanaMap } from 'ui/vis_maps/kibana_map';
|
||||
import ChoroplethLayer from './choropleth_layer';
|
||||
import { truncatedColorMaps } from 'ui/vislib/components/color/truncated_colormaps';
|
||||
import AggResponsePointSeriesTooltipFormatterProvider from './tooltip_formatter';
|
||||
import { ResizeCheckerProvider } from 'ui/resize_checker';
|
||||
import 'ui/vis_maps/lib/service_settings';
|
||||
|
||||
|
||||
const module = uiModules.get('kibana/region_map', ['kibana']);
|
||||
module.controller('KbnRegionMapController', function ($scope, $element, Private, Notifier, getAppState,
|
||||
serviceSettings, config) {
|
||||
|
||||
const tooltipFormatter = Private(AggResponsePointSeriesTooltipFormatterProvider);
|
||||
const ResizeChecker = Private(ResizeCheckerProvider);
|
||||
const notify = new Notifier({ location: 'Region map' });
|
||||
const resizeChecker = new ResizeChecker($element);
|
||||
|
||||
let kibanaMap = null;
|
||||
resizeChecker.on('resize', () => {
|
||||
if (kibanaMap) {
|
||||
kibanaMap.resize();
|
||||
}
|
||||
});
|
||||
let choroplethLayer = null;
|
||||
const kibanaMapReady = makeKibanaMap();
|
||||
|
||||
$scope.$watch('esResponse', async function (response) {
|
||||
kibanaMapReady.then(() => {
|
||||
const metricsAgg = _.first($scope.vis.aggs.bySchemaName.metric);
|
||||
const termAggId = _.first(_.pluck($scope.vis.aggs.bySchemaName.segment, 'id'));
|
||||
|
||||
let results;
|
||||
if (!response || !response.aggregations) {
|
||||
results = [];
|
||||
} else {
|
||||
const buckets = response.aggregations[termAggId].buckets;
|
||||
results = buckets.map((bucket) => {
|
||||
return {
|
||||
term: bucket.key,
|
||||
value: metricsAgg.getValue(bucket)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if (!$scope.vis.params.selectedJoinField && $scope.vis.params.selectedLayer) {
|
||||
$scope.vis.params.selectedJoinField = $scope.vis.params.selectedLayer.fields[0];
|
||||
}
|
||||
|
||||
if (!$scope.vis.params.selectedLayer) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateChoroplethLayer($scope.vis.params.selectedLayer.url);
|
||||
choroplethLayer.setMetrics(results, metricsAgg);
|
||||
setTooltipFormatter();
|
||||
|
||||
kibanaMap.useUiStateFromVisualization($scope.vis);
|
||||
kibanaMap.resize();
|
||||
$element.trigger('renderComplete');
|
||||
});
|
||||
});
|
||||
|
||||
$scope.$watch('vis.params', (visParams) => {
|
||||
kibanaMapReady.then(() => {
|
||||
if (!visParams.selectedJoinField && visParams.selectedLayer) {
|
||||
visParams.selectedJoinField = visParams.selectedLayer.fields[0];
|
||||
}
|
||||
|
||||
if (!visParams.selectedJoinField || !visParams.selectedLayer) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateChoroplethLayer(visParams.selectedLayer.url);
|
||||
choroplethLayer.setJoinField(visParams.selectedJoinField.name);
|
||||
choroplethLayer.setColorRamp(truncatedColorMaps[visParams.colorSchema]);
|
||||
setTooltipFormatter();
|
||||
|
||||
kibanaMap.setShowTooltip(visParams.addTooltip);
|
||||
kibanaMap.setLegendPosition(visParams.legendPosition);
|
||||
kibanaMap.useUiStateFromVisualization($scope.vis);
|
||||
kibanaMap.resize();
|
||||
$element.trigger('renderComplete');
|
||||
});
|
||||
});
|
||||
|
||||
async function makeKibanaMap() {
|
||||
const tmsSettings = await serviceSettings.getTMSService();
|
||||
const minMaxZoom = tmsSettings.getMinMaxZoom(false);
|
||||
kibanaMap = new KibanaMap($element[0], minMaxZoom);
|
||||
const url = tmsSettings.getUrl();
|
||||
const options = tmsSettings.getTMSOptions();
|
||||
kibanaMap.setBaseLayer({ baseLayerType: 'tms', options: { url, ...options } });
|
||||
kibanaMap.addLegendControl();
|
||||
kibanaMap.addFitControl();
|
||||
kibanaMap.persistUiStateForVisualization($scope.vis);
|
||||
}
|
||||
|
||||
function setTooltipFormatter() {
|
||||
const metricsAgg = _.first($scope.vis.aggs.bySchemaName.metric);
|
||||
if ($scope.vis.aggs.bySchemaName.segment && $scope.vis.aggs.bySchemaName.segment[0]) {
|
||||
const fieldName = $scope.vis.aggs.bySchemaName.segment[0].makeLabel();
|
||||
choroplethLayer.setTooltipFormatter(tooltipFormatter, metricsAgg, fieldName);
|
||||
} else {
|
||||
choroplethLayer.setTooltipFormatter(tooltipFormatter, metricsAgg, null);
|
||||
}
|
||||
}
|
||||
|
||||
function updateChoroplethLayer(url) {
|
||||
|
||||
if (choroplethLayer && choroplethLayer.equalsGeoJsonUrl(url)) {//no need to recreate the layer
|
||||
return;
|
||||
}
|
||||
kibanaMap.removeLayer(choroplethLayer);
|
||||
|
||||
const previousMetrics = choroplethLayer ? choroplethLayer.getMetrics() : null;
|
||||
const previousMetricsAgg = choroplethLayer ? choroplethLayer.getMetricsAgg() : null;
|
||||
choroplethLayer = new ChoroplethLayer(url);
|
||||
if (previousMetrics && previousMetricsAgg) {
|
||||
choroplethLayer.setMetrics(previousMetrics, previousMetricsAgg);
|
||||
}
|
||||
choroplethLayer.on('select', function (event) {
|
||||
const aggs = $scope.vis.aggs.getResponseAggs();
|
||||
const aggConfigResult = new AggConfigResult(aggs[0], false, event, event);
|
||||
$scope.vis.listeners.click({ point: { aggConfigResult: aggConfigResult } });
|
||||
});
|
||||
choroplethLayer.on('styleChanged', function (event) {
|
||||
if (event.mismatches.length > 0 && config.get('visualization:regionmap:showWarnings')) {
|
||||
notify.warning(
|
||||
`Could not show ${event.mismatches.length} ${event.mismatches.length > 1 ? 'results' : 'result'} on the map.`
|
||||
+ ` To avoid this, ensure that each term can be joined to a corresponding shape on that shape's join field.`
|
||||
+ ` Could not join following terms: ${event.mismatches.join(',')}`
|
||||
);
|
||||
}
|
||||
});
|
||||
kibanaMap.addLayer(choroplethLayer);
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
81
src/core_plugins/region_map/public/region_map_vis.js
Normal file
81
src/core_plugins/region_map/public/region_map_vis.js
Normal file
|
@ -0,0 +1,81 @@
|
|||
import './region_map.less';
|
||||
import './region_map_controller';
|
||||
import './region_map_vis_params';
|
||||
import regionTemplate from './region_map_controller.html';
|
||||
import image from './images/icon-vector-map.svg';
|
||||
import { TemplateVisTypeProvider } from 'ui/template_vis_type/template_vis_type';
|
||||
import { VisSchemasProvider } from 'ui/vis/schemas';
|
||||
import { VisTypesRegistryProvider } from 'ui/registry/vis_types';
|
||||
import { VisVisTypeProvider } from 'ui/vis/vis_type';
|
||||
import { truncatedColorMaps } from 'ui/vislib/components/color/truncated_colormaps';
|
||||
|
||||
VisTypesRegistryProvider.register(function RegionMapProvider(Private, regionmapsConfig) {
|
||||
|
||||
const VisType = Private(VisVisTypeProvider);
|
||||
const TemplateVisType = Private(TemplateVisTypeProvider);
|
||||
const Schemas = Private(VisSchemasProvider);
|
||||
|
||||
const vectorLayers = regionmapsConfig.layers;
|
||||
const selectedLayer = vectorLayers[0];
|
||||
const selectedJoinField = selectedLayer ? vectorLayers[0].fields[0] : null;
|
||||
|
||||
return new TemplateVisType({
|
||||
name: 'region_map',
|
||||
title: 'Region Map',
|
||||
implementsRenderComplete: true,
|
||||
description: 'Show metrics on a thematic map. Use one of the provide base maps, or add your own. ' +
|
||||
'Darker colors represent higher values.',
|
||||
category: VisType.CATEGORY.MAP,
|
||||
image,
|
||||
template: regionTemplate,
|
||||
params: {
|
||||
defaults: {
|
||||
legendPosition: 'bottomright',
|
||||
addTooltip: true,
|
||||
colorSchema: 'Yellow to Red',
|
||||
selectedLayer: selectedLayer,
|
||||
selectedJoinField: selectedJoinField
|
||||
},
|
||||
legendPositions: [{
|
||||
value: 'bottomleft',
|
||||
text: 'bottom left',
|
||||
}, {
|
||||
value: 'bottomright',
|
||||
text: 'bottom right',
|
||||
}, {
|
||||
value: 'topleft',
|
||||
text: 'top left',
|
||||
}, {
|
||||
value: 'topright',
|
||||
text: 'top right',
|
||||
}],
|
||||
colorSchemas: Object.keys(truncatedColorMaps),
|
||||
vectorLayers: vectorLayers,
|
||||
editor: '<region_map-vis-params></region_map-vis-params>'
|
||||
},
|
||||
schemas: new Schemas([
|
||||
{
|
||||
group: 'metrics',
|
||||
name: 'metric',
|
||||
title: 'Value',
|
||||
min: 1,
|
||||
max: 1,
|
||||
aggFilter: ['count', 'avg', 'sum', 'min', 'max', 'cardinality', 'top_hits', 'sum_bucket', 'min_bucket', 'max_bucket', 'avg_bucket'],
|
||||
defaults: [
|
||||
{ schema: 'metric', type: 'count' }
|
||||
]
|
||||
},
|
||||
{
|
||||
group: 'buckets',
|
||||
name: 'segment',
|
||||
icon: 'fa fa-globe',
|
||||
title: 'shape field',
|
||||
min: 1,
|
||||
max: 1,
|
||||
aggFilter: ['terms']
|
||||
}
|
||||
])
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
<div class="form-group">
|
||||
|
||||
<div class="kuiSideBarSectionTitle">
|
||||
<div class="kuiSideBarSectionTitle__text">
|
||||
Layer Settings
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="kuiSideBarFormRow">
|
||||
<label class="kuiSideBarFormRow__label" for="regionMap">
|
||||
Vector map
|
||||
</label>
|
||||
<div class="kuiSideBarFormRow__control">
|
||||
<select
|
||||
id="regionMap"
|
||||
class="kuiSelect kuiSideBarSelect"
|
||||
ng-model="vis.params.selectedLayer"
|
||||
ng-options="layer.name for layer in vis.type.params.vectorLayers"
|
||||
ng-change="onLayerChange()"
|
||||
ng-init="vis.params.selectedLayer=vis.type.params.vectorLayers[0]"
|
||||
></select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="kuiSideBarFormRow">
|
||||
<label class="kuiSideBarFormRow__label" for="joinField">
|
||||
Join on field
|
||||
</label>
|
||||
<div class="kuiSideBarFormRow__control">
|
||||
<select id="joinField"
|
||||
ng-model="vis.params.selectedJoinField"
|
||||
ng-options="field.description for field in vis.params.selectedLayer.fields"
|
||||
ng-init="vis.params.selectedJoinField=vis.params.selectedLayer.fields[0]"
|
||||
>
|
||||
<option value=''>Select</option></select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="kuiSideBarSectionTitle">
|
||||
<div class="kuiSideBarSectionTitle__text">
|
||||
Style Settings
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="kuiSideBarFormRow" >
|
||||
<label class="kuiSideBarFormRow__label" for="colorSchema">
|
||||
Color Schema
|
||||
</label>
|
||||
<div class="kuiSideBarFormRow__control">
|
||||
<select
|
||||
id="colorSchema"
|
||||
class="kuiSelect kuiSideBarSelect"
|
||||
ng-model="vis.params.colorSchema"
|
||||
ng-options="mode for mode in vis.type.params.colorSchemas"
|
||||
></select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="kuiSideBarSectionTitle">
|
||||
<div class="kuiSideBarSectionTitle__text">
|
||||
Basic Settings
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<vislib-basic-options></vislib-basic-options>
|
||||
|
||||
|
||||
</div>
|
54
src/core_plugins/region_map/public/region_map_vis_params.js
Normal file
54
src/core_plugins/region_map/public/region_map_vis_params.js
Normal file
|
@ -0,0 +1,54 @@
|
|||
import { uiModules } from 'ui/modules';
|
||||
import regionMapVisParamsTemplate from './region_map_vis_params.html';
|
||||
import _ from 'lodash';
|
||||
|
||||
uiModules.get('kibana/region_map')
|
||||
.directive('regionMapVisParams', function (serviceSettings, Notifier) {
|
||||
|
||||
|
||||
const notify = new Notifier({ location: 'Region map' });
|
||||
|
||||
return {
|
||||
restrict: 'E',
|
||||
template: regionMapVisParamsTemplate,
|
||||
link: function ($scope) {
|
||||
|
||||
$scope.onLayerChange = onLayerChange;
|
||||
serviceSettings.getFileLayers()
|
||||
.then(function (layersFromService) {
|
||||
|
||||
const newVectorLayers = $scope.vis.type.params.vectorLayers.slice();
|
||||
for (let i = 0; i < layersFromService.length; i += 1) {
|
||||
const layerFromService = layersFromService[i];
|
||||
const alreadyAdded = newVectorLayers.some((layer) =>_.eq(layerFromService, layer));
|
||||
if (!alreadyAdded) {
|
||||
newVectorLayers.push(layerFromService);
|
||||
}
|
||||
}
|
||||
|
||||
$scope.vis.type.params.vectorLayers = newVectorLayers;
|
||||
|
||||
if ($scope.vis.type.params.vectorLayers[0] && !$scope.vis.params.selectedLayer) {
|
||||
$scope.vis.params.selectedLayer = $scope.vis.type.params.vectorLayers[0];
|
||||
onLayerChange();
|
||||
}
|
||||
|
||||
|
||||
//the dirty flag is set to true because the change in vector layers config causes an update of the scope.params
|
||||
//temp work-around. addressing this issue with the visualize refactor for 6.0
|
||||
setTimeout(function () {
|
||||
$scope.dirty = false;
|
||||
}, 0);
|
||||
|
||||
})
|
||||
.catch(function (error) {
|
||||
notify.warning(error.message);
|
||||
});
|
||||
|
||||
function onLayerChange() {
|
||||
$scope.vis.params.selectedJoinField = $scope.vis.params.selectedLayer.fields[0];
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
});
|
8
src/core_plugins/region_map/public/tooltip.html
Normal file
8
src/core_plugins/region_map/public/tooltip.html
Normal file
|
@ -0,0 +1,8 @@
|
|||
<table>
|
||||
<tbody>
|
||||
<tr ng-repeat="detail in details" >
|
||||
<td class="tooltip-label"><b>{{detail.label}}</b></td>
|
||||
<td class="tooltip-value">{{detail.value}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
32
src/core_plugins/region_map/public/tooltip_formatter.js
Normal file
32
src/core_plugins/region_map/public/tooltip_formatter.js
Normal file
|
@ -0,0 +1,32 @@
|
|||
import $ from 'jquery';
|
||||
export default function TileMapTooltipFormatter($compile, $rootScope) {
|
||||
|
||||
const $tooltipScope = $rootScope.$new();
|
||||
const $el = $('<div>').html(require('./tooltip.html'));
|
||||
$compile($el)($tooltipScope);
|
||||
|
||||
return function tooltipFormatter(metricAgg, metric, fieldName) {
|
||||
|
||||
if (!metric) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$tooltipScope.details = [];
|
||||
if (fieldName && metric) {
|
||||
$tooltipScope.details.push({
|
||||
label: fieldName,
|
||||
value: metric.term
|
||||
});
|
||||
}
|
||||
|
||||
if (metric) {
|
||||
$tooltipScope.details.push({
|
||||
label: metricAgg.makeLabel(),
|
||||
value: metricAgg.fieldFormatter()(metric.value)
|
||||
});
|
||||
}
|
||||
|
||||
$tooltipScope.$apply();
|
||||
return $el.html();
|
||||
};
|
||||
}
|
|
@ -30,17 +30,18 @@ window.__KBN__ = {
|
|||
esRequestTimeout: '300000',
|
||||
tilemapsConfig: {
|
||||
deprecated: {
|
||||
isOverridden: true,
|
||||
config: {
|
||||
url: 'https://tiles.elastic.co/v1/default/{z}/{x}/{y}.png?my_app_name=kibana&my_app_version=1.2.3&elastic_tile_service_tos=agree',
|
||||
options: {
|
||||
minZoom: 1,
|
||||
maxZoom: 10,
|
||||
attribution: '© [Elastic Tile Service](https://www.elastic.co/elastic_tile_service)'
|
||||
isOverridden: false,
|
||||
config: {
|
||||
options: {
|
||||
}
|
||||
}
|
||||
},
|
||||
manifestServiceUrl: 'https://proxy-tiles.elastic.co/v1/manifest'
|
||||
}
|
||||
},
|
||||
regionmapsConfig: {
|
||||
layers: []
|
||||
},
|
||||
mapConfig: {
|
||||
manifestServiceUrl: 'https://geo.elastic.co/v1/manifest'
|
||||
}
|
||||
},
|
||||
uiSettings: {
|
||||
|
|
|
@ -163,12 +163,14 @@ module.exports = () => Joi.object({
|
|||
allowAnonymous: Joi.boolean().default(false),
|
||||
v6ApiFormat: Joi.boolean().default(false)
|
||||
}).default(),
|
||||
tilemap: Joi.object({
|
||||
map: Joi.object({
|
||||
manifestServiceUrl: Joi.when('$dev', {
|
||||
is: true,
|
||||
then: Joi.string().default('https://tiles-stage.elastic.co/v2/manifest'),
|
||||
otherwise: Joi.string().default('https://tiles.elastic.co/v2/manifest')
|
||||
}),
|
||||
then: Joi.string().default('https://geo.elastic.co/v1/manifest'),
|
||||
otherwise: Joi.string().default('https://geo.elastic.co/v1/manifest')
|
||||
})
|
||||
}).default(),
|
||||
tilemap: Joi.object({
|
||||
url: Joi.string(),
|
||||
options: Joi.object({
|
||||
attribution: Joi.string(),
|
||||
|
@ -182,6 +184,17 @@ module.exports = () => Joi.object({
|
|||
bounds: Joi.array().items(Joi.array().items(Joi.number()).min(2).required()).min(2)
|
||||
}).default()
|
||||
}).default(),
|
||||
regionmap: Joi.object({
|
||||
layers: Joi.array().items(Joi.object({
|
||||
url: Joi.string(),
|
||||
type: Joi.string(),
|
||||
name: Joi.string(),
|
||||
fields: Joi.array().items(Joi.object({
|
||||
name: Joi.string(),
|
||||
description: Joi.string()
|
||||
}))
|
||||
}))
|
||||
}).default(),
|
||||
uiSettings: Joi.object({
|
||||
// this is used to prevent the uiSettings from initializing. Since they
|
||||
// require the elasticsearch plugin in order to function we need to turn
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
/* eslint-disable */
|
||||
/*
|
||||
* Decodes geohash to object containing
|
||||
* top-left and bottom-right corners of
|
||||
|
|
|
@ -40,13 +40,13 @@ describe('kibana_map tests', function () {
|
|||
teardownDOM();
|
||||
});
|
||||
|
||||
it('should instantiate with world in view', function () {
|
||||
it('should instantiate at zoom level 2', function () {
|
||||
const bounds = kibanaMap.getBounds();
|
||||
expect(bounds.bottom_right.lon).to.equal(180);
|
||||
expect(bounds.top_left.lon).to.equal(-180);
|
||||
expect(bounds.bottom_right.lon).to.equal(90);
|
||||
expect(bounds.top_left.lon).to.equal(-90);
|
||||
expect(kibanaMap.getCenter().lon).to.equal(0);
|
||||
expect(kibanaMap.getCenter().lat).to.equal(0);
|
||||
expect(kibanaMap.getZoomLevel()).to.equal(1);
|
||||
expect(kibanaMap.getZoomLevel()).to.equal(2);
|
||||
});
|
||||
|
||||
it('should resize to fit container', function () {
|
||||
|
|
202
src/ui/public/vis_maps/__tests__/service_settings.js
Normal file
202
src/ui/public/vis_maps/__tests__/service_settings.js
Normal file
|
@ -0,0 +1,202 @@
|
|||
import expect from 'expect.js';
|
||||
import ngMock from 'ng_mock';
|
||||
import url from 'url';
|
||||
import sinon from 'sinon';
|
||||
|
||||
describe('service_settings (FKA tilemaptest)', function () {
|
||||
|
||||
|
||||
let serviceSettings;
|
||||
let mapsConfig;
|
||||
|
||||
const manifestUrl = 'https://geo.elastic.co/v1/manifest';
|
||||
const tmsManifestUrl = `https://tiles.elastic.co/v2/manifest`;
|
||||
const vectorManifestUrl = `https://layers.geo.elastic.co/v1/manifest`;
|
||||
const manifestUrl2 = 'https://foobar/v1/manifest';
|
||||
|
||||
const manifest = {
|
||||
'services': [{
|
||||
'id': 'tiles_v2',
|
||||
'name': 'Elastic Tile Service',
|
||||
'manifest': tmsManifestUrl,
|
||||
'type': 'tms'
|
||||
},
|
||||
{
|
||||
'id': 'geo_layers',
|
||||
'name': 'Elastic Layer Service',
|
||||
'manifest': vectorManifestUrl,
|
||||
'type': 'file'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const tmsManifest = {
|
||||
'services': [{
|
||||
'id': 'road_map',
|
||||
'url': 'https://tiles.elastic.co/v2/default/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana',
|
||||
'minZoom': 0,
|
||||
'maxZoom': 10,
|
||||
'attribution': '© [OpenStreetMap](http://www.openstreetmap.org/copyright) © [Elastic Tile Service](https://www.elastic.co/elastic-tile-service)'
|
||||
}]
|
||||
};
|
||||
|
||||
const vectorManifest = {
|
||||
'layers': [{
|
||||
'attribution': '',
|
||||
'name': 'US States',
|
||||
'format': 'geojson',
|
||||
'url': 'https://storage.googleapis.com/elastic-layer.appspot.com/L2FwcGhvc3RpbmdfcHJvZC9ibG9icy9BRW5CMlVvNGJ0aVNidFNJR2dEQl9rbTBjeXhKMU01WjRBeW1kN3JMXzM2Ry1qc3F6QjF4WE5XdHY2ODlnQkRpZFdCY2g1T2dqUGRHSFhSRTU3amlxTVFwZjNBSFhycEFwV2lYR29vTENjZjh1QTZaZnRpaHBzby5VXzZoNk1paGJYSkNPalpI?elastic_tile_service_tos=agree',
|
||||
'fields': [{ 'name': 'postal', 'description': 'Two letter abbreviation' }, {
|
||||
'name': 'name',
|
||||
'description': 'State name'
|
||||
}],
|
||||
'created_at': '2017-04-26T19:45:22.377820',
|
||||
'id': 5086441721823232
|
||||
}, {
|
||||
'attribution': '© [Elastic Tile Service](https://www.elastic.co/elastic-tile-service)',
|
||||
'name': 'World Countries',
|
||||
'format': 'geojson',
|
||||
'url': 'https://storage.googleapis.com/elastic-layer.appspot.com/L2FwcGhvc3RpbmdfcHJvZC9ibG9icy9BRW5CMlVwWTZTWnhRRzNmUk9HUE93TENjLXNVd2IwdVNpc09SRXRyRzBVWWdqOU5qY2hldGJLOFNZSFpUMmZmZWdNZGx0NWprT1R1ZkZ0U1JEdFBtRnkwUWo0S0JuLTVYY1I5RFdSMVZ5alBIZkZuME1qVS04TS5oQTRNTl9yRUJCWk9tMk03?elastic_tile_service_tos=agree',
|
||||
'fields': [{ 'name': 'iso2', 'description': 'Two letter abbreviation' }, {
|
||||
'name': 'name',
|
||||
'description': 'Country name'
|
||||
}, { 'name': 'iso3', 'description': 'Three letter abbreviation' }],
|
||||
'created_at': '2017-04-26T17:12:15.978370',
|
||||
'id': 5659313586569216
|
||||
}]
|
||||
};
|
||||
|
||||
|
||||
beforeEach(ngMock.module('kibana', ($provide) => {
|
||||
|
||||
$provide.decorator('mapConfig', () => {
|
||||
return {
|
||||
manifestServiceUrl: manifestUrl
|
||||
};
|
||||
});
|
||||
}));
|
||||
|
||||
beforeEach(ngMock.inject(function ($injector, $rootScope) {
|
||||
|
||||
serviceSettings = $injector.get('serviceSettings');
|
||||
mapsConfig = $injector.get('mapConfig');
|
||||
|
||||
sinon.stub(serviceSettings, '_getManifest', function (url) {
|
||||
let contents = null;
|
||||
if (url.startsWith(tmsManifestUrl)) {
|
||||
contents = tmsManifest;
|
||||
} else if (url.startsWith(vectorManifestUrl)) {
|
||||
contents = vectorManifest;
|
||||
} else if (url.startsWith(manifestUrl)) {
|
||||
contents = manifest;
|
||||
} else if (url.startsWith(manifestUrl2)) {
|
||||
contents = manifest;
|
||||
}
|
||||
return {
|
||||
data: contents
|
||||
};
|
||||
});
|
||||
$rootScope.$digest();
|
||||
}));
|
||||
|
||||
afterEach(function () {
|
||||
serviceSettings._getManifest.restore();
|
||||
});
|
||||
|
||||
describe('TMS', function () {
|
||||
|
||||
it('should get url', async function () {
|
||||
const tmsService = await serviceSettings.getTMSService();
|
||||
const mapUrl = tmsService.getUrl();
|
||||
expect(mapUrl).to.contain('{x}');
|
||||
expect(mapUrl).to.contain('{y}');
|
||||
expect(mapUrl).to.contain('{z}');
|
||||
|
||||
const urlObject = url.parse(mapUrl, true);
|
||||
expect(urlObject.hostname).to.be('tiles.elastic.co');
|
||||
expect(urlObject.query).to.have.property('my_app_name', 'kibana');
|
||||
expect(urlObject.query).to.have.property('elastic_tile_service_tos', 'agree');
|
||||
expect(urlObject.query).to.have.property('my_app_version');
|
||||
});
|
||||
|
||||
it('should get options', async function () {
|
||||
const tmsService = await serviceSettings.getTMSService();
|
||||
const options = tmsService.getTMSOptions();
|
||||
expect(options).to.have.property('minZoom');
|
||||
expect(options).to.have.property('maxZoom');
|
||||
expect(options).to.have.property('attribution').contain('©');
|
||||
});
|
||||
|
||||
describe('modify - url', function () {
|
||||
|
||||
let tilemapSettings;
|
||||
|
||||
function assertQuery(expected) {
|
||||
const mapUrl = tilemapSettings.getUrl();
|
||||
const urlObject = url.parse(mapUrl, true);
|
||||
Object.keys(expected).forEach(key => {
|
||||
expect(urlObject.query).to.have.property(key, expected[key]);
|
||||
});
|
||||
}
|
||||
|
||||
it('accepts an object', async() => {
|
||||
serviceSettings.addQueryParams({ foo: 'bar' });
|
||||
tilemapSettings = await serviceSettings.getTMSService();
|
||||
assertQuery({ foo: 'bar' });
|
||||
});
|
||||
|
||||
it('merged additions with previous values', async() => {
|
||||
// ensure that changes are always additive
|
||||
serviceSettings.addQueryParams({ foo: 'bar' });
|
||||
serviceSettings.addQueryParams({ bar: 'stool' });
|
||||
tilemapSettings = await serviceSettings.getTMSService();
|
||||
assertQuery({ foo: 'bar', bar: 'stool' });
|
||||
});
|
||||
|
||||
it('overwrites conflicting previous values', async() => {
|
||||
// ensure that conflicts are overwritten
|
||||
serviceSettings.addQueryParams({ foo: 'bar' });
|
||||
serviceSettings.addQueryParams({ bar: 'stool' });
|
||||
serviceSettings.addQueryParams({ foo: 'tstool' });
|
||||
tilemapSettings = await serviceSettings.getTMSService();
|
||||
assertQuery({ foo: 'tstool', bar: 'stool' });
|
||||
});
|
||||
|
||||
it('when overridden, should continue to work', async() => {
|
||||
mapsConfig.manifestServiceUrl = manifestUrl2;
|
||||
serviceSettings.addQueryParams({ foo: 'bar' });
|
||||
tilemapSettings = await serviceSettings.getTMSService();
|
||||
assertQuery({ foo: 'bar' });
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
describe('File layers', function () {
|
||||
|
||||
|
||||
it('should load manifest', async function () {
|
||||
serviceSettings.addQueryParams({ foo:'bar' });
|
||||
const fileLayers = await serviceSettings.getFileLayers();
|
||||
fileLayers.forEach(function (fileLayer, index) {
|
||||
const expected = vectorManifest.layers[index];
|
||||
expect(expected.attribution).to.eql(fileLayer.attribution);
|
||||
expect(expected.format).to.eql(fileLayer.format);
|
||||
expect(expected.fields).to.eql(fileLayer.fields);
|
||||
expect(expected.name).to.eql(fileLayer.name);
|
||||
expect(expected.created_at).to.eql(fileLayer.created_at);
|
||||
|
||||
const urlObject = url.parse(fileLayer.url, true);
|
||||
Object.keys({ foo:'bar', elastic_tile_service_tos: 'agree' }).forEach(key => {
|
||||
expect(urlObject.query).to.have.property(key, expected[key]);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
});
|
||||
});
|
|
@ -1,61 +0,0 @@
|
|||
import expect from 'expect.js';
|
||||
import ngMock from 'ng_mock';
|
||||
import url from 'url';
|
||||
|
||||
describe('tilemaptest - TileMapSettingsTests-deprecated', function () {
|
||||
let tilemapSettings;
|
||||
let loadSettings;
|
||||
|
||||
beforeEach(ngMock.module('kibana', ($provide) => {
|
||||
$provide.decorator('tilemapsConfig', () => ({
|
||||
manifestServiceUrl: 'https://proxy-tiles.elastic.co/v1/manifest',
|
||||
deprecated: {
|
||||
isOverridden: true,
|
||||
config: {
|
||||
url: 'https://tiles.elastic.co/v1/default/{z}/{x}/{y}.png?my_app_name=kibana_tests',
|
||||
options: {
|
||||
minZoom: 1,
|
||||
maxZoom: 10,
|
||||
attribution: '© [Elastic Tile Service](https://www.elastic.co/elastic_tile_service)'
|
||||
}
|
||||
},
|
||||
}
|
||||
}));
|
||||
}));
|
||||
|
||||
beforeEach(ngMock.inject(function ($injector, $rootScope) {
|
||||
tilemapSettings = $injector.get('tilemapSettings');
|
||||
|
||||
loadSettings = () => {
|
||||
tilemapSettings.loadSettings();
|
||||
$rootScope.$digest();
|
||||
};
|
||||
}));
|
||||
|
||||
describe('getting settings', function () {
|
||||
beforeEach(function () {
|
||||
loadSettings();
|
||||
});
|
||||
|
||||
it('should get url', function () {
|
||||
|
||||
const mapUrl = tilemapSettings.getUrl();
|
||||
expect(mapUrl).to.contain('{x}');
|
||||
expect(mapUrl).to.contain('{y}');
|
||||
expect(mapUrl).to.contain('{z}');
|
||||
|
||||
const urlObject = url.parse(mapUrl, true);
|
||||
expect(urlObject.hostname).to.be('tiles.elastic.co');
|
||||
expect(urlObject.query).to.have.property('my_app_name', 'kibana_tests');
|
||||
|
||||
});
|
||||
|
||||
it('should get options', function () {
|
||||
const options = tilemapSettings.getTMSOptions();
|
||||
expect(options).to.have.property('minZoom');
|
||||
expect(options).to.have.property('maxZoom');
|
||||
expect(options).to.have.property('attribution');
|
||||
});
|
||||
|
||||
});
|
||||
});
|
|
@ -1,140 +0,0 @@
|
|||
import expect from 'expect.js';
|
||||
import ngMock from 'ng_mock';
|
||||
import url from 'url';
|
||||
|
||||
describe('tilemaptest - TileMapSettingsTests-mocked', function () {
|
||||
let tilemapSettings;
|
||||
let tilemapsConfig;
|
||||
let loadSettings;
|
||||
|
||||
beforeEach(ngMock.module('kibana', ($provide) => {
|
||||
$provide.decorator('tilemapsConfig', () => ({
|
||||
manifestServiceUrl: 'http://foo.bar/manifest',
|
||||
deprecated: {
|
||||
isOverridden: false,
|
||||
config: {
|
||||
url: '',
|
||||
options: {
|
||||
minZoom: 1,
|
||||
maxZoom: 10,
|
||||
attribution: '© [Elastic Tile Service](https://www.elastic.co/elastic_tile_service)'
|
||||
}
|
||||
},
|
||||
}
|
||||
}));
|
||||
}));
|
||||
|
||||
beforeEach(ngMock.inject(($injector, $httpBackend) => {
|
||||
tilemapSettings = $injector.get('tilemapSettings');
|
||||
tilemapsConfig = $injector.get('tilemapsConfig');
|
||||
|
||||
loadSettings = (expectedUrl) => {
|
||||
// body and headers copied from https://proxy-tiles.elastic.co/v1/manifest
|
||||
const MANIFEST_BODY = `{
|
||||
"services":[
|
||||
{
|
||||
"id":"road_map",
|
||||
"url":"https://proxy-tiles.elastic.co/v1/default/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana",
|
||||
"minZoom":0,
|
||||
"maxZoom":12,
|
||||
"attribution":"© [Elastic Tile Service](https://www.elastic.co/elastic-tile-service)"
|
||||
}
|
||||
]
|
||||
}`;
|
||||
|
||||
const MANIFEST_HEADERS = {
|
||||
'access-control-allow-methods': 'GET, OPTIONS',
|
||||
'access-control-allow-origin': '*',
|
||||
'content-length': `${MANIFEST_BODY.length}`,
|
||||
'content-type': 'application/json; charset=utf-8',
|
||||
date: (new Date()).toUTCString(),
|
||||
server: 'tileprox/20170102101655-a02e54d',
|
||||
status: '200',
|
||||
};
|
||||
|
||||
$httpBackend
|
||||
.expect('GET', expectedUrl ? expectedUrl : () => true)
|
||||
.respond(MANIFEST_BODY, MANIFEST_HEADERS);
|
||||
|
||||
tilemapSettings.loadSettings();
|
||||
|
||||
$httpBackend.flush();
|
||||
};
|
||||
}));
|
||||
|
||||
afterEach(ngMock.inject($httpBackend => {
|
||||
$httpBackend.verifyNoOutstandingRequest();
|
||||
$httpBackend.verifyNoOutstandingExpectation();
|
||||
}));
|
||||
|
||||
describe('getting settings', function () {
|
||||
beforeEach(() => {
|
||||
loadSettings();
|
||||
});
|
||||
|
||||
|
||||
it('should get url', async function () {
|
||||
|
||||
const mapUrl = tilemapSettings.getUrl();
|
||||
expect(mapUrl).to.contain('{x}');
|
||||
expect(mapUrl).to.contain('{y}');
|
||||
expect(mapUrl).to.contain('{z}');
|
||||
|
||||
const urlObject = url.parse(mapUrl, true);
|
||||
expect(urlObject).to.have.property('hostname', 'proxy-tiles.elastic.co');
|
||||
expect(urlObject.query).to.have.property('my_app_name', 'kibana');
|
||||
expect(urlObject.query).to.have.property('elastic_tile_service_tos', 'agree');
|
||||
expect(urlObject.query).to.have.property('my_app_version');
|
||||
|
||||
});
|
||||
|
||||
it('should get options', async function () {
|
||||
const options = tilemapSettings.getTMSOptions();
|
||||
expect(options).to.have.property('minZoom', 0);
|
||||
expect(options).to.have.property('maxZoom', 12);
|
||||
expect(options).to.have.property('attribution').contain('©'); // html entity for ©, ensures that attribution is escaped
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('modify', function () {
|
||||
function assertQuery(expected) {
|
||||
const mapUrl = tilemapSettings.getUrl();
|
||||
const urlObject = url.parse(mapUrl, true);
|
||||
Object.keys(expected).forEach(key => {
|
||||
expect(urlObject.query).to.have.property(key, expected[key]);
|
||||
});
|
||||
}
|
||||
|
||||
it('accepts an object', () => {
|
||||
tilemapSettings.addQueryParams({ foo: 'bar' });
|
||||
loadSettings();
|
||||
assertQuery({ foo: 'bar' });
|
||||
});
|
||||
|
||||
it('merged additions with previous values', () => {
|
||||
// ensure that changes are always additive
|
||||
tilemapSettings.addQueryParams({ foo: 'bar' });
|
||||
tilemapSettings.addQueryParams({ bar: 'stool' });
|
||||
loadSettings();
|
||||
assertQuery({ foo: 'bar', bar: 'stool' });
|
||||
});
|
||||
|
||||
it('overwrites conflicting previous values', () => {
|
||||
// ensure that conflicts are overwritten
|
||||
tilemapSettings.addQueryParams({ foo: 'bar' });
|
||||
tilemapSettings.addQueryParams({ bar: 'stool' });
|
||||
tilemapSettings.addQueryParams({ foo: 'tstool' });
|
||||
loadSettings();
|
||||
assertQuery({ foo: 'tstool', bar: 'stool' });
|
||||
});
|
||||
|
||||
it('merges query params into manifest request', () => {
|
||||
tilemapSettings.addQueryParams({ foo: 'bar' });
|
||||
tilemapsConfig.manifestServiceUrl = 'http://test.com/manifest?v=1';
|
||||
loadSettings('http://test.com/manifest?v=1&my_app_version=1.2.3&foo=bar');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
|
@ -101,7 +101,7 @@ export class KibanaMap extends EventEmitter {
|
|||
minZoom: options.minZoom,
|
||||
maxZoom: options.maxZoom,
|
||||
center: options.center ? options.center : [0, 0],
|
||||
zoom: options.zoom ? options.zoom : 0
|
||||
zoom: options.zoom ? options.zoom : 2
|
||||
};
|
||||
|
||||
this._leafletMap = L.map(containerNode, leafletOptions);
|
||||
|
|
175
src/ui/public/vis_maps/lib/service_settings.js
Normal file
175
src/ui/public/vis_maps/lib/service_settings.js
Normal file
|
@ -0,0 +1,175 @@
|
|||
import { uiModules } from 'ui/modules';
|
||||
import _ from 'lodash';
|
||||
import marked from 'marked';
|
||||
import { modifyUrl } from 'ui/url';
|
||||
marked.setOptions({
|
||||
gfm: true, // Github-flavored markdown
|
||||
sanitize: true // Sanitize HTML tags
|
||||
});
|
||||
|
||||
uiModules.get('kibana')
|
||||
.service('serviceSettings', function ($http, $sanitize, mapConfig, tilemapsConfig, kbnVersion) {
|
||||
|
||||
|
||||
const attributionFromConfig = $sanitize(marked(tilemapsConfig.deprecated.config.options.attribution || ''));
|
||||
const tmsOptionsFromConfig = _.assign({}, tilemapsConfig.deprecated.config.options, { attribution: attributionFromConfig });
|
||||
|
||||
const extendUrl = (url, props) => (
|
||||
modifyUrl(url, parsed => _.merge(parsed, props))
|
||||
);
|
||||
|
||||
/**
|
||||
* Unescape a url template that was escaped by encodeURI() so leaflet
|
||||
* will be able to correctly locate the varables in the template
|
||||
* @param {String} url
|
||||
* @return {String}
|
||||
*/
|
||||
const unescapeTemplateVars = url => {
|
||||
const ENCODED_TEMPLATE_VARS_RE = /%7B(\w+?)%7D/g;
|
||||
return url.replace(ENCODED_TEMPLATE_VARS_RE, (total, varName) => `{${varName}}`);
|
||||
};
|
||||
|
||||
|
||||
|
||||
class ServiceSettings {
|
||||
|
||||
constructor() {
|
||||
this._queryParams = {
|
||||
my_app_version: kbnVersion
|
||||
};
|
||||
|
||||
this._loadCatalogue = null;
|
||||
this._loadFileLayers = null;
|
||||
this._loadTMSServices = null;
|
||||
|
||||
this._invalidateSettings();
|
||||
}
|
||||
_invalidateSettings() {
|
||||
|
||||
this._loadCatalogue = _.once(async() => {
|
||||
try {
|
||||
const response = await this._getManifest(mapConfig.manifestServiceUrl, this._queryParams);
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
if (!e) {
|
||||
e = new Error('Unkown error');
|
||||
}
|
||||
if (!(e instanceof Error)) {
|
||||
e = new Error(e.data || `status ${e.statusText || e.status}`);
|
||||
}
|
||||
throw new Error(`Could not retrieve manifest from the tile service: ${e.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
this._loadFileLayers = _.once(async() => {
|
||||
const catalogue = await this._loadCatalogue();
|
||||
const fileService = catalogue.services.filter((service) => service.type === 'file')[0];
|
||||
const manifest = await this._getManifest(fileService.manifest, this._queryParams);
|
||||
const layers = manifest.data.layers.filter(layer => layer.format === 'geojson');
|
||||
layers.forEach((layer) => {
|
||||
layer.url = this._extendUrlWithParams(layer.url);
|
||||
});
|
||||
return layers;
|
||||
});
|
||||
|
||||
this._loadTMSServices = _.once(async() => {
|
||||
|
||||
if (tilemapsConfig.deprecated.isOverridden) {//use settings from yml (which are overridden)
|
||||
const tmsService = _.cloneDeep(tmsOptionsFromConfig);
|
||||
tmsService.url = tilemapsConfig.deprecated.config.url;
|
||||
return tmsService;
|
||||
}
|
||||
|
||||
const catalogue = await this._loadCatalogue();
|
||||
const tmsService = catalogue.services.filter((service) => service.type === 'tms')[0];
|
||||
const manifest = await this._getManifest(tmsService.manifest, this._queryParams);
|
||||
const services = manifest.data.services;
|
||||
|
||||
const firstService = _.cloneDeep(services[0]);
|
||||
if (!firstService) {
|
||||
throw new Error('Manifest response does not include sufficient service data.');
|
||||
}
|
||||
|
||||
|
||||
firstService.attribution = $sanitize(marked(firstService.attribution));
|
||||
firstService.subdomains = firstService.subdomains || [];
|
||||
firstService.url = this._extendUrlWithParams(firstService.url);
|
||||
return firstService;
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
_extendUrlWithParams(url) {
|
||||
return unescapeTemplateVars(extendUrl(url, {
|
||||
query: this._queryParams
|
||||
}));
|
||||
}
|
||||
|
||||
async _getManifest(manifestUrl) {
|
||||
return $http({
|
||||
url: extendUrl(manifestUrl, { query: this._queryParams }),
|
||||
method: 'GET'
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async getFileLayers() {
|
||||
return await this._loadFileLayers();
|
||||
}
|
||||
|
||||
async getTMSService() {
|
||||
|
||||
const tmsService = await this._loadTMSServices();
|
||||
|
||||
return {
|
||||
getUrl: function () {
|
||||
return tmsService.url;
|
||||
},
|
||||
getMinMaxZoom: (isWMSEnabled) => {
|
||||
if (isWMSEnabled) {
|
||||
return {
|
||||
minZoom: 0,
|
||||
maxZoom: 18
|
||||
};
|
||||
}
|
||||
//Otherwise, we use the settings from the yml.
|
||||
//note that it is no longer possible to only override the zoom-settings, since all options are read from the manifest
|
||||
//by default.
|
||||
//For a custom configuration, users will need to override tilemap.url as well.
|
||||
return {
|
||||
minZoom: tmsService.minZoom,
|
||||
maxZoom: tmsService.maxZoom
|
||||
};
|
||||
},
|
||||
getTMSOptions: function () {
|
||||
return tmsService;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
getFallbackZoomSettings(isWMSEnabled) {
|
||||
return (isWMSEnabled) ? { minZoom: 0, maxZoom: 18 } : { minZoom: 0, maxZoom: 10 };
|
||||
}
|
||||
|
||||
/**
|
||||
* Add optional query-parameters to all requests
|
||||
*
|
||||
* @param additionalQueryParams
|
||||
*/
|
||||
addQueryParams(additionalQueryParams) {
|
||||
for (const key in additionalQueryParams) {
|
||||
if (additionalQueryParams.hasOwnProperty(key)) {
|
||||
if (additionalQueryParams[key] !== this._queryParams[key]) {
|
||||
//changes detected.
|
||||
this._queryParams = _.assign({}, this._queryParams, additionalQueryParams);
|
||||
this._invalidateSettings();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new ServiceSettings();
|
||||
});
|
|
@ -1,208 +0,0 @@
|
|||
import { uiModules } from 'ui/modules';
|
||||
import _ from 'lodash';
|
||||
import marked from 'marked';
|
||||
import { modifyUrl } from 'ui/url';
|
||||
|
||||
marked.setOptions({
|
||||
gfm: true, // Github-flavored markdown
|
||||
sanitize: true // Sanitize HTML tags
|
||||
});
|
||||
|
||||
uiModules.get('kibana')
|
||||
.service('tilemapSettings', function ($http, tilemapsConfig, $sanitize, kbnVersion) {
|
||||
const attributionFromConfig = $sanitize(marked(tilemapsConfig.deprecated.config.options.attribution || ''));
|
||||
const optionsFromConfig = _.assign({}, tilemapsConfig.deprecated.config.options, { attribution: attributionFromConfig });
|
||||
const extendUrl = (url, props) => (
|
||||
modifyUrl(url, parsed => _.merge(parsed, props))
|
||||
);
|
||||
|
||||
/**
|
||||
* Unescape a url template that was escaped by encodeURI() so leaflet
|
||||
* will be able to correctly locate the varables in the template
|
||||
* @param {String} url
|
||||
* @return {String}
|
||||
*/
|
||||
const unescapeTemplateVars = url => {
|
||||
const ENCODED_TEMPLATE_VARS_RE = /%7B(\w+?)%7D/g;
|
||||
return url.replace(ENCODED_TEMPLATE_VARS_RE, (total, varName) => `{${varName}}`);
|
||||
};
|
||||
|
||||
class TilemapSettings {
|
||||
|
||||
constructor() {
|
||||
|
||||
this._queryParams = {
|
||||
my_app_version: kbnVersion
|
||||
};
|
||||
this._error = null;
|
||||
|
||||
//initialize settings with the default of the configuration
|
||||
this._url = tilemapsConfig.deprecated.config.url;
|
||||
this._tmsOptions = optionsFromConfig;
|
||||
|
||||
this._invalidateSettings();
|
||||
|
||||
}
|
||||
|
||||
|
||||
_invalidateSettings() {
|
||||
|
||||
this._settingsInitialized = false;
|
||||
this._loadSettings = _.once(async() => {
|
||||
|
||||
if (tilemapsConfig.deprecated.isOverridden) {//if settings are overridden, we will use those.
|
||||
this._settingsInitialized = true;
|
||||
}
|
||||
|
||||
if (this._settingsInitialized) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return this._getTileServiceManifest(tilemapsConfig.manifestServiceUrl, this._queryParams)
|
||||
.then(response => {
|
||||
const service = _.get(response, 'data.services[0]');
|
||||
if (!service) {
|
||||
throw new Error('Manifest response does not include sufficient service data.');
|
||||
}
|
||||
|
||||
this._error = null;
|
||||
this._tmsOptions = {
|
||||
attribution: $sanitize(marked(service.attribution)),
|
||||
minZoom: service.minZoom,
|
||||
maxZoom: service.maxZoom,
|
||||
subdomains: service.subdomains || []
|
||||
};
|
||||
|
||||
this._url = unescapeTemplateVars(extendUrl(service.url, {
|
||||
query: this._queryParams
|
||||
}));
|
||||
|
||||
this._settingsInitialized = true;
|
||||
})
|
||||
.catch(e => {
|
||||
this._settingsInitialized = true;
|
||||
|
||||
if (!e) {
|
||||
e = new Error('Unkown error');
|
||||
}
|
||||
|
||||
if (!(e instanceof Error)) {
|
||||
e = new Error(e.data || `status ${e.statusText || e.status}`);
|
||||
}
|
||||
|
||||
this._error = new Error(`Could not retrieve manifest from the tile service: ${e.message}`);
|
||||
})
|
||||
.then(() => {
|
||||
return true;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Must be called before getUrl/getTMSOptions/getMapOptions can be called.
|
||||
*/
|
||||
loadSettings() {
|
||||
return this._loadSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add optional query-parameters for the request.
|
||||
* These are only applied when requesting dfrom the manifest.
|
||||
*
|
||||
* @param additionalQueryParams
|
||||
*/
|
||||
addQueryParams(additionalQueryParams) {
|
||||
|
||||
//check if there are any changes in the settings.
|
||||
let changes = false;
|
||||
for (const key in additionalQueryParams) {
|
||||
if (additionalQueryParams.hasOwnProperty(key)) {
|
||||
if (additionalQueryParams[key] !== this._queryParams[key]) {
|
||||
changes = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (changes) {
|
||||
this._queryParams = _.assign({}, this._queryParams, additionalQueryParams);
|
||||
this._invalidateSettings();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the url of the default TMS
|
||||
* @return {string}
|
||||
*/
|
||||
getUrl() {
|
||||
if (!this._settingsInitialized) {
|
||||
throw new Error('Cannot retrieve url before calling .loadSettings first');
|
||||
}
|
||||
return this._url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the options of the default TMS
|
||||
* @return {{}}
|
||||
*/
|
||||
getTMSOptions() {
|
||||
if (!this._settingsInitialized) {
|
||||
throw new Error('Cannot retrieve options before calling .loadSettings first');
|
||||
}
|
||||
return this._tmsOptions;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return {{maxZoom: (*|number), minZoom: (*|number)}}
|
||||
*/
|
||||
getMinMaxZoom(isWMSEnabled) {
|
||||
if (isWMSEnabled) {
|
||||
return {
|
||||
minZoom: 0,
|
||||
maxZoom: 18
|
||||
};
|
||||
}
|
||||
|
||||
//Otherwise, we use the settings from the yml.
|
||||
//note that it is no longer possible to only override the zoom-settings, since all options are read from the manifest
|
||||
//by default.
|
||||
//For a custom configuration, users will need to override tilemap.url as well.
|
||||
return {
|
||||
minZoom: this._tmsOptions.minZoom,
|
||||
maxZoom: this._tmsOptions.maxZoom
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
isInitialized() {
|
||||
return this._settingsInitialized;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks if there was an error during initialization of the parameters
|
||||
*/
|
||||
hasError() {
|
||||
return this._error !== null;
|
||||
}
|
||||
|
||||
getError() {
|
||||
return this._error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make this a method to allow for overrides by test code
|
||||
*/
|
||||
_getTileServiceManifest(manifestUrl) {
|
||||
return $http({
|
||||
url: extendUrl(manifestUrl, { query: this._queryParams }),
|
||||
method: 'GET'
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return new TilemapSettings();
|
||||
});
|
|
@ -5,17 +5,17 @@ import { VislibVisTypeBuildChartDataProvider } from 'ui/vislib_vis_type/build_ch
|
|||
import { FilterBarPushFilterProvider } from 'ui/filter_bar/push_filter';
|
||||
import { KibanaMap } from './kibana_map';
|
||||
import { GeohashLayer } from './geohash_layer';
|
||||
import './lib/tilemap_settings';
|
||||
import './lib/service_settings';
|
||||
import './styles/_tilemap.less';
|
||||
import { ResizeCheckerProvider } from 'ui/resize_checker';
|
||||
|
||||
|
||||
module.exports = function MapsRenderbotFactory(Private, $injector, tilemapSettings, Notifier, courier, getAppState) {
|
||||
module.exports = function MapsRenderbotFactory(Private, $injector, serviceSettings, Notifier, courier, getAppState) {
|
||||
|
||||
const ResizeChecker = Private(ResizeCheckerProvider);
|
||||
const Renderbot = Private(VisRenderbotProvider);
|
||||
const buildChartData = Private(VislibVisTypeBuildChartDataProvider);
|
||||
const notify = new Notifier({ location: 'Tilemap' });
|
||||
const notify = new Notifier({ location: 'Coordinate Map' });
|
||||
|
||||
class MapsRenderbot extends Renderbot {
|
||||
|
||||
|
@ -26,12 +26,9 @@ module.exports = function MapsRenderbotFactory(Private, $injector, tilemapSettin
|
|||
this._kibanaMap = null;
|
||||
this._$container = $el;
|
||||
this._kibanaMapReady = this._makeKibanaMap($el);
|
||||
|
||||
this._baseLayerDirty = true;
|
||||
this._dataDirty = true;
|
||||
this._paramsDirty = true;
|
||||
|
||||
|
||||
this._resizeChecker = new ResizeChecker($el);
|
||||
this._resizeChecker.on('resize', () => {
|
||||
if (this._kibanaMap) {
|
||||
|
@ -42,19 +39,19 @@ module.exports = function MapsRenderbotFactory(Private, $injector, tilemapSettin
|
|||
|
||||
async _makeKibanaMap() {
|
||||
|
||||
if (!tilemapSettings.isInitialized()) {
|
||||
await tilemapSettings.loadSettings();
|
||||
}
|
||||
|
||||
if (tilemapSettings.getError()) {
|
||||
//Still allow the visualization to be built, but show a toast that there was a problem retrieving map settings
|
||||
//Even though the basemap will not display, the user will at least still see the overlay data
|
||||
notify.warning(tilemapSettings.getError().message);
|
||||
try {
|
||||
this._tmsService = await serviceSettings.getTMSService();
|
||||
this._tmsError = null;
|
||||
} catch (e) {
|
||||
this._tmsService = null;
|
||||
this._tmsError = e;
|
||||
notify.warning(e.message);
|
||||
}
|
||||
|
||||
if (this._kibanaMap) {
|
||||
this._kibanaMap.destroy();
|
||||
}
|
||||
|
||||
const containerElement = $(this._$container)[0];
|
||||
const options = _.clone(this._getMinMaxZoom());
|
||||
const uiState = this.vis.getUiState();
|
||||
|
@ -107,7 +104,11 @@ module.exports = function MapsRenderbotFactory(Private, $injector, tilemapSettin
|
|||
|
||||
_getMinMaxZoom() {
|
||||
const mapParams = this._getMapsParams();
|
||||
return tilemapSettings.getMinMaxZoom(mapParams.wms.enabled);
|
||||
if (this._tmsError) {
|
||||
return serviceSettings.getFallbackZoomSettings(mapParams.wms.enabled);
|
||||
} else {
|
||||
return this._tmsService.getMinMaxZoom(mapParams.wms.enabled);
|
||||
}
|
||||
}
|
||||
|
||||
_recreateGeohashLayer() {
|
||||
|
@ -180,9 +181,9 @@ module.exports = function MapsRenderbotFactory(Private, $injector, tilemapSettin
|
|||
this._kibanaMap.setZoomLevel(maxZoom);
|
||||
}
|
||||
|
||||
if (!tilemapSettings.hasError()) {
|
||||
const url = tilemapSettings.getUrl();
|
||||
const options = tilemapSettings.getTMSOptions();
|
||||
if (!this._tmsError) {
|
||||
const url = this._tmsService.getUrl();
|
||||
const options = this._tmsService.getTMSOptions();
|
||||
this._kibanaMap.setBaseLayer({
|
||||
baseLayerType: 'tms',
|
||||
options: { url, ...options }
|
||||
|
|
11
src/ui/public/vislib/components/color/truncated_colormaps.js
Normal file
11
src/ui/public/vislib/components/color/truncated_colormaps.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { vislibColorMaps } from './colormaps';
|
||||
|
||||
export const truncatedColorMaps = {};
|
||||
|
||||
const colormaps = vislibColorMaps;
|
||||
for (const key in colormaps) {
|
||||
if (colormaps.hasOwnProperty(key)) {
|
||||
//slice off lightest colors
|
||||
truncatedColorMaps[key] = colormaps[key].slice(Math.floor(colormaps[key].length / 4));
|
||||
}
|
||||
}
|
|
@ -133,7 +133,11 @@ export function getDefaultSettings() {
|
|||
}
|
||||
}, null, 2),
|
||||
type: 'json',
|
||||
description: 'Default <a href="http://leafletjs.com/reference.html#tilelayer-wms" target="_blank">properties</a> for the WMS map server support in the tile map'
|
||||
description: 'Default <a href="http://leafletjs.com/reference.html#tilelayer-wms" target="_blank">properties</a> for the WMS map server support in the coordinate map'
|
||||
},
|
||||
'visualization:regionmap:showWarnings': {
|
||||
value: true,
|
||||
description: 'Should the vector map show a warning when terms cannot be joined to a shape on the map.'
|
||||
},
|
||||
'visualization:colorMapping': {
|
||||
type: 'json',
|
||||
|
|
|
@ -12,6 +12,7 @@ export default function ({ getService, getPageObjects }) {
|
|||
return PageObjects.common.navigateToUrl('visualize', 'new');
|
||||
});
|
||||
|
||||
|
||||
describe('chart types', function indexPatternCreation() {
|
||||
it('should show the correct chart types', function () {
|
||||
const expectedChartTypes = [
|
||||
|
@ -25,7 +26,8 @@ export default function ({ getService, getPageObjects }) {
|
|||
'Gauge',
|
||||
'Goal',
|
||||
'Metric',
|
||||
'Tile Map',
|
||||
'Coordinate Map',
|
||||
'Region Map',
|
||||
'Timelion',
|
||||
'Visual Builder',
|
||||
'Markdown',
|
||||
|
|
95
test/functional/apps/visualize/_region_map.js
Normal file
95
test/functional/apps/visualize/_region_map.js
Normal file
|
@ -0,0 +1,95 @@
|
|||
import expect from 'expect.js';
|
||||
|
||||
|
||||
export default function ({ getService, getPageObjects }) {
|
||||
|
||||
|
||||
describe('visualize app', function describeIndexTests() {
|
||||
|
||||
const fromTime = '2015-09-19 06:31:44.000';
|
||||
const toTime = '2015-09-23 18:31:44.000';
|
||||
|
||||
const log = getService('log');
|
||||
const PageObjects = getPageObjects(['common', 'visualize', 'header', 'settings']);
|
||||
|
||||
before(function () {
|
||||
|
||||
log.debug('navigateToApp visualize');
|
||||
return PageObjects.common.navigateToUrl('visualize', 'new')
|
||||
.then(function () {
|
||||
log.debug('clickRegionMap');
|
||||
return PageObjects.visualize.clickRegionMap();
|
||||
})
|
||||
.then(function () {
|
||||
return PageObjects.visualize.clickNewSearch();
|
||||
})
|
||||
.then(function () {
|
||||
log.debug('Set absolute time range from \"' + fromTime + '\" to \"' + toTime + '\"');
|
||||
return PageObjects.header.setAbsoluteRange(fromTime, toTime);
|
||||
})
|
||||
.then(function clickBucket() {
|
||||
log.debug('Bucket = shape field');
|
||||
return PageObjects.visualize.clickBucket('shape field');
|
||||
})
|
||||
.then(function selectAggregation() {
|
||||
log.debug('Aggregation = Terms');
|
||||
return PageObjects.visualize.selectAggregation('Terms');
|
||||
})
|
||||
.then(function selectField() {
|
||||
log.debug('Field = geo.src');
|
||||
return PageObjects.visualize.selectField('geo.src');
|
||||
})
|
||||
.then(function () {
|
||||
return PageObjects.visualize.clickGo();
|
||||
})
|
||||
.then(function () {
|
||||
return PageObjects.header.waitUntilLoadingHasFinished();
|
||||
});
|
||||
});
|
||||
|
||||
describe('vector map', function indexPatternCreation() {
|
||||
|
||||
it('should show results after clicking play (join on states)', function () {
|
||||
|
||||
const expectedColors = [{ color: 'rgb(253,209,109)' }, { color: 'rgb(164,0,37)' }];
|
||||
|
||||
|
||||
return PageObjects.visualize.getVectorMapData()
|
||||
.then(function (data) {
|
||||
|
||||
log.debug('Actual data-----------------------');
|
||||
log.debug(data);
|
||||
log.debug('---------------------------------');
|
||||
|
||||
expect(data).to.eql(expectedColors);
|
||||
});
|
||||
});
|
||||
|
||||
it('should change color ramp', function () {
|
||||
return PageObjects.visualize.clickOptions()
|
||||
.then(function () {
|
||||
return PageObjects.visualize.selectFieldById('Blues', 'colorSchema');
|
||||
})
|
||||
.then(function () {
|
||||
return PageObjects.visualize.clickGo();
|
||||
})
|
||||
.then(function () {
|
||||
//this should visualize right away, without re-requesting data
|
||||
return PageObjects.visualize.getVectorMapData();
|
||||
})
|
||||
.then(function (data) {
|
||||
|
||||
log.debug('Actual data-----------------------');
|
||||
log.debug(data);
|
||||
log.debug('---------------------------------');
|
||||
|
||||
const expectedColors = [{ color: 'rgb(190,215,236)' }, { color: 'rgb(7,67,136)' }];
|
||||
|
||||
expect(data).to.eql(expectedColors);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
}
|
|
@ -31,6 +31,7 @@ export default function ({ getService, loadTestFile }) {
|
|||
loadTestFile(require.resolve('./_pie_chart'));
|
||||
loadTestFile(require.resolve('./_tag_cloud'));
|
||||
loadTestFile(require.resolve('./_tile_map'));
|
||||
loadTestFile(require.resolve('./_region_map'));
|
||||
loadTestFile(require.resolve('./_vertical_bar_chart'));
|
||||
loadTestFile(require.resolve('./_heatmap_chart'));
|
||||
loadTestFile(require.resolve('./_point_series_options'));
|
||||
|
|
52
test/functional/index.js
Normal file
52
test/functional/index.js
Normal file
|
@ -0,0 +1,52 @@
|
|||
'use strict'; // eslint-disable-line
|
||||
|
||||
define(function (require) {
|
||||
require('intern/dojo/node!../support/env_setup');
|
||||
|
||||
const bdd = require('intern!bdd');
|
||||
const intern = require('intern');
|
||||
|
||||
global.__kibana__intern__ = { intern, bdd };
|
||||
|
||||
bdd.describe('kibana', function () {
|
||||
let PageObjects;
|
||||
let support;
|
||||
|
||||
bdd.before(function () {
|
||||
PageObjects.init(this.remote);
|
||||
support.init(this.remote);
|
||||
});
|
||||
const supportPages = [
|
||||
'intern/dojo/node!../support/page_objects',
|
||||
'intern/dojo/node!../support'
|
||||
];
|
||||
|
||||
const requestedApps = process.argv.reduce((previous, arg) => {
|
||||
const option = arg.split('=');
|
||||
const key = option[0];
|
||||
const value = option[1];
|
||||
if (key === 'appSuites' && value) return value.split(',');
|
||||
});
|
||||
|
||||
const apps = [
|
||||
'intern/dojo/node!./apps/xpack',
|
||||
'intern/dojo/node!./apps/discover',
|
||||
'intern/dojo/node!./apps/management',
|
||||
'intern/dojo/node!./apps/visualize',
|
||||
'intern/dojo/node!./apps/console',
|
||||
'intern/dojo/node!./apps/dashboard',
|
||||
'intern/dojo/node!./status_page',
|
||||
'intern/dojo/node!./apps/context'
|
||||
].filter((suite) => {
|
||||
if (!requestedApps) return true;
|
||||
return requestedApps.reduce((previous, app) => {
|
||||
return previous || ~suite.indexOf(app);
|
||||
}, false);
|
||||
});
|
||||
|
||||
require(supportPages.concat(apps), (loadedPageObjects, loadedSupport) => {
|
||||
PageObjects = loadedPageObjects;
|
||||
support = loadedSupport;
|
||||
});
|
||||
});
|
||||
});
|
|
@ -29,6 +29,44 @@ export function VisualizePageProvider({ getService, getPageObjects }) {
|
|||
.click();
|
||||
}
|
||||
|
||||
|
||||
clickRegionMap() {
|
||||
return remote
|
||||
.setFindTimeout(defaultFindTimeout)
|
||||
.findByPartialLinkText('Region Map')
|
||||
.click();
|
||||
}
|
||||
|
||||
getVectorMapData() {
|
||||
return remote
|
||||
.setFindTimeout(defaultFindTimeout)
|
||||
.findAllByCssSelector('path.leaflet-clickable')
|
||||
.then((chartTypes) => {
|
||||
|
||||
|
||||
function getChartType(chart) {
|
||||
let color;
|
||||
return chart.getAttribute('fill')
|
||||
.then((stroke) => {
|
||||
color = stroke;
|
||||
})
|
||||
.then(() => {
|
||||
return { color: color };
|
||||
});
|
||||
}
|
||||
|
||||
const getChartTypesPromises = chartTypes.map(getChartType);
|
||||
return Promise.all(getChartTypesPromises);
|
||||
})
|
||||
.then((data) => {
|
||||
data = data.filter((country) => {
|
||||
//filter empty colors
|
||||
return country.color !== 'rgb(200,200,200)';
|
||||
});
|
||||
return data;
|
||||
});
|
||||
}
|
||||
|
||||
clickMarkdownWidget() {
|
||||
return remote
|
||||
.setFindTimeout(defaultFindTimeout)
|
||||
|
@ -67,7 +105,7 @@ export function VisualizePageProvider({ getService, getPageObjects }) {
|
|||
clickTileMap() {
|
||||
return remote
|
||||
.setFindTimeout(defaultFindTimeout)
|
||||
.findByPartialLinkText('Tile Map')
|
||||
.findByPartialLinkText('Coordinate Map')
|
||||
.click();
|
||||
}
|
||||
|
||||
|
@ -288,6 +326,16 @@ export function VisualizePageProvider({ getService, getPageObjects }) {
|
|||
});
|
||||
}
|
||||
|
||||
selectFieldById(fieldValue, id) {
|
||||
return retry.try(function tryingForTime() {
|
||||
return remote
|
||||
.setFindTimeout(defaultFindTimeout)
|
||||
// the css below should be more selective
|
||||
.findByCssSelector(`#${id} > option[label="${fieldValue}"]`)
|
||||
.click();
|
||||
});
|
||||
}
|
||||
|
||||
orderBy(fieldValue) {
|
||||
return remote
|
||||
.setFindTimeout(defaultFindTimeout)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue