mirror of
https://github.com/elastic/kibana.git
synced 2025-04-25 02:09:32 -04:00
Geocentroid / tilemap bug fixes (#10871)
- adds the geo-centroid metric as the new default for visualizations - various bug fixes and improvements - avoid unnecessary calls to manifest - avoid map flicker when zooming - enable scroll/pinch/touch zooming - avoid heatmap errors - ensure map fills screen in dashboard - ensure fit works consistently - relax tilemap constraints - remove support for multi-maps - this refactor sets the stage for new vector map visualization which will reuse the same map components
This commit is contained in:
parent
d41042a507
commit
556bfab85d
51 changed files with 3510 additions and 3158 deletions
|
@ -56,7 +56,7 @@
|
|||
<div>
|
||||
<label>
|
||||
Maximum zoom
|
||||
<kbn-info placement="right" info="Map zoom at which all dots are displayed at full intensity. Default: 16"></kbn-info>
|
||||
<kbn-info placement="right" info="Map zoom at which all dots are displayed at full intensity. Default: 0"></kbn-info>
|
||||
</label>
|
||||
<div class="vis-editor-agg-form-row">
|
||||
<input
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
import _ from 'lodash';
|
||||
import supports from 'ui/utils/supports';
|
||||
import MapsVisTypeVislibVisTypeProvider from 'ui/vis_maps/maps_vis_type';
|
||||
import VisSchemasProvider from 'ui/vis/schemas';
|
||||
import AggResponseGeoJsonGeoJsonProvider from 'ui/agg_response/geo_json/geo_json';
|
||||
import FilterBarPushFilterProvider from 'ui/filter_bar/push_filter';
|
||||
import tileMapTemplate from 'plugins/kbn_vislib_vis_types/editors/tile_map.html';
|
||||
|
||||
export default function TileMapVisType(Private, getAppState, courier, config) {
|
||||
|
@ -22,7 +20,7 @@ export default function TileMapVisType(Private, getAppState, courier, config) {
|
|||
mapType: 'Scaled Circle Markers',
|
||||
isDesaturated: true,
|
||||
addTooltip: true,
|
||||
heatMaxZoom: 16,
|
||||
heatMaxZoom: 0,
|
||||
heatMinOpacity: 0.1,
|
||||
heatRadius: 25,
|
||||
heatBlur: 15,
|
||||
|
@ -45,41 +43,14 @@ export default function TileMapVisType(Private, getAppState, courier, config) {
|
|||
value: 'topright',
|
||||
text: 'top right',
|
||||
}],
|
||||
mapTypes: ['Scaled Circle Markers', 'Shaded Circle Markers', 'Shaded Geohash Grid', 'Heatmap'],
|
||||
mapTypes: ['Scaled Circle Markers',
|
||||
'Shaded Circle Markers',
|
||||
'Shaded Geohash Grid',
|
||||
'Heatmap'
|
||||
],
|
||||
canDesaturate: !!supports.cssFilters,
|
||||
editor: tileMapTemplate
|
||||
},
|
||||
listeners: {
|
||||
rectangle: function (event) {
|
||||
const agg = _.get(event, 'chart.geohashGridAgg');
|
||||
if (!agg) return;
|
||||
|
||||
const pushFilter = Private(FilterBarPushFilterProvider)(getAppState());
|
||||
const indexPatternName = agg.vis.indexPattern.id;
|
||||
const field = agg.fieldName();
|
||||
const filter = { geo_bounding_box: {} };
|
||||
filter.geo_bounding_box[field] = event.bounds;
|
||||
|
||||
pushFilter(filter, false, indexPatternName);
|
||||
},
|
||||
mapMoveEnd: function (event) {
|
||||
const vis = _.get(event, 'chart.geohashGridAgg.vis');
|
||||
if (vis && vis.hasUiState()) {
|
||||
vis.getUiState().set('mapCenter', event.center);
|
||||
}
|
||||
},
|
||||
mapZoomEnd: function (event) {
|
||||
const vis = _.get(event, 'chart.geohashGridAgg.vis');
|
||||
if (vis && vis.hasUiState()) {
|
||||
vis.getUiState().set('mapZoom', event.zoom);
|
||||
}
|
||||
|
||||
const autoPrecision = _.get(event, 'chart.geohashGridAgg.params.autoPrecision');
|
||||
if (autoPrecision) {
|
||||
courier.fetch();
|
||||
}
|
||||
}
|
||||
},
|
||||
responseConverter: geoJsonConverter,
|
||||
implementsRenderComplete: true,
|
||||
schemas: new Schemas([
|
||||
|
@ -101,15 +72,6 @@ export default function TileMapVisType(Private, getAppState, courier, config) {
|
|||
aggFilter: 'geohash_grid',
|
||||
min: 1,
|
||||
max: 1
|
||||
},
|
||||
{
|
||||
group: 'buckets',
|
||||
name: 'split',
|
||||
title: 'Split Chart',
|
||||
deprecate: true,
|
||||
deprecateMessage: 'The Split Chart feature for Tile Maps has been deprecated.',
|
||||
min: 0,
|
||||
max: 1
|
||||
}
|
||||
])
|
||||
});
|
||||
|
|
|
@ -62,7 +62,7 @@ module.exports = function (kibana) {
|
|||
config: tilemapConfig,
|
||||
},
|
||||
manifestServiceUrl: serverConfig.get('tilemap.manifestServiceUrl')
|
||||
},
|
||||
}
|
||||
};
|
||||
},
|
||||
},
|
||||
|
|
|
@ -46,6 +46,13 @@
|
|||
vis-editor-agg-group {
|
||||
.flex-parent(0, 1, auto);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
.indented {
|
||||
margin-left: 2em;
|
||||
}
|
||||
|
||||
.vis-editor-content {
|
||||
|
|
|
@ -143,7 +143,6 @@ module.exports = () => Joi.object({
|
|||
}),
|
||||
profile: Joi.boolean().default(false)
|
||||
}).default(),
|
||||
|
||||
status: Joi.object({
|
||||
allowAnonymous: Joi.boolean().default(false)
|
||||
}).default(),
|
||||
|
@ -156,7 +155,7 @@ module.exports = () => Joi.object({
|
|||
url: Joi.string(),
|
||||
options: Joi.object({
|
||||
attribution: Joi.string(),
|
||||
minZoom: Joi.number().min(1, 'Must not be less than 1').default(1),
|
||||
minZoom: Joi.number().min(0, 'Must be 0 or higher').default(0),
|
||||
maxZoom: Joi.number().default(10),
|
||||
tileSize: Joi.number(),
|
||||
subdomains: Joi.array().items(Joi.string()).single(),
|
||||
|
|
|
@ -13,7 +13,7 @@ describe('GeoJson Agg Response Converter', function () {
|
|||
let tabify;
|
||||
let convert;
|
||||
let esResponse;
|
||||
let aggs;
|
||||
let expectedAggs;
|
||||
|
||||
beforeEach(ngMock.module('kibana'));
|
||||
beforeEach(ngMock.inject(function (Private) {
|
||||
|
@ -28,8 +28,7 @@ describe('GeoJson Agg Response Converter', function () {
|
|||
type: 'tile_map',
|
||||
aggs: [
|
||||
{ schema: 'metric', type: 'avg', params: { field: 'bytes' } },
|
||||
{ schema: 'split', type: 'terms', params: { field: '@tags' } },
|
||||
{ schema: 'segment', type: 'geohash_grid', params: { field: 'geo.coordinates', precision: 3 } }
|
||||
{ schema: 'segment', type: 'geohash_grid', params: { field: 'geo.coordinates', precision: 3, useGeocentroid: false } }
|
||||
],
|
||||
params: {
|
||||
isDesaturated: true,
|
||||
|
@ -37,17 +36,16 @@ describe('GeoJson Agg Response Converter', function () {
|
|||
}
|
||||
});
|
||||
|
||||
aggs = {
|
||||
expectedAggs = {
|
||||
metric: vis.aggs[0],
|
||||
split: vis.aggs[1],
|
||||
geo: vis.aggs[2]
|
||||
geo: vis.aggs[1]
|
||||
};
|
||||
}));
|
||||
|
||||
[ { asAggConfigResults: true }, { asAggConfigResults: false } ].forEach(function (tableOpts) {
|
||||
|
||||
function makeTable() {
|
||||
return _.sample(_.sample(tabify(vis, esResponse, tableOpts).tables).tables);
|
||||
return _.sample(tabify(vis, esResponse, tableOpts).tables);
|
||||
}
|
||||
|
||||
function makeSingleChart(table) {
|
||||
|
@ -72,8 +70,8 @@ describe('GeoJson Agg Response Converter', function () {
|
|||
|
||||
expect(chart.title).to.be(table.title());
|
||||
expect(chart.tooltipFormatter).to.be.a('function');
|
||||
expect(chart.valueFormatter).to.be(aggs.metric.fieldFormatter());
|
||||
expect(chart.geohashGridAgg).to.be(aggs.geo);
|
||||
expect(chart.valueFormatter).to.be(expectedAggs.metric.fieldFormatter());
|
||||
expect(chart.geohashGridAgg).to.be(expectedAggs.geo);
|
||||
expect(chart.geoJson).to.be.an('object');
|
||||
});
|
||||
|
||||
|
@ -116,8 +114,8 @@ describe('GeoJson Agg Response Converter', function () {
|
|||
before(function () {
|
||||
table = makeTable();
|
||||
chart = makeSingleChart(table);
|
||||
geoColI = _.findIndex(table.columns, { aggConfig: aggs.geo });
|
||||
metricColI = _.findIndex(table.columns, { aggConfig: aggs.metric });
|
||||
geoColI = _.findIndex(table.columns, { aggConfig: expectedAggs.geo });
|
||||
metricColI = _.findIndex(table.columns, { aggConfig: expectedAggs.metric });
|
||||
});
|
||||
|
||||
it('should be geoJson format', function () {
|
||||
|
|
|
@ -15,10 +15,12 @@ export default function TileMapConverterFn(Private) {
|
|||
|
||||
const geoI = columnIndex('segment');
|
||||
const metricI = columnIndex('metric');
|
||||
const centroidI = _.findIndex(table.columns, (col) => col.aggConfig.type.name === 'geo_centroid');
|
||||
|
||||
const geoAgg = _.get(table.columns, [geoI, 'aggConfig']);
|
||||
const metricAgg = _.get(table.columns, [metricI, 'aggConfig']);
|
||||
|
||||
const features = rowsToFeatures(table, geoI, metricI);
|
||||
const features = rowsToFeatures(table, geoI, metricI, centroidI);
|
||||
const values = features.map(function (feature) {
|
||||
return feature.properties.value;
|
||||
});
|
||||
|
|
|
@ -10,7 +10,8 @@ function unwrap(val) {
|
|||
return getAcr(val) ? val.value : val;
|
||||
}
|
||||
|
||||
function convertRowsToFeatures(table, geoI, metricI) {
|
||||
function convertRowsToFeatures(table, geoI, metricI, centroidI) {
|
||||
|
||||
return _.transform(table.rows, function (features, row) {
|
||||
const geohash = unwrap(row[geoI]);
|
||||
if (!geohash) return;
|
||||
|
@ -23,6 +24,16 @@ function convertRowsToFeatures(table, geoI, metricI) {
|
|||
location.longitude[2]
|
||||
];
|
||||
|
||||
//courtsey of @JacobBrandt: https://github.com/elastic/kibana/pull/9676/files#diff-c7c9f237e673ff486654f6cc6caa89f6
|
||||
let point = centerLatLng;
|
||||
const centroid = unwrap(row[centroidI]);
|
||||
if (centroid) {
|
||||
point = [
|
||||
centroid.lat,
|
||||
centroid.lon
|
||||
];
|
||||
}
|
||||
|
||||
// order is nw, ne, se, sw
|
||||
const rectangle = [
|
||||
[location.latitude[0], location.longitude[0]],
|
||||
|
@ -37,7 +48,7 @@ function convertRowsToFeatures(table, geoI, metricI) {
|
|||
type: 'Feature',
|
||||
geometry: {
|
||||
type: 'Point',
|
||||
coordinates: centerLatLng.slice(0).reverse()
|
||||
coordinates: point.slice(0).reverse()
|
||||
},
|
||||
properties: {
|
||||
geohash: geohash,
|
||||
|
|
|
@ -8,7 +8,7 @@ describe('Geohash Agg', function () {
|
|||
|
||||
const paramWriter = new AggTypesBucketsGeoHashProvider(function PrivateMock() {
|
||||
return function BucketMock(geohashProvider) {
|
||||
return geohashProvider.params[4];
|
||||
return geohashProvider.params[5];
|
||||
};
|
||||
}, {
|
||||
get: function () {
|
||||
|
|
|
@ -8,8 +8,6 @@ import AggTypesParamTypesStringProvider from 'ui/agg_types/param_types/string';
|
|||
import AggTypesParamTypesRawJsonProvider from 'ui/agg_types/param_types/raw_json';
|
||||
import AggTypesParamTypesBaseProvider from 'ui/agg_types/param_types/base';
|
||||
export default function AggParamsFactory(Private) {
|
||||
|
||||
|
||||
const paramTypeMap = {
|
||||
field: Private(AggTypesParamTypesFieldProvider),
|
||||
optioned: Private(AggTypesParamTypesOptionedProvider),
|
||||
|
|
|
@ -110,6 +110,17 @@ export default function AggTypeFactory(Private) {
|
|||
this.params = new AggParams(this.params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Designed for multi-value metric aggs, this method can return a
|
||||
* set of AggConfigs that should replace this aggConfig in requests
|
||||
*
|
||||
* @method getRequestAggs
|
||||
* @returns {array[AggConfig]|undefined} - an array of aggConfig objects
|
||||
* that should replace this one,
|
||||
* or undefined
|
||||
*/
|
||||
this.getRequestAggs = config.getRequestAggs || _.noop;
|
||||
|
||||
/**
|
||||
* Designed for multi-value metric aggs, this method can return a
|
||||
* set of AggConfigs that should replace this aggConfig in result sets
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
import _ from 'lodash';
|
||||
import AggTypesBucketsBucketAggTypeProvider from 'ui/agg_types/buckets/_bucket_agg_type';
|
||||
import VisAggConfigProvider from 'ui/vis/agg_config';
|
||||
import precisionTemplate from 'ui/agg_types/controls/precision.html';
|
||||
import { geohashColumns } from 'ui/utils/decode_geo_hash';
|
||||
|
||||
export default function GeoHashAggDefinition(Private, config) {
|
||||
const BucketAggType = Private(AggTypesBucketsBucketAggTypeProvider);
|
||||
const AggConfig = Private(VisAggConfigProvider);
|
||||
|
||||
const defaultPrecision = 2;
|
||||
const maxPrecision = parseInt(config.get('visualization:tileMap:maxPrecision'), 10) || 12;
|
||||
/**
|
||||
|
@ -54,6 +57,11 @@ export default function GeoHashAggDefinition(Private, config) {
|
|||
default: true,
|
||||
write: _.noop
|
||||
},
|
||||
{
|
||||
name: 'useGeocentroid',
|
||||
default: true,
|
||||
write: _.noop
|
||||
},
|
||||
{
|
||||
name: 'mapZoom',
|
||||
write: _.noop
|
||||
|
@ -79,6 +87,23 @@ export default function GeoHashAggDefinition(Private, config) {
|
|||
output.params.precision = aggConfig.params.autoPrecision ? autoPrecisionVal : getPrecision(aggConfig.params.precision);
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
getRequestAggs: function (agg) {
|
||||
if (!agg.params.useGeocentroid) {
|
||||
return agg;
|
||||
}
|
||||
|
||||
/**
|
||||
* By default, add the geo_centroid aggregation
|
||||
*/
|
||||
return [agg, new AggConfig(agg.vis, {
|
||||
type: 'geo_centroid',
|
||||
enabled:true,
|
||||
params: {
|
||||
field: agg.getField()
|
||||
},
|
||||
schema: 'metric'
|
||||
})];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="vis-option-item">
|
||||
<div class="vis-option-item indented">
|
||||
<label>
|
||||
<input type="checkbox"
|
||||
name="autoPrecision"
|
||||
|
@ -27,3 +27,12 @@
|
|||
</label>
|
||||
</div>
|
||||
|
||||
<div class="vis-option-item indented">
|
||||
<label>
|
||||
<input type="checkbox"
|
||||
name="useGeocentroid"
|
||||
ng-model="agg.params.useGeocentroid">
|
||||
Place markers off grid (use geocentroid)
|
||||
</label>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import AggTypesMetricsTopHitProvider from 'ui/agg_types/metrics/top_hit';
|
|||
import AggTypesMetricsStdDeviationProvider from 'ui/agg_types/metrics/std_deviation';
|
||||
import AggTypesMetricsCardinalityProvider from 'ui/agg_types/metrics/cardinality';
|
||||
import AggTypesMetricsPercentilesProvider from 'ui/agg_types/metrics/percentiles';
|
||||
import AggTypesMetricsGeoCentroidProvider from 'ui/agg_types/metrics/geo_centroid';
|
||||
import AggTypesMetricsPercentileRanksProvider from 'ui/agg_types/metrics/percentile_ranks';
|
||||
import AggTypesMetricsDerivativeProvider from 'ui/agg_types/metrics/derivative';
|
||||
import AggTypesMetricsCumulativeSumProvider from 'ui/agg_types/metrics/cumulative_sum';
|
||||
|
@ -28,6 +29,8 @@ import AggTypesMetricsBucketSumProvider from 'ui/agg_types/metrics/bucket_sum';
|
|||
import AggTypesMetricsBucketAvgProvider from 'ui/agg_types/metrics/bucket_avg';
|
||||
import AggTypesMetricsBucketMinProvider from 'ui/agg_types/metrics/bucket_min';
|
||||
import AggTypesMetricsBucketMaxProvider from 'ui/agg_types/metrics/bucket_max';
|
||||
|
||||
|
||||
export default function AggTypeService(Private) {
|
||||
|
||||
const aggs = {
|
||||
|
@ -51,6 +54,7 @@ export default function AggTypeService(Private) {
|
|||
Private(AggTypesMetricsBucketSumProvider),
|
||||
Private(AggTypesMetricsBucketMinProvider),
|
||||
Private(AggTypesMetricsBucketMaxProvider),
|
||||
Private(AggTypesMetricsGeoCentroidProvider)
|
||||
],
|
||||
buckets: [
|
||||
Private(AggTypesBucketsDateHistogramProvider),
|
||||
|
@ -61,7 +65,7 @@ export default function AggTypeService(Private) {
|
|||
Private(AggTypesBucketsTermsProvider),
|
||||
Private(AggTypesBucketsFiltersProvider),
|
||||
Private(AggTypesBucketsSignificantTermsProvider),
|
||||
Private(AggTypesBucketsGeoHashProvider)
|
||||
Private(AggTypesBucketsGeoHashProvider),
|
||||
]
|
||||
};
|
||||
|
||||
|
|
22
src/ui/public/agg_types/metrics/geo_centroid.js
Normal file
22
src/ui/public/agg_types/metrics/geo_centroid.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
import AggTypesMetricsMetricAggTypeProvider from 'ui/agg_types/metrics/metric_agg_type';
|
||||
|
||||
export default function AggTypeMetricGeoCentroidProvider(Private) {
|
||||
const MetricAggType = Private(AggTypesMetricsMetricAggTypeProvider);
|
||||
|
||||
return new MetricAggType({
|
||||
name: 'geo_centroid',
|
||||
title: 'Geo Centroid',
|
||||
makeLabel: function () {
|
||||
return 'Geo Centroid';
|
||||
},
|
||||
params: [
|
||||
{
|
||||
name: 'field',
|
||||
filterFieldTypes: 'geo_point'
|
||||
}
|
||||
],
|
||||
getValue: function (agg, bucket) {
|
||||
return bucket[agg.id] && bucket[agg.id].location;
|
||||
}
|
||||
});
|
||||
}
|
38
src/ui/public/utils/zoom_to_precision.js
Normal file
38
src/ui/public/utils/zoom_to_precision.js
Normal file
|
@ -0,0 +1,38 @@
|
|||
import { geohashColumns } from 'ui/utils/decode_geo_hash';
|
||||
|
||||
const maxPrecision = 12;
|
||||
/**
|
||||
* Map Leaflet zoom levels to geohash precision levels.
|
||||
* The size of a geohash column-width on the map should be at least `minGeohashPixels` pixels wide.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
|
||||
const zoomPrecisionMap = {};
|
||||
const minGeohashPixels = 16;
|
||||
|
||||
function calculateZoomToPrecisionMap(maxZoom) {
|
||||
|
||||
for (let zoom = 0; zoom <= maxZoom; zoom += 1) {
|
||||
if (typeof zoomPrecisionMap[zoom] === 'number') {
|
||||
continue;
|
||||
}
|
||||
const worldPixels = 256 * Math.pow(2, zoom);
|
||||
zoomPrecisionMap[zoom] = 1;
|
||||
for (let precision = 2; precision <= maxPrecision; precision += 1) {
|
||||
const columns = geohashColumns(precision);
|
||||
if ((worldPixels / columns) >= minGeohashPixels) {
|
||||
zoomPrecisionMap[zoom] = precision;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default function zoomToPrecision(mapZoom, maxPrecision, maxZoom) {
|
||||
calculateZoomToPrecisionMap(typeof maxZoom === 'number' ? maxZoom : 21);
|
||||
return Math.min(zoomPrecisionMap[mapZoom], maxPrecision);
|
||||
}
|
|
@ -260,6 +260,11 @@ export default function AggConfigFactory(Private) {
|
|||
);
|
||||
};
|
||||
|
||||
AggConfig.prototype.getRequestAggs = function () {
|
||||
if (!this.type) return;
|
||||
return this.type.getRequestAggs(this) || [this];
|
||||
};
|
||||
|
||||
AggConfig.prototype.getResponseAggs = function () {
|
||||
if (!this.type) return;
|
||||
return this.type.getResponseAggs(this) || [this];
|
||||
|
|
|
@ -108,7 +108,6 @@ export default function AggConfigsFactory(Private) {
|
|||
})
|
||||
.value();
|
||||
}
|
||||
|
||||
this.getRequestAggs()
|
||||
.filter(function (config) {
|
||||
return !config.type.hasNoDsl;
|
||||
|
@ -145,12 +144,17 @@ export default function AggConfigsFactory(Private) {
|
|||
});
|
||||
|
||||
removeParentAggs(dslTopLvl);
|
||||
|
||||
return dslTopLvl;
|
||||
};
|
||||
|
||||
AggConfigs.prototype.getRequestAggs = function () {
|
||||
return _.sortBy(this, function (agg) {
|
||||
//collect all the aggregations
|
||||
const aggregations = this.reduce((requestValuesAggs, agg) => {
|
||||
const aggs = agg.getRequestAggs();
|
||||
return aggs ? requestValuesAggs.concat(aggs) : requestValuesAggs;
|
||||
}, []);
|
||||
//move metrics to the end
|
||||
return _.sortBy(aggregations, function (agg) {
|
||||
return agg.schema.group === 'metrics' ? 1 : 0;
|
||||
});
|
||||
};
|
||||
|
|
494
src/ui/public/vis_maps/__tests__/geohash_layer.js
Normal file
494
src/ui/public/vis_maps/__tests__/geohash_layer.js
Normal file
|
@ -0,0 +1,494 @@
|
|||
import expect from 'expect.js';
|
||||
import KibanaMap from 'ui/vis_maps/kibana_map';
|
||||
import GeohashLayer from 'ui/vis_maps/geohash_layer';
|
||||
import sampleData from './geohash_sample_data';
|
||||
|
||||
describe('kibana_map tests', function () {
|
||||
|
||||
let domNode;
|
||||
let kibanaMap;
|
||||
|
||||
|
||||
function setupDOM() {
|
||||
domNode = document.createElement('div');
|
||||
domNode.style.top = '0';
|
||||
domNode.style.left = '0';
|
||||
domNode.style.width = '512px';
|
||||
domNode.style.height = '512px';
|
||||
domNode.style.position = 'fixed';
|
||||
domNode.style['pointer-events'] = 'none';
|
||||
document.body.appendChild(domNode);
|
||||
}
|
||||
|
||||
function teardownDOM() {
|
||||
domNode.innerHTML = '';
|
||||
document.body.removeChild(domNode);
|
||||
}
|
||||
|
||||
|
||||
describe('GeohashGridLayer', function () {
|
||||
|
||||
beforeEach(async function () {
|
||||
setupDOM();
|
||||
kibanaMap = new KibanaMap(domNode, {
|
||||
minZoom: 1,
|
||||
maxZoom: 10
|
||||
});
|
||||
kibanaMap.setZoomLevel(3);
|
||||
kibanaMap.setCenter({
|
||||
lon: -100,
|
||||
lat: 40
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
kibanaMap.destroy();
|
||||
teardownDOM();
|
||||
});
|
||||
|
||||
[
|
||||
{
|
||||
options: { 'mapType': 'Scaled Circle Markers' },
|
||||
expected: `[
|
||||
{
|
||||
"fill": "#bd0026",
|
||||
"d": "M343,263.8A19.2,19.2,0,1,1,342.9,263.8 z"
|
||||
},
|
||||
{
|
||||
"fill": "#bd0026",
|
||||
"d": "M343,225.03843394373595A18.961566056264047,18.961566056264047,0,1,1,342.9,225.03843394373595 z"
|
||||
},
|
||||
{
|
||||
"fill": "#bd0026",
|
||||
"d": "M283,264.19815701843777A17.80184298156226,17.80184298156226,0,1,1,282.9,264.19815701843777 z"
|
||||
},
|
||||
{
|
||||
"fill": "#f03b20",
|
||||
"d": "M405,224.2748797495895A16.72512025041049,16.72512025041049,0,1,1,404.9,224.2748797495895 z"
|
||||
},
|
||||
{
|
||||
"fill": "#f03b20",
|
||||
"d": "M285,223.50180417608374A16.498195823916255,16.498195823916255,0,1,1,284.9,223.50180417608374 z"
|
||||
},
|
||||
{
|
||||
"fill": "#f03b20",
|
||||
"d": "M343,299.1036928470748A15.896307152925205,15.896307152925205,0,1,1,342.9,299.1036928470748 z"
|
||||
},
|
||||
{
|
||||
"fill": "#f03b20",
|
||||
"d": "M283,300.2846189453604A15.71538105463958,15.71538105463958,0,1,1,282.9,300.2846189453604 z"
|
||||
},
|
||||
{
|
||||
"fill": "#fd8d3c",
|
||||
"d": "M148,267.0272116156895A13.972788384310489,13.972788384310489,0,1,1,147.9,267.0272116156895 z"
|
||||
},
|
||||
{
|
||||
"fill": "#feb24c",
|
||||
"d": "M219,270.4178825645856A11.582117435414355,11.582117435414355,0,1,1,218.9,270.4178825645856 z"
|
||||
},
|
||||
{
|
||||
"fill": "#feb24c",
|
||||
"d": "M146,189.63311915018554A11.366880849814459,11.366880849814459,0,1,1,145.9,189.63311915018554 z"
|
||||
},
|
||||
{
|
||||
"fill": "#feb24c",
|
||||
"d": "M281,191.96973262756177A11.030267372438226,11.030267372438226,0,1,1,280.9,191.96973262756177 z"
|
||||
},
|
||||
{
|
||||
"fill": "#feb24c",
|
||||
"d": "M220,231.85362974571228A10.146370254287714,10.146370254287714,0,1,1,219.9,231.85362974571228 z"
|
||||
},
|
||||
{
|
||||
"fill": "#feb24c",
|
||||
"d": "M144,231.1923722152369A9.807627784763092,9.807627784763092,0,1,1,143.9,231.1923722152369 z"
|
||||
},
|
||||
{
|
||||
"fill": "#feb24c",
|
||||
"d": "M387,268.27221854599287A9.72778145400714,9.72778145400714,0,1,1,386.9,268.27221854599287 z"
|
||||
},
|
||||
{
|
||||
"fill": "#feb24c",
|
||||
"d": "M217,191.09542834646925A8.90457165353074,8.90457165353074,0,1,1,216.9,191.09542834646925 z"
|
||||
},
|
||||
{
|
||||
"fill": "#fed976",
|
||||
"d": "M218,300.40744573968243A8.592554260317598,8.592554260317598,0,1,1,217.9,300.40744573968243 z"
|
||||
},
|
||||
{
|
||||
"fill": "#fed976",
|
||||
"d": "M363,339.5411821762003A7.458817823799684,7.458817823799684,0,1,1,362.9,339.5411821762003 z"
|
||||
},
|
||||
{
|
||||
"fill": "#fed976",
|
||||
"d": "M331,205.43072931381437A6.569270686185644,6.569270686185644,0,1,1,330.9,205.43072931381437 z"
|
||||
},
|
||||
{
|
||||
"fill": "#fed976",
|
||||
"d": "M163,299.9012571034098A5.098742896590189,5.098742896590189,0,1,1,162.9,299.9012571034098 z"
|
||||
},
|
||||
{
|
||||
"fill": "#fed976",
|
||||
"d": "M34,77.6735731867532A4.326426813246795,4.326426813246795,0,1,1,33.9,77.6735731867532 z"
|
||||
},
|
||||
{
|
||||
"fill": "#fed976",
|
||||
"d": "M268,341.7954688958982A4.204531104101819,4.204531104101819,0,1,1,267.9,341.7954688958982 z"
|
||||
},
|
||||
{
|
||||
"fill": "#fed976",
|
||||
"d": "M71,118.82649906983305A4.173500930166947,4.173500930166947,0,1,1,70.9,118.82649906983305 z"
|
||||
},
|
||||
{
|
||||
"fill": "#fed976",
|
||||
"d": "M119,235.1169130974434A3.8830869025566206,3.8830869025566206,0,1,1,118.9,235.1169130974434 z"
|
||||
},
|
||||
{
|
||||
"fill": "#fed976",
|
||||
"d": "M451,396.15053353027315A3.849466469726874,3.849466469726874,0,1,1,450.9,396.15053353027315 z"
|
||||
},
|
||||
{
|
||||
"fill": "#fed976",
|
||||
"d": "M64,104.18445019554242A3.815549804457569,3.815549804457569,0,1,1,63.9,104.18445019554242 z"
|
||||
},
|
||||
{
|
||||
"fill": "#fed976",
|
||||
"d": "M7,15.430879972386867A3.5691200276131325,3.5691200276131325,0,1,1,6.9,15.430879972386867 z"
|
||||
},
|
||||
{
|
||||
"fill": "#fed976",
|
||||
"d": "M434,206.8985557756997A3.1014442243003013,3.1014442243003013,0,1,1,433.9,206.8985557756997 z"
|
||||
},
|
||||
{
|
||||
"fill": "#fed976",
|
||||
"d": "M119,201.2073035006183A2.792696499381677,2.792696499381677,0,1,1,118.9,201.2073035006183 z"
|
||||
},
|
||||
{
|
||||
"fill": "#fed976",
|
||||
"d": "M-1,420.89773444794906A2.1022655520509095,2.1022655520509095,0,1,1,-1.1,420.89773444794906 z"
|
||||
},
|
||||
{
|
||||
"fill": "#fed976",
|
||||
"d": "M443,217.859886428343A1.1401135716569843,1.1401135716569843,0,1,1,442.9,217.859886428343 z"
|
||||
},
|
||||
{
|
||||
"fill": "#fed976",
|
||||
"d": "M121,260.85988642834303A1.1401135716569843,1.1401135716569843,0,1,1,120.9,260.85988642834303 z"
|
||||
},
|
||||
{
|
||||
"fill": "#fed976",
|
||||
"d": "M-4,399.27892886445886A0.7210711355411324,0.7210711355411324,0,1,1,-4.1,399.27892886445886 z"
|
||||
}
|
||||
]`
|
||||
},
|
||||
{
|
||||
options: { 'mapType': 'Shaded Circle Markers' },
|
||||
expected: `[
|
||||
{
|
||||
"fill": "#bd0026",
|
||||
"d": "M343,267A16,16,0,1,1,342.9,267 z"
|
||||
},
|
||||
{
|
||||
"fill": "#bd0026",
|
||||
"d": "M343,226A18,18,0,1,1,342.9,226 z"
|
||||
},
|
||||
{
|
||||
"fill": "#bd0026",
|
||||
"d": "M283,266A16,16,0,1,1,282.9,266 z"
|
||||
},
|
||||
{
|
||||
"fill": "#f03b20",
|
||||
"d": "M405,223A18,18,0,1,1,404.9,223 z"
|
||||
},
|
||||
{
|
||||
"fill": "#f03b20",
|
||||
"d": "M285,222A18,18,0,1,1,284.9,222 z"
|
||||
},
|
||||
{
|
||||
"fill": "#f03b20",
|
||||
"d": "M343,300A15,15,0,1,1,342.9,300 z"
|
||||
},
|
||||
{
|
||||
"fill": "#f03b20",
|
||||
"d": "M283,301A15,15,0,1,1,282.9,301 z"
|
||||
},
|
||||
{
|
||||
"fill": "#fd8d3c",
|
||||
"d": "M148,265A16,16,0,1,1,147.9,265 z"
|
||||
},
|
||||
{
|
||||
"fill": "#feb24c",
|
||||
"d": "M219,266A16,16,0,1,1,218.9,266 z"
|
||||
},
|
||||
{
|
||||
"fill": "#feb24c",
|
||||
"d": "M146,183A18,18,0,1,1,145.9,183 z"
|
||||
},
|
||||
{
|
||||
"fill": "#feb24c",
|
||||
"d": "M281,184A19,19,0,1,1,280.9,184 z"
|
||||
},
|
||||
{
|
||||
"fill": "#feb24c",
|
||||
"d": "M220,225A17,17,0,1,1,219.9,225 z"
|
||||
},
|
||||
{
|
||||
"fill": "#feb24c",
|
||||
"d": "M144,224A17,17,0,1,1,143.9,224 z"
|
||||
},
|
||||
{
|
||||
"fill": "#feb24c",
|
||||
"d": "M387,262A16,16,0,1,1,386.9,262 z"
|
||||
},
|
||||
{
|
||||
"fill": "#feb24c",
|
||||
"d": "M217,181A19,19,0,1,1,216.9,181 z"
|
||||
},
|
||||
{
|
||||
"fill": "#fed976",
|
||||
"d": "M218,293A16,16,0,1,1,217.9,293 z"
|
||||
},
|
||||
{
|
||||
"fill": "#fed976",
|
||||
"d": "M363,333A14,14,0,1,1,362.9,333 z"
|
||||
},
|
||||
{
|
||||
"fill": "#fed976",
|
||||
"d": "M331,194A18,18,0,1,1,330.9,194 z"
|
||||
},
|
||||
{
|
||||
"fill": "#fed976",
|
||||
"d": "M163,290A15,15,0,1,1,162.9,290 z"
|
||||
},
|
||||
{
|
||||
"fill": "#fed976",
|
||||
"d": "M34,56A26,26,0,1,1,33.9,56 z"
|
||||
},
|
||||
{
|
||||
"fill": "#fed976",
|
||||
"d": "M268,332A14,14,0,1,1,267.9,332 z"
|
||||
},
|
||||
{
|
||||
"fill": "#fed976",
|
||||
"d": "M71,100A23,23,0,1,1,70.9,100 z"
|
||||
},
|
||||
{
|
||||
"fill": "#fed976",
|
||||
"d": "M119,222A17,17,0,1,1,118.9,222 z"
|
||||
},
|
||||
{
|
||||
"fill": "#fed976",
|
||||
"d": "M451,387A13,13,0,1,1,450.9,387 z"
|
||||
},
|
||||
{
|
||||
"fill": "#fed976",
|
||||
"d": "M64,84A24,24,0,1,1,63.9,84 z"
|
||||
},
|
||||
{
|
||||
"fill": "#fed976",
|
||||
"d": "M7,-7A26,26,0,1,1,6.9,-7 z"
|
||||
},
|
||||
{
|
||||
"fill": "#fed976",
|
||||
"d": "M434,192A18,18,0,1,1,433.9,192 z"
|
||||
},
|
||||
{
|
||||
"fill": "#fed976",
|
||||
"d": "M119,185A19,19,0,1,1,118.9,185 z"
|
||||
},
|
||||
{
|
||||
"fill": "#fed976",
|
||||
"d": "M-1,410A13,13,0,1,1,-1.1,410 z"
|
||||
},
|
||||
{
|
||||
"fill": "#fed976",
|
||||
"d": "M443,201A18,18,0,1,1,442.9,201 z"
|
||||
},
|
||||
{
|
||||
"fill": "#fed976",
|
||||
"d": "M121,245A17,17,0,1,1,120.9,245 z"
|
||||
},
|
||||
{
|
||||
"fill": "#fed976",
|
||||
"d": "M-4,386A14,14,0,1,1,-4.1,386 z"
|
||||
}
|
||||
]`
|
||||
},
|
||||
{
|
||||
options: { 'mapType': 'Shaded Geohash Grid' },
|
||||
expected: `[
|
||||
{
|
||||
"fill": "#bd0026",
|
||||
"d": "M313 301L313 261L377 261L377 301z"
|
||||
},
|
||||
{
|
||||
"fill": "#bd0026",
|
||||
"d": "M313 261L313 218L377 218L377 261z"
|
||||
},
|
||||
{
|
||||
"fill": "#bd0026",
|
||||
"d": "M249 301L249 261L313 261L313 301z"
|
||||
},
|
||||
{
|
||||
"fill": "#f03b20",
|
||||
"d": "M377 261L377 218L441 218L441 261z"
|
||||
},
|
||||
{
|
||||
"fill": "#f03b20",
|
||||
"d": "M249 261L249 218L313 218L313 261z"
|
||||
},
|
||||
{
|
||||
"fill": "#f03b20",
|
||||
"d": "M313 338L313 301L377 301L377 338z"
|
||||
},
|
||||
{
|
||||
"fill": "#f03b20",
|
||||
"d": "M249 338L249 301L313 301L313 338z"
|
||||
},
|
||||
{
|
||||
"fill": "#fd8d3c",
|
||||
"d": "M121 301L121 261L185 261L185 301z"
|
||||
},
|
||||
{
|
||||
"fill": "#feb24c",
|
||||
"d": "M185 301L185 261L249 261L249 301z"
|
||||
},
|
||||
{
|
||||
"fill": "#feb24c",
|
||||
"d": "M121 218L121 170L185 170L185 218z"
|
||||
},
|
||||
{
|
||||
"fill": "#feb24c",
|
||||
"d": "M249 218L249 170L313 170L313 218z"
|
||||
},
|
||||
{
|
||||
"fill": "#feb24c",
|
||||
"d": "M185 261L185 218L249 218L249 261z"
|
||||
},
|
||||
{
|
||||
"fill": "#feb24c",
|
||||
"d": "M121 261L121 218L185 218L185 261z"
|
||||
},
|
||||
{
|
||||
"fill": "#feb24c",
|
||||
"d": "M377 301L377 261L441 261L441 301z"
|
||||
},
|
||||
{
|
||||
"fill": "#feb24c",
|
||||
"d": "M185 218L185 170L249 170L249 218z"
|
||||
},
|
||||
{
|
||||
"fill": "#fed976",
|
||||
"d": "M185 338L185 301L249 301L249 338z"
|
||||
},
|
||||
{
|
||||
"fill": "#fed976",
|
||||
"d": "M313 374L313 338L377 338L377 374z"
|
||||
},
|
||||
{
|
||||
"fill": "#fed976",
|
||||
"d": "M313 218L313 170L377 170L377 218z"
|
||||
},
|
||||
{
|
||||
"fill": "#fed976",
|
||||
"d": "M121 338L121 301L185 301L185 338z"
|
||||
},
|
||||
{
|
||||
"fill": "#fed976",
|
||||
"d": "M-7 116L-7 54L57 54L57 116z"
|
||||
},
|
||||
{
|
||||
"fill": "#fed976",
|
||||
"d": "M249 374L249 338L313 338L313 374z"
|
||||
},
|
||||
{
|
||||
"fill": "#fed976",
|
||||
"d": "M57 170L57 116L121 116L121 170z"
|
||||
},
|
||||
{
|
||||
"fill": "#fed976",
|
||||
"d": "M57 261L57 218L121 218L121 261z"
|
||||
},
|
||||
{
|
||||
"fill": "#fed976",
|
||||
"d": "M441 408L441 374L505 374L505 408z"
|
||||
},
|
||||
{
|
||||
"fill": "#fed976",
|
||||
"d": "M57 116L57 54L121 54L121 116z"
|
||||
},
|
||||
{
|
||||
"fill": "#fed976",
|
||||
"d": "M-7 54L-7 -21L57 -21L57 54z"
|
||||
},
|
||||
{
|
||||
"fill": "#fed976",
|
||||
"d": "M377 218L377 170L441 170L441 218z"
|
||||
},
|
||||
{
|
||||
"fill": "#fed976",
|
||||
"d": "M57 218L57 170L121 170L121 218z"
|
||||
},
|
||||
{
|
||||
"fill": "#fed976",
|
||||
"d": "M-7 441L-7 408L57 408L57 441z"
|
||||
},
|
||||
{
|
||||
"fill": "#fed976",
|
||||
"d": "M441 261L441 218L505 218L505 261z"
|
||||
},
|
||||
{
|
||||
"fill": "#fed976",
|
||||
"d": "M57 301L57 261L121 261L121 301z"
|
||||
},
|
||||
{
|
||||
"fill": "#fed976",
|
||||
"d": "M-7 408L-7 374L57 374L57 408z"
|
||||
}
|
||||
]`
|
||||
}
|
||||
].forEach(function (test) {
|
||||
|
||||
it(test.options.mapType, function () {
|
||||
|
||||
const geohashGridOptions = test.options;
|
||||
const geohashLayer = new GeohashLayer(sampleData, geohashGridOptions, kibanaMap.getZoomLevel(), kibanaMap);
|
||||
kibanaMap.addLayer(geohashLayer);
|
||||
const markersNodeList = domNode.querySelectorAll('path.leaflet-clickable');
|
||||
const markerArray = [];
|
||||
for (let i = 0; i < markersNodeList.length; i++) {
|
||||
markerArray.push(markersNodeList[i]);
|
||||
}
|
||||
|
||||
const expectedGeohashGridMarkers = test.expected;
|
||||
const expectedMarkers = JSON.parse(expectedGeohashGridMarkers).map(path => {
|
||||
return {
|
||||
fill: path.fill,
|
||||
coords: path.d.match(/[0-9\.]+/g).map(parseFloat)
|
||||
};
|
||||
});
|
||||
const actualMarkers = markerArray.map(a => {
|
||||
return {
|
||||
fill: a.getAttribute('fill'),
|
||||
coords: a.getAttribute('d').match(/[0-9\.]+/g).map(parseFloat)
|
||||
};
|
||||
});
|
||||
expect(actualMarkers.length).to.equal(expectedMarkers.length);
|
||||
for (let i = 0; i < expectedMarkers.length; i++) {
|
||||
expect(actualMarkers[i].fill).to.equal(expectedMarkers[i].fill);
|
||||
actualMarkers[i].coords.forEach((coord, c) => {
|
||||
closeTo(actualMarkers[i].coords[c], expectedMarkers[i].coords[c]);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
function closeTo(actual, expected) {
|
||||
const epsilon = 1;//allow 2px slack
|
||||
expect(actual - epsilon < expected && expected < actual + epsilon).to.equal(true);
|
||||
}
|
1341
src/ui/public/vis_maps/__tests__/geohash_sample_data.js
Normal file
1341
src/ui/public/vis_maps/__tests__/geohash_sample_data.js
Normal file
File diff suppressed because it is too large
Load diff
137
src/ui/public/vis_maps/__tests__/kibana_map.js
Normal file
137
src/ui/public/vis_maps/__tests__/kibana_map.js
Normal file
|
@ -0,0 +1,137 @@
|
|||
import expect from 'expect.js';
|
||||
import KibanaMap from 'ui/vis_maps/kibana_map';
|
||||
|
||||
describe('kibana_map tests', function () {
|
||||
|
||||
let domNode;
|
||||
let kibanaMap;
|
||||
|
||||
function setupDOM() {
|
||||
domNode = document.createElement('div');
|
||||
domNode.style.top = '0';
|
||||
domNode.style.left = '0';
|
||||
domNode.style.width = '512px';
|
||||
domNode.style.height = '512px';
|
||||
domNode.style.position = 'fixed';
|
||||
domNode.style['pointer-events'] = 'none';
|
||||
document.body.appendChild(domNode);
|
||||
}
|
||||
|
||||
function teardownDOM() {
|
||||
domNode.innerHTML = '';
|
||||
document.body.removeChild(domNode);
|
||||
}
|
||||
|
||||
|
||||
describe('KibanaMap - basics', function () {
|
||||
|
||||
beforeEach(async function () {
|
||||
setupDOM();
|
||||
kibanaMap = new KibanaMap(domNode, {
|
||||
minZoom: 1,
|
||||
maxZoom: 10
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
kibanaMap.destroy();
|
||||
teardownDOM();
|
||||
});
|
||||
|
||||
it('should instantiate with world in view', function () {
|
||||
const bounds = kibanaMap.getBounds();
|
||||
expect(bounds.bottom_right.lon).to.equal(180);
|
||||
expect(bounds.top_left.lon).to.equal(-180);
|
||||
expect(kibanaMap.getCenter().lon).to.equal(0);
|
||||
expect(kibanaMap.getCenter().lat).to.equal(0);
|
||||
expect(kibanaMap.getZoomLevel()).to.equal(1);
|
||||
});
|
||||
|
||||
it('should resize to fit container', function () {
|
||||
|
||||
kibanaMap.setZoomLevel(2);
|
||||
expect(kibanaMap.getCenter().lon).to.equal(0);
|
||||
expect(kibanaMap.getCenter().lat).to.equal(0);
|
||||
|
||||
domNode.style.width = '1024px';
|
||||
domNode.style.height = '1024px';
|
||||
kibanaMap.resize();
|
||||
|
||||
expect(kibanaMap.getCenter().lon).to.equal(0);
|
||||
expect(kibanaMap.getCenter().lat).to.equal(0);
|
||||
const bounds = kibanaMap.getBounds();
|
||||
expect(bounds.bottom_right.lon).to.equal(180);
|
||||
expect(bounds.top_left.lon).to.equal(-180);
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('KibanaMap - baseLayer', function () {
|
||||
|
||||
beforeEach(async function () {
|
||||
setupDOM();
|
||||
kibanaMap = new KibanaMap(domNode, {
|
||||
minZoom: 1,
|
||||
maxZoom: 10
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
kibanaMap.destroy();
|
||||
teardownDOM();
|
||||
});
|
||||
|
||||
|
||||
it('TMS', async function () {
|
||||
|
||||
const options = {
|
||||
'url': 'https://tiles-stage.elastic.co/v2/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)'
|
||||
};
|
||||
|
||||
|
||||
return new Promise(function (resolve) {
|
||||
kibanaMap.on('baseLayer:loaded', () => {
|
||||
resolve();
|
||||
});
|
||||
kibanaMap.setBaseLayer({
|
||||
baseLayerType: 'tms',
|
||||
options: options
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('WMS', async function () {
|
||||
|
||||
const options = {
|
||||
url: 'https://basemap.nationalmap.gov/arcgis/services/USGSTopo/ MapServer/WMSServer',
|
||||
version: '1.3.0',
|
||||
layers: '0',
|
||||
format: 'image/png',
|
||||
transparent: true,
|
||||
attribution: 'Maps provided by USGS',
|
||||
styles: '',
|
||||
minZoom: 1,
|
||||
maxZoom: 18
|
||||
};
|
||||
|
||||
|
||||
return new Promise(function (resolve) {
|
||||
kibanaMap.on('baseLayer:loaded', () => {
|
||||
resolve();
|
||||
});
|
||||
kibanaMap.setBaseLayer({
|
||||
baseLayerType: 'wms',
|
||||
options: options
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
});
|
|
@ -1,215 +0,0 @@
|
|||
import expect from 'expect.js';
|
||||
import ngMock from 'ng_mock';
|
||||
import _ from 'lodash';
|
||||
import L from 'leaflet';
|
||||
|
||||
import sinon from 'auto-release-sinon';
|
||||
import geoJsonData from 'fixtures/vislib/mock_data/geohash/_geo_json';
|
||||
import $ from 'jquery';
|
||||
import VislibVisualizationsMapProvider from 'ui/vis_maps/visualizations/_map';
|
||||
|
||||
// // Data
|
||||
// const dataArray = [
|
||||
// ['geojson', require('fixtures/vislib/mock_data/geohash/_geo_json')],
|
||||
// ['columns', require('fixtures/vislib/mock_data/geohash/_columns')],
|
||||
// ['rows', require('fixtures/vislib/mock_data/geohash/_rows')],
|
||||
// ];
|
||||
|
||||
// TODO: Test the specific behavior of each these
|
||||
// const mapTypes = [
|
||||
// 'Scaled Circle Markers',
|
||||
// 'Shaded Circle Markers',
|
||||
// 'Shaded Geohash Grid',
|
||||
// 'Heatmap'
|
||||
// ];
|
||||
|
||||
describe('tilemaptest - TileMap Map Tests', function () {
|
||||
const $mockMapEl = $('<div>');
|
||||
let TileMapMap;
|
||||
let tilemapSettings;
|
||||
const leafletStubs = {};
|
||||
const leafletMocks = {};
|
||||
|
||||
|
||||
beforeEach(ngMock.module('kibana'));
|
||||
beforeEach(ngMock.inject(function (Private, $injector) {
|
||||
// mock parts of leaflet
|
||||
leafletMocks.tileLayer = { on: sinon.stub() };
|
||||
leafletMocks.map = { on: sinon.stub() };
|
||||
leafletStubs.tileLayer = sinon.stub(L, 'tileLayer', _.constant(leafletMocks.tileLayer));
|
||||
leafletStubs.tileLayer.wms = sinon.stub(L.tileLayer, 'wms', _.constant(leafletMocks.tileLayer));
|
||||
|
||||
leafletStubs.map = sinon.stub(L, 'map', _.constant(leafletMocks.map));
|
||||
|
||||
TileMapMap = Private(VislibVisualizationsMapProvider);
|
||||
|
||||
tilemapSettings = $injector.get('tilemapSettings');
|
||||
|
||||
}));
|
||||
|
||||
async function loadTileMapSettings() {
|
||||
await tilemapSettings.loadSettings();
|
||||
}
|
||||
|
||||
describe('instantiation', function () {
|
||||
let createStub;
|
||||
|
||||
beforeEach(loadTileMapSettings);
|
||||
|
||||
beforeEach(async function () {
|
||||
createStub = sinon.stub(TileMapMap.prototype, '_createMap', _.noop);
|
||||
new TileMapMap($mockMapEl, geoJsonData, {});
|
||||
});
|
||||
|
||||
it('should create the map', function () {
|
||||
expect(createStub.callCount).to.equal(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createMap', function () {
|
||||
let map;
|
||||
let mapStubs;
|
||||
|
||||
beforeEach(loadTileMapSettings);
|
||||
|
||||
beforeEach(function () {
|
||||
mapStubs = {
|
||||
destroy: sinon.stub(TileMapMap.prototype, 'destroy'),
|
||||
attachEvents: sinon.stub(TileMapMap.prototype, '_attachEvents'),
|
||||
addMarkers: sinon.stub(TileMapMap.prototype, '_addMarkers'),
|
||||
};
|
||||
map = new TileMapMap($mockMapEl, geoJsonData, {});
|
||||
});
|
||||
|
||||
it('should create leaflet objects for tileLayer and map', function () {
|
||||
expect(leafletStubs.tileLayer.callCount).to.equal(1);
|
||||
expect(leafletStubs.map.callCount).to.equal(1);
|
||||
|
||||
const callArgs = leafletStubs.map.firstCall.args;
|
||||
const mapOptions = callArgs[1];
|
||||
expect(callArgs[0]).to.be($mockMapEl.get(0));
|
||||
expect(mapOptions).to.have.property('zoom');
|
||||
expect(mapOptions).to.have.property('center');
|
||||
});
|
||||
|
||||
it('should attach events and add markers', function () {
|
||||
expect(mapStubs.attachEvents.callCount).to.equal(1);
|
||||
expect(mapStubs.addMarkers.callCount).to.equal(1);
|
||||
});
|
||||
|
||||
it('should call destroy only if a map exists', function () {
|
||||
expect(mapStubs.destroy.callCount).to.equal(0);
|
||||
map._createMap();
|
||||
expect(mapStubs.destroy.callCount).to.equal(1);
|
||||
});
|
||||
|
||||
it('should create a WMS layer if WMS is enabled', function () {
|
||||
expect(L.tileLayer.wms.called).to.be(false);
|
||||
map = new TileMapMap($mockMapEl, geoJsonData, { attr: { wms: { enabled: true } } });
|
||||
map._createMap();
|
||||
expect(L.tileLayer.wms.called).to.be(true);
|
||||
});
|
||||
|
||||
it('should create layer with all options from `tilemapSettings.getOptions()`', () => {
|
||||
sinon.assert.calledOnce(L.tileLayer);
|
||||
|
||||
const leafletOptions = tilemapSettings.getTMSOptions();
|
||||
expect(L.tileLayer.firstCall.args[1]).to.eql(leafletOptions);
|
||||
});
|
||||
});
|
||||
|
||||
describe('attachEvents', function () {
|
||||
beforeEach(loadTileMapSettings);
|
||||
|
||||
beforeEach(function () {
|
||||
sinon.stub(TileMapMap.prototype, '_createMap', function () {
|
||||
this._tileLayer = leafletMocks.tileLayer;
|
||||
this.map = leafletMocks.map;
|
||||
this._attachEvents();
|
||||
});
|
||||
new TileMapMap($mockMapEl, geoJsonData, {});
|
||||
});
|
||||
|
||||
it('should attach interaction events', function () {
|
||||
const expectedTileEvents = ['tileload'];
|
||||
const expectedMapEvents = ['draw:created', 'moveend', 'zoomend', 'unload'];
|
||||
const matchedEvents = {
|
||||
tiles: 0,
|
||||
maps: 0,
|
||||
};
|
||||
|
||||
_.times(leafletMocks.tileLayer.on.callCount, function (index) {
|
||||
const ev = leafletMocks.tileLayer.on.getCall(index).args[0];
|
||||
if (_.includes(expectedTileEvents, ev)) matchedEvents.tiles++;
|
||||
});
|
||||
expect(matchedEvents.tiles).to.equal(expectedTileEvents.length);
|
||||
|
||||
_.times(leafletMocks.map.on.callCount, function (index) {
|
||||
const ev = leafletMocks.map.on.getCall(index).args[0];
|
||||
if (_.includes(expectedMapEvents, ev)) matchedEvents.maps++;
|
||||
});
|
||||
expect(matchedEvents.maps).to.equal(expectedMapEvents.length);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('addMarkers', function () {
|
||||
let map;
|
||||
let createStub;
|
||||
|
||||
beforeEach(loadTileMapSettings);
|
||||
|
||||
beforeEach(function () {
|
||||
sinon.stub(TileMapMap.prototype, '_createMap');
|
||||
createStub = sinon.stub(TileMapMap.prototype, '_createMarkers', _.constant({ addLegend: _.noop }));
|
||||
map = new TileMapMap($mockMapEl, geoJsonData, {});
|
||||
});
|
||||
it('should pass the map options to the marker', function () {
|
||||
map._addMarkers();
|
||||
|
||||
const args = createStub.firstCall.args[0];
|
||||
expect(args).to.have.property('tooltipFormatter');
|
||||
expect(args).to.have.property('valueFormatter');
|
||||
expect(args).to.have.property('attr');
|
||||
});
|
||||
|
||||
it('should destroy existing markers', function () {
|
||||
const destroyStub = sinon.stub();
|
||||
map._markers = { destroy: destroyStub };
|
||||
map._addMarkers();
|
||||
|
||||
expect(destroyStub.callCount).to.be(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDataRectangles', function () {
|
||||
let map;
|
||||
|
||||
beforeEach(loadTileMapSettings);
|
||||
|
||||
beforeEach(function () {
|
||||
sinon.stub(TileMapMap.prototype, '_createMap');
|
||||
map = new TileMapMap($mockMapEl, geoJsonData, {});
|
||||
});
|
||||
|
||||
it('should return an empty array if no data', function () {
|
||||
map = new TileMapMap($mockMapEl, {}, {});
|
||||
const rects = map._getDataRectangles();
|
||||
expect(rects).to.have.length(0);
|
||||
});
|
||||
|
||||
it('should return an array of arrays of rectangles', function () {
|
||||
const rects = map._getDataRectangles();
|
||||
_.times(5, function () {
|
||||
const index = _.random(rects.length - 1);
|
||||
const rect = rects[index];
|
||||
const featureRect = geoJsonData.geoJson.features[index].properties.rectangle;
|
||||
expect(rect.length).to.equal(featureRect.length);
|
||||
|
||||
// should swap the array
|
||||
const checkIndex = _.random(rect.length - 1);
|
||||
expect(rect[checkIndex]).to.eql(featureRect[checkIndex]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,362 +0,0 @@
|
|||
|
||||
import angular from 'angular';
|
||||
import expect from 'expect.js';
|
||||
import ngMock from 'ng_mock';
|
||||
import _ from 'lodash';
|
||||
import L from 'leaflet';
|
||||
import sinon from 'auto-release-sinon';
|
||||
import geoJsonData from 'fixtures/vislib/mock_data/geohash/_geo_json';
|
||||
import VislibVisualizationsMarkerTypesBaseMarkerProvider from 'ui/vis_maps/visualizations/marker_types/base_marker';
|
||||
import VislibVisualizationsMarkerTypesShadedCirclesProvider from 'ui/vis_maps/visualizations/marker_types/shaded_circles';
|
||||
import VislibVisualizationsMarkerTypesScaledCirclesProvider from 'ui/vis_maps/visualizations/marker_types/scaled_circles';
|
||||
import VislibVisualizationsMarkerTypesHeatmapProvider from 'ui/vis_maps/visualizations/marker_types/heatmap';
|
||||
// defaults to roughly the lower 48 US states
|
||||
const defaultSWCoords = [13.496, -143.789];
|
||||
const defaultNECoords = [55.526, -57.919];
|
||||
const bounds = {};
|
||||
|
||||
angular.module('MarkerFactory', ['kibana']);
|
||||
|
||||
function setBounds(southWest, northEast) {
|
||||
bounds.southWest = L.latLng(southWest || defaultSWCoords);
|
||||
bounds.northEast = L.latLng(northEast || defaultNECoords);
|
||||
}
|
||||
|
||||
function getBounds() {
|
||||
return L.latLngBounds(bounds.southWest, bounds.northEast);
|
||||
}
|
||||
|
||||
const mockMap = {
|
||||
addLayer: _.noop,
|
||||
closePopup: _.noop,
|
||||
getBounds: getBounds,
|
||||
removeControl: _.noop,
|
||||
removeLayer: _.noop,
|
||||
getZoom: _.constant(5)
|
||||
};
|
||||
|
||||
describe('tilemaptest - Marker Tests', function () {
|
||||
let mapData;
|
||||
let markerLayer;
|
||||
|
||||
function createMarker(MarkerClass, geoJson, tooltipFormatter) {
|
||||
mapData = _.assign({}, geoJsonData.geoJson, geoJson || {});
|
||||
mapData.properties.allmin = mapData.properties.min;
|
||||
mapData.properties.allmax = mapData.properties.max;
|
||||
|
||||
return new MarkerClass(mockMap, mapData, {
|
||||
valueFormatter: geoJsonData.valueFormatter,
|
||||
tooltipFormatter: tooltipFormatter || null
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
setBounds();
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
if (markerLayer) {
|
||||
markerLayer.destroy();
|
||||
markerLayer = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
describe('Base Methods', function () {
|
||||
let MarkerClass;
|
||||
|
||||
beforeEach(ngMock.module('MarkerFactory'));
|
||||
beforeEach(ngMock.inject(function (Private) {
|
||||
MarkerClass = Private(VislibVisualizationsMarkerTypesBaseMarkerProvider);
|
||||
markerLayer = createMarker(MarkerClass);
|
||||
}));
|
||||
|
||||
describe('filterToMapBounds', function () {
|
||||
it('should not filter any features', function () {
|
||||
// set bounds to the entire world
|
||||
setBounds([-87.252, -343.828], [87.252, 343.125]);
|
||||
const boundFilter = markerLayer._filterToMapBounds();
|
||||
const mapFeature = mapData.features.filter(boundFilter);
|
||||
|
||||
expect(mapFeature.length).to.equal(mapData.features.length);
|
||||
});
|
||||
|
||||
it('should filter out data points that are outside of the map bounds', function () {
|
||||
// set bounds to roughly US southwest
|
||||
setBounds([31.690, -124.387], [42.324, -102.919]);
|
||||
const boundFilter = markerLayer._filterToMapBounds();
|
||||
const mapFeature = mapData.features.filter(boundFilter);
|
||||
|
||||
expect(mapFeature.length).to.be.lessThan(mapData.features.length);
|
||||
});
|
||||
});
|
||||
|
||||
describe('legendQuantizer', function () {
|
||||
it('should return a range of hex colors', function () {
|
||||
const minColor = markerLayer._legendQuantizer(mapData.properties.allmin);
|
||||
const maxColor = markerLayer._legendQuantizer(mapData.properties.allmax);
|
||||
|
||||
expect(minColor.substring(0, 1)).to.equal('#');
|
||||
expect(minColor).to.have.length(7);
|
||||
expect(maxColor.substring(0, 1)).to.equal('#');
|
||||
expect(maxColor).to.have.length(7);
|
||||
expect(minColor).to.not.eql(maxColor);
|
||||
});
|
||||
|
||||
it('should return a color with 1 color', function () {
|
||||
const geoJson = { properties: { min: 1, max: 1 } };
|
||||
markerLayer = createMarker(MarkerClass, geoJson);
|
||||
|
||||
// ensure the quantizer domain is correct
|
||||
const color = markerLayer._legendQuantizer(1);
|
||||
expect(color).to.not.be(undefined);
|
||||
expect(color.substring(0, 1)).to.equal('#');
|
||||
|
||||
// should always get the same color back
|
||||
_.times(5, function () {
|
||||
const randColor = markerLayer._legendQuantizer(0);
|
||||
expect(randColor).to.equal(color);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('applyShadingStyle', function () {
|
||||
it('should return a style object', function () {
|
||||
const style = markerLayer.applyShadingStyle(100);
|
||||
expect(style).to.be.an('object');
|
||||
|
||||
const keys = _.keys(style);
|
||||
const expected = ['fillColor', 'color'];
|
||||
_.each(expected, function (key) {
|
||||
expect(keys).to.contain(key);
|
||||
});
|
||||
});
|
||||
|
||||
it('should use the legendQuantizer', function () {
|
||||
const spy = sinon.spy(markerLayer, '_legendQuantizer');
|
||||
markerLayer.applyShadingStyle(100);
|
||||
expect(spy.callCount).to.equal(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('showTooltip', function () {
|
||||
it('should use the tooltip formatter', function () {
|
||||
const sample = _.sample(mapData.features);
|
||||
|
||||
markerLayer = createMarker(MarkerClass, null, Function.prototype);//create marker with tooltip
|
||||
markerLayer._attr.addTooltip = true;
|
||||
const stub = sinon.stub(markerLayer, '_tooltipFormatter', function () {
|
||||
return;
|
||||
});
|
||||
markerLayer._showTooltip(sample);
|
||||
expect(stub.callCount).to.equal(1);
|
||||
expect(stub.firstCall.calledWith(sample)).to.be(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addLegend', function () {
|
||||
let addToSpy;
|
||||
let leafletControlStub;
|
||||
|
||||
beforeEach(function () {
|
||||
addToSpy = sinon.spy();
|
||||
leafletControlStub = sinon.stub(L, 'control', function () {
|
||||
return {
|
||||
addTo: addToSpy
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
it('should do nothing if there is already a legend', function () {
|
||||
markerLayer._legend = { legend: 'exists' }; // anything truthy
|
||||
|
||||
markerLayer.addLegend();
|
||||
expect(leafletControlStub.callCount).to.equal(0);
|
||||
});
|
||||
|
||||
it('should create a leaflet control', function () {
|
||||
markerLayer.addLegend();
|
||||
expect(leafletControlStub.callCount).to.equal(1);
|
||||
expect(addToSpy.callCount).to.equal(1);
|
||||
expect(addToSpy.firstCall.calledWith(markerLayer.map)).to.be(true);
|
||||
expect(markerLayer._legend).to.have.property('onAdd');
|
||||
});
|
||||
|
||||
it('should use the value formatter', function () {
|
||||
const formatterSpy = sinon.spy(markerLayer, '_valueFormatter');
|
||||
// called twice for every legend color defined
|
||||
const expectedCallCount = markerLayer._legendColors.length * 2;
|
||||
|
||||
markerLayer.addLegend();
|
||||
const legend = markerLayer._legend.onAdd();
|
||||
|
||||
expect(formatterSpy.callCount).to.equal(expectedCallCount);
|
||||
expect(legend).to.be.a(HTMLDivElement);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Shaded Circles', function () {
|
||||
beforeEach(ngMock.module('MarkerFactory'));
|
||||
beforeEach(ngMock.inject(function (Private) {
|
||||
const MarkerClass = Private(VislibVisualizationsMarkerTypesShadedCirclesProvider);
|
||||
markerLayer = createMarker(MarkerClass);
|
||||
}));
|
||||
|
||||
describe('geohashMinDistance method', function () {
|
||||
it('should return a finite number', function () {
|
||||
const sample = _.sample(mapData.features);
|
||||
const distance = markerLayer._geohashMinDistance(sample);
|
||||
|
||||
expect(distance).to.be.a('number');
|
||||
expect(_.isFinite(distance)).to.be(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Scaled Circles', function () {
|
||||
let zoom;
|
||||
|
||||
beforeEach(ngMock.module('MarkerFactory'));
|
||||
beforeEach(ngMock.inject(function (Private) {
|
||||
zoom = _.random(1, 18);
|
||||
sinon.stub(mockMap, 'getZoom', _.constant(zoom));
|
||||
const MarkerClass = Private(VislibVisualizationsMarkerTypesScaledCirclesProvider);
|
||||
markerLayer = createMarker(MarkerClass);
|
||||
}));
|
||||
|
||||
describe('radiusScale method', function () {
|
||||
const valueArray = [10, 20, 30, 40, 50, 60];
|
||||
const max = _.max(valueArray);
|
||||
|
||||
it('should return 0 for value of 0', function () {
|
||||
expect(markerLayer._radiusScale(0)).to.equal(0);
|
||||
});
|
||||
|
||||
it('should return a scaled value for negative and positive numbers', function () {
|
||||
const upperBound = markerLayer._radiusScale(max);
|
||||
const results = [];
|
||||
|
||||
function roundValue(value) {
|
||||
// round number to 6 decimal places
|
||||
const r = Math.pow(10, 6);
|
||||
return Math.round(value * r) / r;
|
||||
}
|
||||
|
||||
_.each(valueArray, function (value, i) {
|
||||
const ratio = Math.pow(value / max, 0.5);
|
||||
const comparison = ratio * upperBound;
|
||||
const radius = markerLayer._radiusScale(value);
|
||||
const negRadius = markerLayer._radiusScale(value * -1);
|
||||
results.push(radius);
|
||||
|
||||
expect(negRadius).to.equal(radius);
|
||||
expect(roundValue(radius)).to.equal(roundValue(comparison));
|
||||
|
||||
// check that the radius is getting larger
|
||||
if (i > 0) {
|
||||
expect(radius).to.be.above(results[i - 1]);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Heatmaps', function () {
|
||||
beforeEach(ngMock.module('MarkerFactory'));
|
||||
beforeEach(ngMock.inject(function (Private) {
|
||||
const MarkerClass = Private(VislibVisualizationsMarkerTypesHeatmapProvider);
|
||||
markerLayer = createMarker(MarkerClass);
|
||||
}));
|
||||
|
||||
describe('dataToHeatArray', function () {
|
||||
let max;
|
||||
|
||||
beforeEach(function () {
|
||||
max = mapData.properties.allmax;
|
||||
});
|
||||
|
||||
it('should return an array or values for each feature', function () {
|
||||
const arr = markerLayer._dataToHeatArray(max);
|
||||
expect(arr).to.be.an('array');
|
||||
expect(arr).to.have.length(mapData.features.length);
|
||||
|
||||
});
|
||||
|
||||
it('should return an array item with lat, lng, metric for each feature', function () {
|
||||
_.times(3, function () {
|
||||
const arr = markerLayer._dataToHeatArray(max);
|
||||
const index = _.random(mapData.features.length - 1);
|
||||
const feature = mapData.features[index];
|
||||
const featureValue = feature.properties.value;
|
||||
const featureArr = feature.geometry.coordinates.slice(0).concat(featureValue);
|
||||
expect(arr[index]).to.eql(featureArr);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return an array item with lat, lng, normalized metric for each feature', function () {
|
||||
_.times(5, function () {
|
||||
markerLayer._attr.heatNormalizeData = true;
|
||||
|
||||
const arr = markerLayer._dataToHeatArray(max);
|
||||
const index = _.random(mapData.features.length - 1);
|
||||
const feature = mapData.features[index];
|
||||
const featureValue = feature.properties.value / max;
|
||||
const featureArr = feature.geometry.coordinates.slice(0).concat(featureValue);
|
||||
expect(arr[index]).to.eql(featureArr);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('tooltipProximity', function () {
|
||||
it('should return true if feature is close enough to event latlng', function () {
|
||||
_.times(5, function () {
|
||||
const feature = _.sample(mapData.features);
|
||||
const point = markerLayer._getLatLng(feature);
|
||||
const arr = markerLayer._tooltipProximity(point, feature);
|
||||
expect(arr).to.be(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return false if feature is not close enough to event latlng', function () {
|
||||
_.times(5, function () {
|
||||
const feature = _.sample(mapData.features);
|
||||
const point = L.latLng(90, -180);
|
||||
const arr = markerLayer._tooltipProximity(point, feature);
|
||||
expect(arr).to.be(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('nearestFeature', function () {
|
||||
it('should return nearest geoJson feature object', function () {
|
||||
_.times(5, function () {
|
||||
const feature = _.sample(mapData.features);
|
||||
const point = markerLayer._getLatLng(feature);
|
||||
const nearestPoint = markerLayer._nearestFeature(point);
|
||||
expect(nearestPoint).to.equal(feature);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLatLng', function () {
|
||||
it('should return a leaflet latLng object', function () {
|
||||
const feature = _.sample(mapData.features);
|
||||
const latLng = markerLayer._getLatLng(feature);
|
||||
const compare = L.latLng(feature.geometry.coordinates.slice(0).reverse());
|
||||
expect(latLng).to.eql(compare);
|
||||
});
|
||||
|
||||
it('should memoize the result', function () {
|
||||
const spy = sinon.spy(L, 'latLng');
|
||||
const feature = _.sample(mapData.features);
|
||||
|
||||
markerLayer._getLatLng(feature);
|
||||
expect(spy.callCount).to.be(1);
|
||||
|
||||
markerLayer._getLatLng(feature);
|
||||
expect(spy.callCount).to.be(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
|
@ -1,137 +0,0 @@
|
|||
import expect from 'expect.js';
|
||||
import ngMock from 'ng_mock';
|
||||
import _ from 'lodash';
|
||||
import sinon from 'auto-release-sinon';
|
||||
|
||||
import geoJsonData from 'fixtures/vislib/mock_data/geohash/_geo_json';
|
||||
import MockMap from 'fixtures/tilemap_map';
|
||||
import $ from 'jquery';
|
||||
import VislibVisualizationsTileMapProvider from 'ui/vis_maps/visualizations/tile_map';
|
||||
const mockChartEl = $('<div>');
|
||||
|
||||
let TileMap;
|
||||
let extentsStub;
|
||||
|
||||
function createTileMap(handler, chartEl, chartData) {
|
||||
handler = handler || {
|
||||
visConfig: {
|
||||
get: function () {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
uiState: {
|
||||
get: function () {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
};
|
||||
chartEl = chartEl || mockChartEl;
|
||||
chartData = chartData || geoJsonData;
|
||||
|
||||
return new TileMap(handler, chartEl, chartData);
|
||||
}
|
||||
|
||||
describe('tilemaptest - TileMap Tests', function () {
|
||||
let tilemap;
|
||||
|
||||
beforeEach(ngMock.module('kibana'));
|
||||
beforeEach(ngMock.inject(function (Private) {
|
||||
Private.stub(require('ui/vis_maps/visualizations/_map'), MockMap);
|
||||
TileMap = Private(VislibVisualizationsTileMapProvider);
|
||||
extentsStub = sinon.stub(TileMap.prototype, '_appendGeoExtents', _.noop);
|
||||
}));
|
||||
|
||||
beforeEach(function () {
|
||||
tilemap = createTileMap();
|
||||
});
|
||||
|
||||
it('should inherit props from chartData', function () {
|
||||
_.each(geoJsonData, function (val, prop) {
|
||||
expect(tilemap).to.have.property(prop, val);
|
||||
});
|
||||
});
|
||||
|
||||
it('should append geoExtents', function () {
|
||||
expect(extentsStub.callCount).to.equal(1);
|
||||
});
|
||||
|
||||
describe('draw', function () {
|
||||
it('should return a function', function () {
|
||||
expect(tilemap.draw()).to.be.a('function');
|
||||
});
|
||||
});
|
||||
|
||||
describe('appendMap', function () {
|
||||
let $selection;
|
||||
|
||||
beforeEach(function () {
|
||||
$selection = $('<div>');
|
||||
expect(tilemap.maps).to.have.length(0);
|
||||
tilemap._appendMap($selection);
|
||||
});
|
||||
|
||||
it('should add the tilemap class', function () {
|
||||
expect($selection.hasClass('tilemap')).to.equal(true);
|
||||
});
|
||||
|
||||
it('should append maps and required controls', function () {
|
||||
expect(tilemap.maps).to.have.length(1);
|
||||
const map = tilemap.maps[0];
|
||||
expect(map.addTitle.callCount).to.equal(0);
|
||||
expect(map.addFitControl.callCount).to.equal(1);
|
||||
expect(map.addBoundingControl.callCount).to.equal(1);
|
||||
});
|
||||
|
||||
it('should only add controls if data exists', function () {
|
||||
const noData = {
|
||||
geohashGridAgg: { vis: { params: {} } },
|
||||
geoJson: {
|
||||
features: [],
|
||||
properties: {},
|
||||
hits: 20
|
||||
}
|
||||
};
|
||||
tilemap = createTileMap(null, null, noData);
|
||||
|
||||
tilemap._appendMap($selection);
|
||||
expect(tilemap.maps).to.have.length(1);
|
||||
|
||||
const map = tilemap.maps[0];
|
||||
expect(map.addTitle.callCount).to.equal(0);
|
||||
expect(map.addFitControl.callCount).to.equal(0);
|
||||
expect(map.addBoundingControl.callCount).to.equal(0);
|
||||
});
|
||||
|
||||
it('should append title if set in the data object', function () {
|
||||
const mapTitle = 'Test Title';
|
||||
tilemap = createTileMap(null, null, _.assign({ title: mapTitle }, geoJsonData));
|
||||
tilemap._appendMap($selection);
|
||||
const map = tilemap.maps[0];
|
||||
|
||||
expect(map.addTitle.callCount).to.equal(1);
|
||||
expect(map.addTitle.firstCall.calledWith(mapTitle)).to.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('destroy', function () {
|
||||
const maps = [];
|
||||
const mapCount = 5;
|
||||
|
||||
beforeEach(function () {
|
||||
_.times(mapCount, function () {
|
||||
maps.push(new MockMap());
|
||||
});
|
||||
tilemap.maps = maps;
|
||||
expect(tilemap.maps).to.have.length(mapCount);
|
||||
tilemap.destroy();
|
||||
});
|
||||
|
||||
it('should destroy all the maps', function () {
|
||||
expect(tilemap.maps).to.have.length(0);
|
||||
expect(maps).to.have.length(mapCount);
|
||||
_.each(maps, function (map) {
|
||||
expect(map.destroy.callCount).to.equal(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
90
src/ui/public/vis_maps/geohash_layer.js
Normal file
90
src/ui/public/vis_maps/geohash_layer.js
Normal file
|
@ -0,0 +1,90 @@
|
|||
import KibanaMapLayer from './kibana_map_layer';
|
||||
import _ from 'lodash';
|
||||
import Heatmap from './markers/heatmap';
|
||||
import ScaledCircles from './markers/scaled_circles';
|
||||
import ShadedCircles from './markers/shaded_circles';
|
||||
import GeohashGrid from './markers/geohash_grid';
|
||||
|
||||
export default class GeohashLayer extends KibanaMapLayer {
|
||||
|
||||
constructor(featureCollection, options, zoom, kibanaMap) {
|
||||
|
||||
super();
|
||||
|
||||
this._geohashGeoJson = featureCollection;
|
||||
this._geohashOptions = options;
|
||||
this._zoom = zoom;
|
||||
this._kibanaMap = kibanaMap;
|
||||
|
||||
this._createGeohashMarkers();
|
||||
}
|
||||
|
||||
_createGeohashMarkers() {
|
||||
const markerOptions = {
|
||||
valueFormatter: this._geohashOptions.valueFormatter,
|
||||
tooltipFormatter: this._geohashOptions.tooltipFormatter
|
||||
};
|
||||
switch (this._geohashOptions.mapType) {
|
||||
case 'Scaled Circle Markers':
|
||||
this._geohashMarkers = new ScaledCircles(this._geohashGeoJson, markerOptions, this._zoom, this._kibanaMap);
|
||||
break;
|
||||
case 'Shaded Circle Markers':
|
||||
this._geohashMarkers = new ShadedCircles(this._geohashGeoJson, markerOptions, this._zoom, this._kibanaMap);
|
||||
break;
|
||||
case 'Shaded Geohash Grid':
|
||||
this._geohashMarkers = new GeohashGrid(this._geohashGeoJson, markerOptions, this._zoom, this._kibanaMap);
|
||||
break;
|
||||
case 'Heatmap':
|
||||
this._geohashMarkers = new Heatmap(this._geohashGeoJson, {
|
||||
radius: parseFloat(this._geohashOptions.heatmap.heatRadius),
|
||||
blur: parseFloat(this._geohashOptions.heatmap.heatBlur),
|
||||
maxZoom: parseFloat(this._geohashOptions.heatmap.heatMaxZoom),
|
||||
minOpaxity: parseFloat(this._geohashOptions.heatmap.heatMinOpacity),
|
||||
heatNormalizeData: parseFloat(this._geohashOptions.heatmap.heatNormalizeData),
|
||||
tooltipFormatter: this._geohashOptions.tooltipFormatter
|
||||
}, this._zoom, this._kibanaMap);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`${this._geohashOptions.mapType} mapType not recognized`);
|
||||
|
||||
}
|
||||
|
||||
this._geohashMarkers.on('showTooltip', (event) => this.emit('showTooltip', event));
|
||||
this._geohashMarkers.on('hideTooltip', (event) => this.emit('hideTooltip', event));
|
||||
this._leafletLayer = this._geohashMarkers.getLeafletLayer();
|
||||
}
|
||||
|
||||
appendLegendContents(jqueryDiv) {
|
||||
return this._geohashMarkers.appendLegendContents(jqueryDiv);
|
||||
}
|
||||
|
||||
movePointer(...args) {
|
||||
this._geohashMarkers.movePointer(...args);
|
||||
}
|
||||
|
||||
updateExtent() {
|
||||
//this removal is required to trigger the bounds filter again
|
||||
this._kibanaMap.removeLayer(this);
|
||||
this._createGeohashMarkers();
|
||||
this._kibanaMap.addLayer(this);
|
||||
}
|
||||
|
||||
|
||||
isReusable(options) {
|
||||
|
||||
if (_.isEqual(this._geohashOptions, options)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this._geohashOptions.mapType !== options.mapType) {
|
||||
return false;
|
||||
} else if (this._geohashOptions.mapType === 'Heatmap' && !_.isEqual(this._geohashOptions.heatmap, options)) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
526
src/ui/public/vis_maps/kibana_map.js
Normal file
526
src/ui/public/vis_maps/kibana_map.js
Normal file
|
@ -0,0 +1,526 @@
|
|||
import { EventEmitter } from 'events';
|
||||
import L from 'leaflet';
|
||||
import $ from 'jquery';
|
||||
import _ from 'lodash';
|
||||
import zoomToPrecision from 'ui/utils/zoom_to_precision';
|
||||
|
||||
const FitControl = L.Control.extend({
|
||||
options: {
|
||||
position: 'topleft'
|
||||
},
|
||||
initialize: function (fitContainer, kibanaMap) {
|
||||
this._fitContainer = fitContainer;
|
||||
this._kibanaMap = kibanaMap;
|
||||
this._leafletMap = null;
|
||||
},
|
||||
onAdd: function (leafletMap) {
|
||||
this._leafletMap = leafletMap;
|
||||
$(this._fitContainer).html('<a class="fa fa-crop" href="#" title="Fit Data Bounds"></a>')
|
||||
.on('click', e => {
|
||||
e.preventDefault();
|
||||
this._kibanaMap.fitToData();
|
||||
});
|
||||
|
||||
return this._fitContainer;
|
||||
},
|
||||
onRemove: function () {
|
||||
$(this._fitContainer).off('click');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const LegendControl = L.Control.extend({
|
||||
|
||||
options: {
|
||||
position: 'topright'
|
||||
},
|
||||
|
||||
updateContents() {
|
||||
this._legendContainer.empty();
|
||||
const $div = $('<div>').addClass('tilemap-legend');
|
||||
this._legendContainer.append($div);
|
||||
const layers = this._kibanaMap.getLayers();
|
||||
layers.forEach((layer) =>layer.appendLegendContents($div));
|
||||
},
|
||||
|
||||
|
||||
initialize: function (container, kibanaMap, position) {
|
||||
this._legendContainer = container;
|
||||
this._kibanaMap = kibanaMap;
|
||||
this.options.position = position;
|
||||
|
||||
},
|
||||
onAdd: function () {
|
||||
this._layerUpdateHandle = () => this.updateContents();
|
||||
this._kibanaMap.on('layers:update', this._layerUpdateHandle);
|
||||
this.updateContents();
|
||||
return this._legendContainer.get(0);
|
||||
},
|
||||
onRemove: function () {
|
||||
this._kibanaMap.removeListener('layers:update', this._layerUpdateHandle);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
/**
|
||||
* Collects map functionality required for Kibana.
|
||||
* Serves as simple abstraction for leaflet as well.
|
||||
*/
|
||||
class KibanaMap extends EventEmitter {
|
||||
|
||||
constructor(containerNode, options) {
|
||||
|
||||
super();
|
||||
this._containerNode = containerNode;
|
||||
this._leafletBaseLayer = null;
|
||||
this._baseLayerSettings = null;
|
||||
this._baseLayerIsDesaturated = true;
|
||||
|
||||
this._leafletDrawControl = null;
|
||||
this._leafletFitControl = null;
|
||||
this._leafletLegendControl = null;
|
||||
this._legendPosition = 'topright';
|
||||
|
||||
this._layers = [];
|
||||
this._listeners = [];
|
||||
this._showTooltip = false;
|
||||
|
||||
this._leafletMap = L.map(containerNode, {
|
||||
minZoom: options.minZoom,
|
||||
maxZoom: options.maxZoom
|
||||
});
|
||||
this._leafletMap.fitWorld();
|
||||
const worldBounds = L.latLngBounds(L.latLng(-90, -180), L.latLng(90, 180));
|
||||
this._leafletMap.setMaxBounds(worldBounds);
|
||||
|
||||
let previousZoom = this._leafletMap.getZoom();
|
||||
this._leafletMap.on('zoomend', () => {
|
||||
if (previousZoom !== this._leafletMap.getZoom()) {
|
||||
previousZoom = this._leafletMap.getZoom();
|
||||
this.emit('zoomchange');
|
||||
}
|
||||
});
|
||||
this._leafletMap.on('zoomend', () => this.emit('zoomend'));
|
||||
this._leafletMap.on('moveend', () => this.emit('moveend'));
|
||||
this._leafletMap.on('dragend', e => this._layers.forEach(layer => layer.updateExtent('dragend', e)));
|
||||
this._leafletMap.on('mousemove', e => this._layers.forEach(layer => layer.movePointer('mousemove', e)));
|
||||
this._leafletMap.on('mouseout', e => this._layers.forEach(layer => layer.movePointer('mouseout', e)));
|
||||
this._leafletMap.on('mousedown', e => this._layers.forEach(layer => layer.movePointer('mousedown', e)));
|
||||
this._leafletMap.on('mouseup', e => this._layers.forEach(layer => layer.movePointer('mouseup', e)));
|
||||
this._leafletMap.on('draw:created', event => {
|
||||
const drawType = event.layerType;
|
||||
if (drawType === 'rectangle') {
|
||||
const bounds = event.layer.getBounds();
|
||||
|
||||
const southEast = bounds.getSouthEast();
|
||||
const northWest = bounds.getNorthWest();
|
||||
let southEastLng = southEast.lng;
|
||||
if (southEastLng > 180) {
|
||||
southEastLng -= 360;
|
||||
}
|
||||
let northWestLng = northWest.lng;
|
||||
if (northWestLng < -180) {
|
||||
northWestLng += 360;
|
||||
}
|
||||
|
||||
const southEastLat = southEast.lat;
|
||||
const northWestLat = northWest.lat;
|
||||
|
||||
//Bounds cannot be created unless they form a box with larger than 0 dimensions
|
||||
//Invalid areas are rejected by ES.
|
||||
if (southEastLat === northWestLat || southEastLng === northWestLng) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.emit('drawCreated:rectangle', {
|
||||
bounds: {
|
||||
bottom_right: {
|
||||
lat: southEastLat,
|
||||
lon: southEastLng
|
||||
},
|
||||
top_left: {
|
||||
lat: northWestLat,
|
||||
lon: northWestLng
|
||||
}
|
||||
}
|
||||
});
|
||||
} else if (drawType === 'polygon') {
|
||||
const latLongs = event.layer.getLatLngs();
|
||||
this.emit('drawCreated:polygon', {
|
||||
points: latLongs.map(leafletLatLng => {
|
||||
return {
|
||||
lat: leafletLatLng.lat,
|
||||
lon: leafletLatLng.lng
|
||||
};
|
||||
})
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.resize();
|
||||
|
||||
}
|
||||
|
||||
setShowTooltip(showTooltip) {
|
||||
this._showTooltip = showTooltip;
|
||||
}
|
||||
|
||||
getLayers() {
|
||||
return this._layers.slice();
|
||||
}
|
||||
|
||||
|
||||
addLayer(kibanaLayer) {
|
||||
|
||||
|
||||
this.emit('layers:invalidate');
|
||||
|
||||
const onshowTooltip = (event) => {
|
||||
|
||||
if (!this._showTooltip) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._popup) {
|
||||
this._popup = L.popup({ autoPan: false });
|
||||
this._popup.setLatLng(event.position);
|
||||
this._popup.setContent(event.content);
|
||||
this._popup.openOn(this._leafletMap);
|
||||
} else {
|
||||
if (!this._popup.getLatLng().equals(event.position)) {
|
||||
this._popup.setLatLng(event.position);
|
||||
}
|
||||
if (this._popup.getContent() !== event.content) {
|
||||
this._popup.setContent(event.content);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
kibanaLayer.on('showTooltip', onshowTooltip);
|
||||
this._listeners.push({ name: 'showTooltip', handle: onshowTooltip, layer: kibanaLayer });
|
||||
|
||||
const onHideTooltip = () => {
|
||||
this._leafletMap.closePopup();
|
||||
this._popup = null;
|
||||
};
|
||||
kibanaLayer.on('hideTooltip', onHideTooltip);
|
||||
this._listeners.push({ name: 'hideTooltip', handle: onHideTooltip, layer: kibanaLayer });
|
||||
|
||||
|
||||
const onStyleChanged = () => {
|
||||
if (this._leafletLegendControl) {
|
||||
this._leafletLegendControl.updateContents();
|
||||
}
|
||||
};
|
||||
kibanaLayer.on('styleChanged', onStyleChanged);
|
||||
this._listeners.push({ name: 'styleChanged', handle: onStyleChanged, layer: kibanaLayer });
|
||||
|
||||
this._layers.push(kibanaLayer);
|
||||
kibanaLayer.addToLeafletMap(this._leafletMap);
|
||||
this.emit('layers:update');
|
||||
}
|
||||
|
||||
removeLayer(layer) {
|
||||
const index = this._layers.indexOf(layer);
|
||||
if (index >= 0) {
|
||||
this._layers.splice(index, 1);
|
||||
layer.removeFromLeafletMap(this._leafletMap);
|
||||
}
|
||||
this._listeners.forEach(listener => {
|
||||
if (listener.layer === layer) {
|
||||
listener.layer.removeListener(listener.name, listener.handle);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this._leafletFitControl) {
|
||||
this._leafletMap.removeControl(this._leafletFitControl);
|
||||
}
|
||||
if (this._leafletDrawControl) {
|
||||
this._leafletMap.removeControl(this._leafletDrawControl);
|
||||
}
|
||||
if (this._leafletLegendControl) {
|
||||
this._leafletMap.removeControl(this._leafletLegendControl);
|
||||
}
|
||||
this.setBaseLayer(null);
|
||||
for (const layer of this._layers) {
|
||||
layer.removeFromLeafletMap(this._leafletMap);
|
||||
}
|
||||
this._leafletMap.remove();
|
||||
this._containerNode.innerHTML = '';
|
||||
this._listeners.forEach(listener => listener.layer.removeListener(listener.name, listener.handle));
|
||||
}
|
||||
|
||||
getCenter() {
|
||||
const center = this._leafletMap.getCenter();
|
||||
return { lon: center.lng, lat: center.lat };
|
||||
}
|
||||
|
||||
setCenter(latitude, longitude) {
|
||||
const latLong = L.latLng(latitude, longitude);
|
||||
if (latLong.equals && !latLong.equals(this._leafletMap.getCenter())) {
|
||||
this._leafletMap.setView(latLong);
|
||||
}
|
||||
}
|
||||
|
||||
setZoomLevel(zoomLevel) {
|
||||
if (this._leafletMap.getZoom() !== zoomLevel) {
|
||||
this._leafletMap.setZoom(zoomLevel);
|
||||
}
|
||||
}
|
||||
|
||||
getZoomLevel() {
|
||||
return this._leafletMap.getZoom();
|
||||
}
|
||||
|
||||
getAutoPrecision() {
|
||||
return zoomToPrecision(this._leafletMap.getZoom(), 12, this._leafletMap.getMaxZoom());
|
||||
}
|
||||
|
||||
getBounds() {
|
||||
|
||||
const bounds = this._leafletMap.getBounds();
|
||||
if (!bounds) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const southEast = bounds.getSouthEast();
|
||||
const northWest = bounds.getNorthWest();
|
||||
let southEastLng = southEast.lng;
|
||||
if (southEastLng > 180) {
|
||||
southEastLng -= 360;
|
||||
}
|
||||
let northWestLng = northWest.lng;
|
||||
if (northWestLng < -180) {
|
||||
northWestLng += 360;
|
||||
}
|
||||
|
||||
const southEastLat = southEast.lat;
|
||||
const northWestLat = northWest.lat;
|
||||
|
||||
//Bounds cannot be created unless they form a box with larger than 0 dimensions
|
||||
//Invalid areas are rejected by ES.
|
||||
if (southEastLat === northWestLat || southEastLng === northWestLng) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
bottom_right: {
|
||||
lat: southEastLat,
|
||||
lon: southEastLng
|
||||
},
|
||||
top_left: {
|
||||
lat: northWestLat,
|
||||
lon: northWestLng
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
setDesaturateBaseLayer(isDesaturated) {
|
||||
if (isDesaturated === this._baseLayerIsDesaturated) {
|
||||
return;
|
||||
}
|
||||
this._baseLayerIsDesaturated = isDesaturated;
|
||||
this._updateDesaturation();
|
||||
this._leafletBaseLayer.redraw();
|
||||
}
|
||||
|
||||
addDrawControl() {
|
||||
const drawOptions = {
|
||||
draw: {
|
||||
polyline: false,
|
||||
marker: false,
|
||||
circle: false,
|
||||
polygon: false,
|
||||
rectangle: {
|
||||
shapeOptions: {
|
||||
stroke: false,
|
||||
color: '#000'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
this._leafletDrawControl = new L.Control.Draw(drawOptions);
|
||||
this._leafletMap.addControl(this._leafletDrawControl);
|
||||
}
|
||||
|
||||
addFitControl() {
|
||||
|
||||
if (this._leafletFitControl || !this._leafletMap) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fitContainer = L.DomUtil.create('div', 'leaflet-control leaflet-bar leaflet-control-fit');
|
||||
this._leafletFitControl = new FitControl(fitContainer, this);
|
||||
this._leafletMap.addControl(this._leafletFitControl);
|
||||
}
|
||||
|
||||
addLegendControl() {
|
||||
if (this._leafletLegendControl || !this._leafletMap) {
|
||||
return;
|
||||
}
|
||||
this._updateLegend();
|
||||
}
|
||||
|
||||
setLegendPosition(position) {
|
||||
this._legendPosition = position;
|
||||
if (this._leafletLegendControl) {
|
||||
this._leafletMap.removeControl(this._leafletLegendControl);
|
||||
this._updateLegend();
|
||||
}
|
||||
}
|
||||
|
||||
_updateLegend() {
|
||||
const $wrapper = $('<div>').addClass('tilemap-legend-wrapper');
|
||||
this._leafletLegendControl = new LegendControl($wrapper, this, this._legendPosition);
|
||||
this._leafletMap.addControl(this._leafletLegendControl);
|
||||
}
|
||||
|
||||
resize() {
|
||||
this._leafletMap.invalidateSize();
|
||||
this._updateExtent();
|
||||
}
|
||||
|
||||
|
||||
setBaseLayer(settings) {
|
||||
|
||||
if (_.isEqual(settings, this._baseLayerSettings)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._baseLayerSettings = settings;
|
||||
if (settings === null) {
|
||||
if (this._leafletBaseLayer && this._leafletMap) {
|
||||
this._leafletMap.removeLayer(this._leafletBaseLayer);
|
||||
this._leafletBaseLayer = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._leafletBaseLayer) {
|
||||
this._leafletMap.removeLayer(this._leafletBaseLayer);
|
||||
this._leafletBaseLayer = null;
|
||||
}
|
||||
|
||||
let baseLayer;
|
||||
if (settings.baseLayerType === 'wms') {
|
||||
baseLayer = this._getWMSBaseLayer(settings.options);
|
||||
} else if (settings.baseLayerType === 'tms') {
|
||||
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);
|
||||
}
|
||||
this.resize();
|
||||
|
||||
}
|
||||
|
||||
isInside(bucketRectBounds) {
|
||||
const mapBounds = this._leafletMap.getBounds();
|
||||
return mapBounds.intersects(bucketRectBounds);
|
||||
}
|
||||
|
||||
fitToData() {
|
||||
|
||||
if (!this._leafletMap) {
|
||||
return;
|
||||
}
|
||||
|
||||
let bounds = null;
|
||||
this._layers.forEach(layer => {
|
||||
const leafletLayer = layer.getLeafletLayer();
|
||||
const b = leafletLayer.getBounds();
|
||||
if (bounds) {
|
||||
bounds.extend(b);
|
||||
} else {
|
||||
bounds = b;
|
||||
}
|
||||
});
|
||||
|
||||
if (bounds) {
|
||||
this._leafletMap.fitBounds(bounds);
|
||||
}
|
||||
}
|
||||
|
||||
_getTMSBaseLayer(options) {
|
||||
return L.tileLayer(options.url, {
|
||||
minZoom: options.minZoom,
|
||||
maxZoom: options.maxZoom,
|
||||
subdomains: options.subdomains || [],
|
||||
attribution: options.attribution
|
||||
});
|
||||
}
|
||||
|
||||
_getWMSBaseLayer(options) {
|
||||
return L.tileLayer.wms(options.url, {
|
||||
attribution: options.attribution,
|
||||
format: options.format,
|
||||
layers: options.layers,
|
||||
minZoom: options.minZoom,
|
||||
maxZoom: options.maxZoom,
|
||||
styles: options.styles,
|
||||
transparent: options.transparent,
|
||||
version: options.version
|
||||
});
|
||||
}
|
||||
|
||||
_updateExtent() {
|
||||
this._layers.forEach(layer => layer.updateExtent());
|
||||
}
|
||||
|
||||
_updateDesaturation() {
|
||||
const tiles = $('img.leaflet-tile-loaded');
|
||||
if (this._baseLayerIsDesaturated) {
|
||||
tiles.removeClass('filters-off');
|
||||
} else if (!this._baseLayerIsDesaturated) {
|
||||
tiles.addClass('filters-off');
|
||||
}
|
||||
}
|
||||
|
||||
persistUiStateForVisualization(visualization) {
|
||||
this.on('moveend', () => {
|
||||
const uiState = visualization.getUiState();
|
||||
const centerFromUIState = uiState.get('mapCenter');
|
||||
const zoomFromUiState = parseInt(uiState.get('mapZoom'));
|
||||
if (isNaN(zoomFromUiState) || this.getZoomLevel() !== zoomFromUiState) {
|
||||
uiState.set('mapZoom', this.getZoomLevel());
|
||||
}
|
||||
const centerFromMap = this.getCenter();
|
||||
if (!centerFromUIState || centerFromMap.lon !== centerFromUIState[1] || centerFromMap.lat !== centerFromUIState[0]) {
|
||||
uiState.set('mapCenter', [centerFromMap.lat, centerFromMap.lon]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
useUiStateFromVisualization(visualization) {
|
||||
const uiState = visualization.getUiState();
|
||||
const zoomFromUiState = parseInt(uiState.get('mapZoom'));
|
||||
const centerFromUIState = uiState.get('mapCenter');
|
||||
if (!isNaN(zoomFromUiState)) {
|
||||
this.setZoomLevel(zoomFromUiState);
|
||||
}
|
||||
if (centerFromUIState) {
|
||||
this.setCenter(centerFromUIState[0], centerFromUIState[1]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
export default KibanaMap;
|
||||
|
40
src/ui/public/vis_maps/kibana_map_layer.js
Normal file
40
src/ui/public/vis_maps/kibana_map_layer.js
Normal file
|
@ -0,0 +1,40 @@
|
|||
import { EventEmitter } from 'events';
|
||||
|
||||
|
||||
export default class KibanaMapLayer extends EventEmitter {
|
||||
constructor() {
|
||||
super();
|
||||
this._leafletLayer = null;
|
||||
}
|
||||
getLeafletLayer() {
|
||||
return this._leafletLayer;
|
||||
}
|
||||
|
||||
addToLeafletMap(leafletMap) {
|
||||
this._leafletLayer.addTo(leafletMap);
|
||||
}
|
||||
|
||||
removeFromLeafletMap(leafletMap) {
|
||||
leafletMap.removeLayer(this._leafletLayer);
|
||||
}
|
||||
|
||||
appendLegendContents() {
|
||||
}
|
||||
|
||||
updateExtent() {
|
||||
}
|
||||
|
||||
movePointer() {
|
||||
}
|
||||
|
||||
getBounds() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,200 +0,0 @@
|
|||
import d3 from 'd3';
|
||||
import _ from 'lodash';
|
||||
export default function DataFactory() {
|
||||
/**
|
||||
* Provides an API for pulling values off the data
|
||||
* and calculating values using the data
|
||||
*
|
||||
* @class Data
|
||||
* @constructor
|
||||
* @param data {Object} Elasticsearch query results
|
||||
* @param attr {Object|*} Visualization options
|
||||
*/
|
||||
class Data {
|
||||
constructor(data, uiState) {
|
||||
this.uiState = uiState;
|
||||
this.data = this.copyDataObj(data);
|
||||
this._normalizeOrdered();
|
||||
}
|
||||
|
||||
copyDataObj(data) {
|
||||
const copyChart = data => {
|
||||
const newData = {};
|
||||
Object.keys(data).forEach(key => {
|
||||
if (key !== 'series') {
|
||||
newData[key] = data[key];
|
||||
} else {
|
||||
newData[key] = data[key].map(seri => {
|
||||
return {
|
||||
label: seri.label,
|
||||
values: seri.values.map(val => {
|
||||
const newVal = _.clone(val);
|
||||
newVal.aggConfig = val.aggConfig;
|
||||
newVal.aggConfigResult = val.aggConfigResult;
|
||||
newVal.extraMetrics = val.extraMetrics;
|
||||
return newVal;
|
||||
})
|
||||
};
|
||||
});
|
||||
}
|
||||
});
|
||||
return newData;
|
||||
};
|
||||
|
||||
if (!data.series) {
|
||||
const newData = {};
|
||||
Object.keys(data).forEach(key => {
|
||||
if (!['rows', 'columns'].includes(key)) {
|
||||
newData[key] = data[key];
|
||||
}
|
||||
else {
|
||||
newData[key] = data[key].map(chart => {
|
||||
return copyChart(chart);
|
||||
});
|
||||
}
|
||||
});
|
||||
return newData;
|
||||
}
|
||||
return copyChart(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of the actual x and y data value objects
|
||||
* from data with series keys
|
||||
*
|
||||
* @method chartData
|
||||
* @returns {*} Array of data objects
|
||||
*/
|
||||
chartData() {
|
||||
if (!this.data.series) {
|
||||
const arr = this.data.rows ? this.data.rows : this.data.columns;
|
||||
return _.toArray(arr);
|
||||
}
|
||||
return [this.data];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of chart data objects
|
||||
*
|
||||
* @method getVisData
|
||||
* @returns {*} Array of chart data objects
|
||||
*/
|
||||
getVisData() {
|
||||
let visData;
|
||||
|
||||
if (this.data.rows) {
|
||||
visData = this.data.rows;
|
||||
} else if (this.data.columns) {
|
||||
visData = this.data.columns;
|
||||
} else {
|
||||
visData = [this.data];
|
||||
}
|
||||
|
||||
return visData;
|
||||
}
|
||||
|
||||
/**
|
||||
* get min and max for all cols, rows of data
|
||||
*
|
||||
* @method getMaxMin
|
||||
* @return {Object}
|
||||
*/
|
||||
getGeoExtents() {
|
||||
const visData = this.getVisData();
|
||||
|
||||
return _.reduce(_.pluck(visData, 'geoJson.properties'), function (minMax, props) {
|
||||
return {
|
||||
min: Math.min(props.min, minMax.min),
|
||||
max: Math.max(props.max, minMax.max)
|
||||
};
|
||||
}, { min: Infinity, max: -Infinity });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get attributes off the data, e.g. `tooltipFormatter` or `xAxisFormatter`
|
||||
* pulls the value off the first item in the array
|
||||
* these values are typically the same between data objects of the same chart
|
||||
* TODO: May need to verify this or refactor
|
||||
*
|
||||
* @method get
|
||||
* @param thing {String} Data object key
|
||||
* @returns {*} Data object value
|
||||
*/
|
||||
get(thing, def) {
|
||||
const source = (this.data.rows || this.data.columns || [this.data])[0];
|
||||
return _.get(source, thing, def);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an array of all value objects
|
||||
* Pluck the data.series array from each data object
|
||||
* Create an array of all the value objects from the series array
|
||||
*
|
||||
* @method flatten
|
||||
* @returns {Array} Value objects
|
||||
*/
|
||||
flatten() {
|
||||
return _(this.chartData())
|
||||
.pluck('series')
|
||||
.flattenDeep()
|
||||
.pluck('values')
|
||||
.flattenDeep()
|
||||
.value();
|
||||
}
|
||||
|
||||
/**
|
||||
* ensure that the datas ordered property has a min and max
|
||||
* if the data represents an ordered date range.
|
||||
*
|
||||
* @return {undefined}
|
||||
*/
|
||||
_normalizeOrdered() {
|
||||
const data = this.getVisData();
|
||||
const self = this;
|
||||
|
||||
data.forEach(function (d) {
|
||||
if (!d.ordered || !d.ordered.date) return;
|
||||
|
||||
const missingMin = d.ordered.min == null;
|
||||
const missingMax = d.ordered.max == null;
|
||||
|
||||
if (missingMax || missingMin) {
|
||||
const extent = d3.extent(self.xValues());
|
||||
if (missingMin) d.ordered.min = extent[0];
|
||||
if (missingMax) d.ordered.max = extent[1];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates min and max values for all map data
|
||||
* series.rows is an array of arrays
|
||||
* each row is an array of values
|
||||
* last value in row array is bucket count
|
||||
*
|
||||
* @method mapDataExtents
|
||||
* @param series {Array} Array of data objects
|
||||
* @returns {Array} min and max values
|
||||
*/
|
||||
mapDataExtents(series) {
|
||||
const values = _.map(series.rows, function (row) {
|
||||
return row[row.length - 1];
|
||||
});
|
||||
return [_.min(values), _.max(values)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the maximum number of series, considering each chart
|
||||
* individually.
|
||||
*
|
||||
* @return {number} - the largest number of series from all charts
|
||||
*/
|
||||
maxNumberOfSeries() {
|
||||
return this.chartData().reduce(function (max, chart) {
|
||||
return Math.max(max, chart.series.length);
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
return Data;
|
||||
}
|
|
@ -1,299 +0,0 @@
|
|||
import d3 from 'd3';
|
||||
import _ from 'lodash';
|
||||
import $ from 'jquery';
|
||||
import SimpleEmitter from 'ui/utils/simple_emitter';
|
||||
|
||||
export default function DispatchClass(Private, config) {
|
||||
|
||||
/**
|
||||
* Handles event responses
|
||||
*
|
||||
* @class Dispatch
|
||||
* @constructor
|
||||
* @param handler {Object} Reference to Handler Class Object
|
||||
*/
|
||||
|
||||
class Dispatch extends SimpleEmitter {
|
||||
constructor(handler) {
|
||||
super();
|
||||
this.handler = handler;
|
||||
this._listeners = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Response to click and hover events
|
||||
*
|
||||
* @param d {Object} Data point
|
||||
* @param i {Number} Index number of data point
|
||||
* @returns {{value: *, point: *, label: *, color: *, pointIndex: *,
|
||||
* series: *, config: *, data: (Object|*),
|
||||
* e: (d3.event|*), handler: (Object|*)}} Event response object
|
||||
*/
|
||||
eventResponse(d, i) {
|
||||
const datum = d._input || d;
|
||||
const data = d3.event.target.nearestViewportElement ?
|
||||
d3.event.target.nearestViewportElement.__data__ : d3.event.target.__data__;
|
||||
const label = d.label ? d.label : (d.series || 'Count');
|
||||
const isSeries = !!(data && data.series);
|
||||
const isSlices = !!(data && data.slices);
|
||||
const series = isSeries ? data.series : undefined;
|
||||
const slices = isSlices ? data.slices : undefined;
|
||||
const handler = this.handler;
|
||||
const color = _.get(handler, 'data.color');
|
||||
const isPercentage = (handler && handler.visConfig.get('mode', 'normal') === 'percentage');
|
||||
|
||||
const eventData = {
|
||||
value: d.y,
|
||||
point: datum,
|
||||
datum: datum,
|
||||
label: label,
|
||||
color: color ? color(label) : undefined,
|
||||
pointIndex: i,
|
||||
series: series,
|
||||
slices: slices,
|
||||
config: handler && handler.visConfig,
|
||||
data: data,
|
||||
e: d3.event,
|
||||
handler: handler
|
||||
};
|
||||
|
||||
if (isSeries) {
|
||||
// Find object with the actual d value and add it to the point object
|
||||
const object = _.find(series, { 'label': label });
|
||||
if (object) {
|
||||
eventData.value = +object.values[i].y;
|
||||
|
||||
if (isPercentage) {
|
||||
// Add the formatted percentage to the point object
|
||||
eventData.percent = (100 * d.y).toFixed(1) + '%';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return eventData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a function that adds events and listeners to a D3 selection
|
||||
*
|
||||
* @method addEvent
|
||||
* @param event {String}
|
||||
* @param callback {Function}
|
||||
* @returns {Function}
|
||||
*/
|
||||
addEvent(event, callback) {
|
||||
return function (selection) {
|
||||
selection.each(function () {
|
||||
const element = d3.select(this);
|
||||
|
||||
if (typeof callback === 'function') {
|
||||
return element.on(event, callback);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @method addHoverEvent
|
||||
* @returns {Function}
|
||||
*/
|
||||
addHoverEvent() {
|
||||
const self = this;
|
||||
const isClickable = this.listenerCount('click') > 0;
|
||||
const addEvent = this.addEvent;
|
||||
const $el = this.handler.el;
|
||||
if (!this.handler.highlight) {
|
||||
this.handler.highlight = self.highlight;
|
||||
}
|
||||
|
||||
function hover(d, i) {
|
||||
// Add pointer if item is clickable
|
||||
if (isClickable) {
|
||||
self.addMousePointer.call(this, arguments);
|
||||
}
|
||||
|
||||
self.handler.highlight.call(this, $el);
|
||||
self.emit('hover', self.eventResponse(d, i));
|
||||
}
|
||||
|
||||
return addEvent('mouseover', hover);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @method addMouseoutEvent
|
||||
* @returns {Function}
|
||||
*/
|
||||
addMouseoutEvent() {
|
||||
const self = this;
|
||||
const addEvent = this.addEvent;
|
||||
const $el = this.handler.el;
|
||||
if (!this.handler.unHighlight) {
|
||||
this.handler.unHighlight = self.unHighlight;
|
||||
}
|
||||
|
||||
function mouseout() {
|
||||
self.handler.unHighlight.call(this, $el);
|
||||
}
|
||||
|
||||
return addEvent('mouseout', mouseout);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @method addClickEvent
|
||||
* @returns {Function}
|
||||
*/
|
||||
addClickEvent() {
|
||||
const self = this;
|
||||
const addEvent = this.addEvent;
|
||||
|
||||
function click(d, i) {
|
||||
self.emit('click', self.eventResponse(d, i));
|
||||
}
|
||||
|
||||
return addEvent('click', click);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if we will allow brushing
|
||||
*
|
||||
* @method allowBrushing
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
allowBrushing() {
|
||||
const xAxis = this.handler.categoryAxes[0];
|
||||
|
||||
//Allow brushing for ordered axis - date histogram and histogram
|
||||
return Boolean(xAxis.ordered);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if brushing is currently enabled
|
||||
*
|
||||
* @method isBrushable
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
isBrushable() {
|
||||
return this.allowBrushing() && this.listenerCount('brush') > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mouseover Behavior
|
||||
*
|
||||
* @method addMousePointer
|
||||
* @returns {d3.Selection}
|
||||
*/
|
||||
addMousePointer() {
|
||||
return d3.select(this).style('cursor', 'pointer');
|
||||
}
|
||||
|
||||
/**
|
||||
* Highlight the element that is under the cursor
|
||||
* by reducing the opacity of all the elements on the graph.
|
||||
* @param element {d3.Selection}
|
||||
* @method highlight
|
||||
*/
|
||||
highlight(element) {
|
||||
const label = this.getAttribute('data-label');
|
||||
if (!label) return;
|
||||
|
||||
const dimming = config.get('visualization:dimmingOpacity');
|
||||
$(element).parent().find('[data-label]')
|
||||
.css('opacity', 1)//Opacity 1 is needed to avoid the css application
|
||||
.not((els, el) => String($(el).data('label')) === label)
|
||||
.css('opacity', justifyOpacity(dimming));
|
||||
}
|
||||
|
||||
/**
|
||||
* Mouseout Behavior
|
||||
*
|
||||
* @param element {d3.Selection}
|
||||
* @method unHighlight
|
||||
*/
|
||||
unHighlight(element) {
|
||||
$('[data-label]', element.parentNode).css('opacity', 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds D3 brush to SVG and returns the brush function
|
||||
*
|
||||
* @param xScale {Function} D3 xScale function
|
||||
* @param svg {HTMLElement} Reference to SVG
|
||||
* @returns {*} Returns a D3 brush function and a SVG with a brush group attached
|
||||
*/
|
||||
createBrush(xScale, svg) {
|
||||
const self = this;
|
||||
const visConfig = self.handler.visConfig;
|
||||
const { width, height } = svg.node().getBBox();
|
||||
const isHorizontal = self.handler.categoryAxes[0].axisConfig.isHorizontal();
|
||||
|
||||
// Brush scale
|
||||
const brush = d3.svg.brush();
|
||||
if (isHorizontal) {
|
||||
brush.x(xScale);
|
||||
} else {
|
||||
brush.y(xScale);
|
||||
}
|
||||
|
||||
brush.on('brushend', function brushEnd() {
|
||||
|
||||
// Assumes data is selected at the chart level
|
||||
// In this case, the number of data objects should always be 1
|
||||
const data = d3.select(this).data()[0];
|
||||
const isTimeSeries = (data.ordered && data.ordered.date);
|
||||
|
||||
// Allows for brushing on d3.scale.ordinal()
|
||||
const selected = xScale.domain().filter(function (d) {
|
||||
return (brush.extent()[0] <= xScale(d)) && (xScale(d) <= brush.extent()[1]);
|
||||
});
|
||||
const range = isTimeSeries ? brush.extent() : selected;
|
||||
|
||||
return self.emit('brush', {
|
||||
range: range,
|
||||
config: visConfig,
|
||||
e: d3.event,
|
||||
data: data
|
||||
});
|
||||
});
|
||||
|
||||
// if `addBrushing` is true, add brush canvas
|
||||
if (self.listenerCount('brush')) {
|
||||
const rect = svg.insert('g', 'g')
|
||||
.attr('class', 'brush')
|
||||
.call(brush)
|
||||
.call(function (brushG) {
|
||||
// hijack the brush start event to filter out right/middle clicks
|
||||
const brushHandler = brushG.on('mousedown.brush');
|
||||
if (!brushHandler) return; // touch events in use
|
||||
brushG.on('mousedown.brush', function () {
|
||||
if (validBrushClick(d3.event)) brushHandler.apply(this, arguments);
|
||||
});
|
||||
})
|
||||
.selectAll('rect');
|
||||
|
||||
if (isHorizontal) {
|
||||
rect.attr('height', height);
|
||||
} else {
|
||||
rect.attr('width', width);
|
||||
}
|
||||
|
||||
return brush;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function validBrushClick(event) {
|
||||
return event.button === 0;
|
||||
}
|
||||
|
||||
|
||||
function justifyOpacity(opacity) {
|
||||
const decimalNumber = parseFloat(opacity, 10);
|
||||
const fallbackOpacity = 0.5;
|
||||
return (0 <= decimalNumber && decimalNumber <= 1) ? decimalNumber : fallbackOpacity;
|
||||
}
|
||||
|
||||
return Dispatch;
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
import d3 from 'd3';
|
||||
import MapSplitProvider from './splits/map_split';
|
||||
|
||||
export default function LayoutFactory(Private) {
|
||||
const mapSplit = Private(MapSplitProvider);
|
||||
class Layout {
|
||||
constructor(el, config, data) {
|
||||
this.el = el;
|
||||
this.config = config;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
render() {
|
||||
this.removeAll();
|
||||
this.createLayout();
|
||||
}
|
||||
|
||||
createLayout() {
|
||||
const wrapper = this.appendElem(this.el, 'div', 'vis-wrapper');
|
||||
wrapper.datum(this.data.data);
|
||||
const colWrapper = this.appendElem(wrapper.node(), 'div', 'vis-col-wrapper');
|
||||
const chartWrapper = this.appendElem(colWrapper.node(), 'div', 'chart-wrapper');
|
||||
chartWrapper.call(mapSplit, colWrapper.node(), this.config);
|
||||
}
|
||||
|
||||
appendElem(el, type, className) {
|
||||
if (!el || !type || !className) {
|
||||
throw new Error('Function requires that an el, type, and class be provided');
|
||||
}
|
||||
|
||||
if (typeof el === 'string') {
|
||||
// Create a DOM reference with a d3 selection
|
||||
// Need to make sure that the `el` is bound to this object
|
||||
// to prevent it from being appended to another Layout
|
||||
el = d3.select(this.el)
|
||||
.select(el)[0][0];
|
||||
}
|
||||
|
||||
return d3.select(el)
|
||||
.append(type)
|
||||
.attr('class', className);
|
||||
}
|
||||
|
||||
removeAll() {
|
||||
return d3.select(this.el).selectAll('*').remove();
|
||||
}
|
||||
}
|
||||
|
||||
return Layout;
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
/**
|
||||
* Provides vislib configuration, throws error if invalid property is accessed without providing defaults
|
||||
*/
|
||||
import _ from 'lodash';
|
||||
|
||||
export default function MapsConfigFactory() {
|
||||
|
||||
const DEFAULT_VIS_CONFIG = {
|
||||
style: {
|
||||
margin : { top: 10, right: 3, bottom: 5, left: 3 }
|
||||
},
|
||||
alerts: {},
|
||||
categoryAxes: [],
|
||||
valueAxes: []
|
||||
};
|
||||
|
||||
|
||||
class MapsConfig {
|
||||
constructor(mapsConfigArgs) {
|
||||
this._values = _.defaultsDeep({}, mapsConfigArgs, DEFAULT_VIS_CONFIG);
|
||||
}
|
||||
|
||||
get(property, defaults) {
|
||||
if (_.has(this._values, property) || typeof defaults !== 'undefined') {
|
||||
return _.get(this._values, property, defaults);
|
||||
} else {
|
||||
throw new Error(`Accessing invalid config property: ${property}`);
|
||||
return defaults;
|
||||
}
|
||||
}
|
||||
|
||||
set(property, value) {
|
||||
return _.set(this._values, property, value);
|
||||
}
|
||||
}
|
||||
|
||||
return MapsConfig;
|
||||
}
|
|
@ -1,52 +0,0 @@
|
|||
import d3 from 'd3';
|
||||
define(function () {
|
||||
return function ChartSplitFactory() {
|
||||
|
||||
/*
|
||||
* Adds div DOM elements to the `.chart-wrapper` element based on the data layout.
|
||||
* For example, if the data has rows, it returns the same number of
|
||||
* `.chart` elements as row objects.
|
||||
*/
|
||||
return function split(selection) {
|
||||
selection.each(function (data) {
|
||||
const div = d3.select(this)
|
||||
.attr('class', function () {
|
||||
// Determine the parent class
|
||||
if (data.rows) {
|
||||
return 'chart-wrapper-row';
|
||||
} else if (data.columns) {
|
||||
return 'chart-wrapper-column';
|
||||
} else {
|
||||
return 'chart-wrapper';
|
||||
}
|
||||
});
|
||||
let divClass;
|
||||
|
||||
const charts = div.selectAll('charts')
|
||||
.append('div')
|
||||
.data(function (d) {
|
||||
// Determine the child class
|
||||
if (d.rows) {
|
||||
divClass = 'chart-row';
|
||||
return d.rows;
|
||||
} else if (d.columns) {
|
||||
divClass = 'chart-column';
|
||||
return d.columns;
|
||||
} else {
|
||||
divClass = 'chart';
|
||||
return [d];
|
||||
}
|
||||
})
|
||||
.enter()
|
||||
.append('div')
|
||||
.attr('class', function () {
|
||||
return divClass;
|
||||
});
|
||||
|
||||
if (!data.geoJson) {
|
||||
charts.call(split);
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
});
|
|
@ -1,7 +1,6 @@
|
|||
import uiModules from 'ui/modules';
|
||||
import _ from 'lodash';
|
||||
import marked from 'marked';
|
||||
import uiRoutes from 'ui/routes';
|
||||
import { modifyUrl } from 'ui/url';
|
||||
|
||||
marked.setOptions({
|
||||
|
@ -9,16 +8,6 @@ marked.setOptions({
|
|||
sanitize: true // Sanitize HTML tags
|
||||
});
|
||||
|
||||
/**
|
||||
* Reloads the setting for each route,
|
||||
* This is to ensure, that if the license changed during the lifecycle of the application,
|
||||
* we get an update.
|
||||
* tilemapSettings itself will take care that the manifest-service is not queried when not necessary.
|
||||
*/
|
||||
uiRoutes.afterSetupWork(function (tilemapSettings) {
|
||||
return tilemapSettings.loadSettings();
|
||||
});
|
||||
|
||||
uiModules.get('kibana')
|
||||
.service('tilemapSettings', function ($http, tilemapsConfig, $sanitize, kbnVersion) {
|
||||
const attributionFromConfig = $sanitize(marked(tilemapsConfig.deprecated.config.options.attribution || ''));
|
||||
|
@ -190,6 +179,10 @@ uiModules.get('kibana')
|
|||
|
||||
}
|
||||
|
||||
isInitialized() {
|
||||
return this._settingsInitialized;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks if there was an error during initialization of the parameters
|
||||
|
|
|
@ -1,123 +0,0 @@
|
|||
import _ from 'lodash';
|
||||
import $ from 'jquery';
|
||||
import d3 from 'd3';
|
||||
import MapsConfigProvider from './lib/maps_config';
|
||||
import TileMapChartProvider from './visualizations/tile_map';
|
||||
import EventsProvider from 'ui/events';
|
||||
import MapsDataProvider from './lib/data';
|
||||
import LayoutProvider from './lib/layout';
|
||||
import './styles/_tilemap.less';
|
||||
|
||||
export default function MapsFactory(Private) {
|
||||
const Events = Private(EventsProvider);
|
||||
const MapsConfig = Private(MapsConfigProvider);
|
||||
const TileMapChart = Private(TileMapChartProvider);
|
||||
const Data = Private(MapsDataProvider);
|
||||
const Layout = Private(LayoutProvider);
|
||||
|
||||
class Maps extends Events {
|
||||
constructor($el, vis, mapsConfigArgs) {
|
||||
super(arguments);
|
||||
this.el = $el.get ? $el.get(0) : $el;
|
||||
this.vis = vis;
|
||||
this.mapsConfigArgs = mapsConfigArgs;
|
||||
|
||||
// memoize so that the same function is returned every time,
|
||||
// allowing us to remove/re-add the same function
|
||||
this.getProxyHandler = _.memoize(function (event) {
|
||||
const self = this;
|
||||
return function (e) {
|
||||
self.emit(event, e);
|
||||
};
|
||||
});
|
||||
|
||||
this.enable = this.chartEventProxyToggle('on');
|
||||
this.disable = this.chartEventProxyToggle('off');
|
||||
}
|
||||
|
||||
chartEventProxyToggle(method) {
|
||||
return function (event, chart) {
|
||||
const proxyHandler = this.getProxyHandler(event);
|
||||
|
||||
_.each(chart ? [chart] : this.charts, function (chart) {
|
||||
chart.events[method](event, proxyHandler);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
on(event, listener) {
|
||||
const first = this.listenerCount(event) === 0;
|
||||
const ret = Events.prototype.on.call(this, event, listener);
|
||||
const added = this.listenerCount(event) > 0;
|
||||
|
||||
// if this is the first listener added for the event
|
||||
// enable the event in the handler
|
||||
if (first && added && this.handler) this.handler.enable(event);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
off(event, listener) {
|
||||
const last = this.listenerCount(event) === 1;
|
||||
const ret = Events.prototype.off.call(this, event, listener);
|
||||
const removed = this.listenerCount(event) === 0;
|
||||
|
||||
// Once all listeners are removed, disable the events in the handler
|
||||
if (last && removed && this.handler) this.handler.disable(event);
|
||||
return ret;
|
||||
}
|
||||
|
||||
render(data, uiState) {
|
||||
if (!data) {
|
||||
throw new Error('No valid data!');
|
||||
}
|
||||
|
||||
this.uiState = uiState;
|
||||
this.data = new Data(data, this.uiState);
|
||||
this.visConfig = new MapsConfig(this.mapsConfigArgs, this.data, this.uiState);
|
||||
this.layout = new Layout(this.el, this.visConfig, this.data);
|
||||
this.draw();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.charts.forEach(chart => chart.destroy());
|
||||
d3.select(this.el).selectAll('*').remove();
|
||||
}
|
||||
|
||||
draw() {
|
||||
// Destroy the charts before they get removed from the DOM on the new
|
||||
// layout render.
|
||||
if(this.charts !== undefined) {
|
||||
this.charts.forEach(chart => chart.destroy());
|
||||
}
|
||||
|
||||
this.layout.render();
|
||||
const self = this;
|
||||
this.charts = [];
|
||||
|
||||
|
||||
let loadedCount = 0;
|
||||
const chartSelection = d3.select(this.el).selectAll('.chart');
|
||||
chartSelection.each(function (chartData) {
|
||||
const chart = new TileMapChart(self, this, chartData);
|
||||
|
||||
self.activeEvents().forEach(function (event) {
|
||||
self.enable(event, chart);
|
||||
});
|
||||
|
||||
self.charts.push(chart);
|
||||
chart.render();
|
||||
|
||||
chart.events.on('rendered', function () {
|
||||
loadedCount++;
|
||||
if (loadedCount === chartSelection.length) {
|
||||
$(self.el).trigger('renderComplete');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return Maps;
|
||||
}
|
|
@ -1,86 +1,237 @@
|
|||
import $ from 'jquery';
|
||||
import _ from 'lodash';
|
||||
import MapsProvider from 'ui/vis_maps/maps';
|
||||
import VisRenderbotProvider from 'ui/vis/renderbot';
|
||||
import MapsVisTypeBuildChartDataProvider from 'ui/vislib_vis_type/build_chart_data';
|
||||
import FilterBarPushFilterProvider from 'ui/filter_bar/push_filter';
|
||||
import KibanaMap from './kibana_map';
|
||||
import GeohashLayer from './geohash_layer';
|
||||
import './lib/tilemap_settings';
|
||||
import './styles/_tilemap.less';
|
||||
import { ResizeCheckerProvider } from 'ui/resize_checker';
|
||||
|
||||
module.exports = function MapsRenderbotFactory(Private, $injector, tilemapSettings, Notifier) {
|
||||
const AngularPromise = $injector.get('Promise');
|
||||
const Maps = Private(MapsProvider);
|
||||
|
||||
module.exports = function MapsRenderbotFactory(Private, $injector, tilemapSettings, Notifier, courier, getAppState) {
|
||||
|
||||
const ResizeChecker = Private(ResizeCheckerProvider);
|
||||
const Renderbot = Private(VisRenderbotProvider);
|
||||
const buildChartData = Private(MapsVisTypeBuildChartDataProvider);
|
||||
const notify = new Notifier({
|
||||
location: 'Tilemap'
|
||||
});
|
||||
const notify = new Notifier({ location: 'Tilemap' });
|
||||
|
||||
_.class(MapsRenderbot).inherits(Renderbot);
|
||||
function MapsRenderbot(vis, $el, uiState) {
|
||||
MapsRenderbot.Super.call(this, vis, $el, uiState);
|
||||
this._createVis();
|
||||
class MapsRenderbot extends Renderbot {
|
||||
|
||||
constructor(vis, $el, uiState) {
|
||||
super(vis, $el, uiState);
|
||||
this._buildChartData = buildChartData.bind(this);
|
||||
this._geohashLayer = null;
|
||||
this._kibanaMap = null;
|
||||
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) {
|
||||
this._kibanaMap.resize();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async _makeKibanaMap($el) {
|
||||
|
||||
if (!tilemapSettings.isInitialized()) {
|
||||
await tilemapSettings.loadSettings();
|
||||
}
|
||||
|
||||
MapsRenderbot.prototype._createVis = function () {
|
||||
if (tilemapSettings.getError()) {
|
||||
//Still allow the visualization to be build, but show a toast that there was a problem retrieving map settings
|
||||
//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);
|
||||
}
|
||||
if (this.mapsVis) this.destroy();
|
||||
this.mapsParams = this._getMapsParams();
|
||||
this.mapsVis = new Maps(this.$el[0], this.vis, this.mapsParams);
|
||||
|
||||
_.each(this.vis.listeners, (listener, event) => {
|
||||
this.mapsVis.on(event, listener);
|
||||
const containerElement = $($el)[0];
|
||||
const minMaxZoom = tilemapSettings.getMinMaxZoom(false);
|
||||
this._kibanaMap = new KibanaMap(containerElement, minMaxZoom);
|
||||
this._kibanaMap.addDrawControl();
|
||||
this._kibanaMap.addFitControl();
|
||||
this._kibanaMap.addLegendControl();
|
||||
|
||||
this._kibanaMap.persistUiStateForVisualization(this.vis);
|
||||
this._kibanaMap.useUiStateFromVisualization(this.vis);
|
||||
|
||||
let previousPrecision = this._kibanaMap.getAutoPrecision();
|
||||
let precisionChange = false;
|
||||
this._kibanaMap.on('zoomchange', () => {
|
||||
precisionChange = (previousPrecision !== this._kibanaMap.getAutoPrecision());
|
||||
previousPrecision = this._kibanaMap.getAutoPrecision();
|
||||
});
|
||||
this._kibanaMap.on('zoomend', () => {
|
||||
|
||||
const isAutoPrecision = _.get(this._chartData, 'geohashGridAgg.params.autoPrecision', true);
|
||||
if (!isAutoPrecision) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._dataDirty = true;
|
||||
if (precisionChange) {
|
||||
courier.fetch();
|
||||
} else {
|
||||
this._recreateGeohashLayer();
|
||||
this._dataDirty = false;
|
||||
this._doRenderComplete();
|
||||
}
|
||||
});
|
||||
|
||||
if (this.mapsData) {
|
||||
this.mapsVis.render(this.mapsData, this.uiState);
|
||||
|
||||
this._kibanaMap.on('drawCreated:rectangle', event => {
|
||||
addSpatialFilter(_.get(this._chartData, 'geohashGridAgg'), 'geo_bounding_box', event.bounds);
|
||||
});
|
||||
this._kibanaMap.on('baseLayer:loaded', () => {
|
||||
this._baseLayerDirty = false;
|
||||
this._doRenderComplete();
|
||||
});
|
||||
this._kibanaMap.on('baseLayer:loading', () => {
|
||||
this._baseLayerDirty = true;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
MapsRenderbot.prototype._getMapsParams = function () {
|
||||
const self = this;
|
||||
_recreateGeohashLayer() {
|
||||
if (this._geohashLayer) {
|
||||
this._kibanaMap.removeLayer(this._geohashLayer);
|
||||
}
|
||||
if (!this._geohashGeoJson) {
|
||||
return;
|
||||
}
|
||||
const geohashOptions = this._getGeohashOptions();
|
||||
this._geohashLayer = new GeohashLayer(this._chartData.geoJson, geohashOptions, this._kibanaMap.getZoomLevel(), this._kibanaMap);
|
||||
this._kibanaMap.addLayer(this._geohashLayer);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* called on data change
|
||||
* @param esResponse
|
||||
*/
|
||||
render(esResponse) {
|
||||
this._dataDirty = true;
|
||||
this._kibanaMapReady.then(() => {
|
||||
this._chartData = this._buildChartData(esResponse);
|
||||
this._geohashGeoJson = this._chartData.geoJson;
|
||||
this._recreateGeohashLayer();
|
||||
this._kibanaMap.useUiStateFromVisualization(this.vis);
|
||||
this._kibanaMap.resize();
|
||||
this._dataDirty = false;
|
||||
this._doRenderComplete();
|
||||
});
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this._kibanaMap) {
|
||||
this._kibanaMap.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* called on options change (vis.params change)
|
||||
*/
|
||||
updateParams() {
|
||||
|
||||
this._paramsDirty = true;
|
||||
this._kibanaMapReady.then(() => {
|
||||
const mapParams = this._getMapsParams();
|
||||
if (mapParams.wms.enabled) {
|
||||
const { minZoom, maxZoom } = tilemapSettings.getMinMaxZoom(true);
|
||||
this._kibanaMap.setBaseLayer({
|
||||
baseLayerType: 'wms',
|
||||
options: {
|
||||
minZoom: minZoom,
|
||||
maxZoom: maxZoom,
|
||||
url: mapParams.wms.url,
|
||||
...mapParams.wms.options
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (!tilemapSettings.hasError()) {
|
||||
const url = tilemapSettings.getUrl();
|
||||
const options = tilemapSettings.getTMSOptions();
|
||||
this._kibanaMap.setBaseLayer({
|
||||
baseLayerType: 'tms',
|
||||
options: { url, ...options }
|
||||
});
|
||||
}
|
||||
}
|
||||
const geohashOptions = this._getGeohashOptions();
|
||||
if (!this._geohashLayer || !this._geohashLayer.isReusable(geohashOptions)) {
|
||||
this._recreateGeohashLayer();
|
||||
}
|
||||
|
||||
this._kibanaMap.setDesaturateBaseLayer(mapParams.isDesaturated);
|
||||
this._kibanaMap.setShowTooltip(mapParams.addTooltip);
|
||||
this._kibanaMap.setLegendPosition(mapParams.legendPosition);
|
||||
|
||||
this._kibanaMap.useUiStateFromVisualization(this.vis);
|
||||
this._kibanaMap.resize();
|
||||
this._paramsDirty = false;
|
||||
this._doRenderComplete();
|
||||
});
|
||||
}
|
||||
|
||||
_getMapsParams() {
|
||||
return _.assign(
|
||||
{},
|
||||
self.vis.type.params.defaults,
|
||||
this.vis.type.params.defaults,
|
||||
{
|
||||
type: self.vis.type.name,
|
||||
// Add attribute which determines whether an index is time based or not.
|
||||
hasTimeField: self.vis.indexPattern && self.vis.indexPattern.hasTimeField()
|
||||
type: this.vis.type.name,
|
||||
hasTimeField: this.vis.indexPattern && this.vis.indexPattern.hasTimeField()// Add attribute which determines whether an index is time based or not.
|
||||
},
|
||||
self.vis.params
|
||||
this.vis.params
|
||||
);
|
||||
}
|
||||
|
||||
_getGeohashOptions() {
|
||||
const newParams = this._getMapsParams();
|
||||
return {
|
||||
valueFormatter: this._chartData ? this._chartData.valueFormatter : null,
|
||||
tooltipFormatter: this._chartData ? this._chartData.tooltipFormatter : null,
|
||||
mapType: newParams.mapType,
|
||||
heatmap: {
|
||||
heatBlur: newParams.heatBlur,
|
||||
heatMaxZoom: newParams.heatMaxZoom,
|
||||
heatMinOpacity: newParams.heatMinOpacity,
|
||||
heatNormalizeData: newParams.heatNormalizeData,
|
||||
heatRadius: newParams.heatRadius
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
MapsRenderbot.prototype.buildChartData = buildChartData;
|
||||
MapsRenderbot.prototype.render = function (esResponse) {
|
||||
this.mapsData = this.buildChartData(esResponse);
|
||||
return AngularPromise.delay(1).then(() => {
|
||||
this.mapsVis.render(this.mapsData, this.uiState);
|
||||
});
|
||||
};
|
||||
_doRenderComplete() {
|
||||
if (this._paramsDirty || this._dataDirty || this._baseLayerDirty) {
|
||||
return;
|
||||
}
|
||||
$(this.el).trigger('renderComplete');
|
||||
}
|
||||
|
||||
MapsRenderbot.prototype.destroy = function () {
|
||||
const self = this;
|
||||
}
|
||||
|
||||
const mapsVis = self.mapsVis;
|
||||
function addSpatialFilter(agg, filterName, filterData) {
|
||||
if (!agg) {
|
||||
return;
|
||||
}
|
||||
|
||||
_.forOwn(self.vis.listeners, function (listener, event) {
|
||||
mapsVis.off(event, listener);
|
||||
});
|
||||
const indexPatternName = agg.vis.indexPattern.id;
|
||||
const field = agg.fieldName();
|
||||
const filter = {};
|
||||
filter[filterName] = {};
|
||||
filter[filterName][field] = filterData;
|
||||
|
||||
mapsVis.destroy();
|
||||
};
|
||||
const putFilter = Private(FilterBarPushFilterProvider)(getAppState());
|
||||
return putFilter(filter, false, indexPatternName);
|
||||
}
|
||||
|
||||
MapsRenderbot.prototype.updateParams = function () {
|
||||
const self = this;
|
||||
|
||||
// get full maps params object
|
||||
const newParams = self._getMapsParams();
|
||||
|
||||
// if there's been a change, replace the vis
|
||||
if (!_.isEqual(newParams, self.mapsParams)) self._createVis();
|
||||
};
|
||||
|
||||
return MapsRenderbot;
|
||||
};
|
||||
|
||||
|
||||
|
|
17
src/ui/public/vis_maps/markers/geohash_grid.js
Normal file
17
src/ui/public/vis_maps/markers/geohash_grid.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
import L from 'leaflet';
|
||||
import ScaledCircles from './scaled_circles';
|
||||
|
||||
export default class GeohashGrid extends ScaledCircles {
|
||||
getMarkerFunction() {
|
||||
return function (feature) {
|
||||
const geohashRect = feature.properties.rectangle;
|
||||
// get bounds from northEast[3] and southWest[1]
|
||||
// corners in geohash rectangle
|
||||
const corners = [
|
||||
[geohashRect[3][0], geohashRect[3][1]],
|
||||
[geohashRect[1][0], geohashRect[1][1]]
|
||||
];
|
||||
return L.rectangle(corners);
|
||||
};
|
||||
}
|
||||
}
|
197
src/ui/public/vis_maps/markers/heatmap.js
Normal file
197
src/ui/public/vis_maps/markers/heatmap.js
Normal file
|
@ -0,0 +1,197 @@
|
|||
import L from 'leaflet';
|
||||
import _ from 'lodash';
|
||||
import d3 from 'd3';
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
/**
|
||||
* Map overlay: canvas layer with leaflet.heat plugin
|
||||
*
|
||||
* @param map {Leaflet Object}
|
||||
* @param geoJson {geoJson Object}
|
||||
* @param params {Object}
|
||||
*/
|
||||
export default class Heatmap extends EventEmitter {
|
||||
|
||||
constructor(featureCollection, options, zoom) {
|
||||
|
||||
super();
|
||||
this._geojsonFeatureCollection = featureCollection;
|
||||
const max = _.get(featureCollection, 'properties.max');
|
||||
const points = dataToHeatArray(max, options.heatNormalizeData, featureCollection);
|
||||
this._leafletLayer = L.heatLayer(points, options);
|
||||
this._tooltipFormatter = options.tooltipFormatter;
|
||||
this._zoom = zoom;
|
||||
this._disableTooltips = false;
|
||||
this._getLatLng = _.memoize(function (feature) {
|
||||
return L.latLng(
|
||||
feature.geometry.coordinates[1],
|
||||
feature.geometry.coordinates[0]
|
||||
);
|
||||
}, function (feature) {
|
||||
// turn coords into a string for the memoize cache
|
||||
return [feature.geometry.coordinates[1], feature.geometry.coordinates[0]].join(',');
|
||||
});
|
||||
|
||||
this._currentFeature = null;
|
||||
this._addTooltips();
|
||||
}
|
||||
|
||||
getBounds() {
|
||||
return this._leafletLayer.getBounds();
|
||||
}
|
||||
|
||||
getLeafletLayer() {
|
||||
return this._leafletLayer;
|
||||
}
|
||||
|
||||
appendLegendContents() {
|
||||
}
|
||||
|
||||
|
||||
movePointer(type, event) {
|
||||
if (type === 'mousemove') {
|
||||
this._deboundsMoveMoveLocation(event);
|
||||
} else if (type === 'mouseout') {
|
||||
this.emit('hideTooltip');
|
||||
} else if (type === 'mousedown') {
|
||||
this._disableTooltips = true;
|
||||
this.emit('hideTooltip');
|
||||
} else if (type === 'mouseup') {
|
||||
this._disableTooltips = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
_addTooltips() {
|
||||
|
||||
const mouseMoveLocation = (e) => {
|
||||
|
||||
|
||||
|
||||
if (!this._geojsonFeatureCollection.features.length || this._disableTooltips) {
|
||||
this.emit('hideTooltip');
|
||||
return;
|
||||
}
|
||||
|
||||
const feature = this._nearestFeature(e.latlng);
|
||||
if (this._tooltipProximity(e.latlng, feature)) {
|
||||
const content = this._tooltipFormatter(feature);
|
||||
if (!content) {
|
||||
return;
|
||||
}
|
||||
this.emit('showTooltip', {
|
||||
content: content,
|
||||
position: e.latlng
|
||||
});
|
||||
} else { this.emit('hideTooltip');
|
||||
}
|
||||
};
|
||||
|
||||
this._deboundsMoveMoveLocation = _.debounce(mouseMoveLocation.bind(this), 15, {
|
||||
'leading': true,
|
||||
'trailing': false
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds nearest feature in mapData to event latlng
|
||||
*
|
||||
* @method _nearestFeature
|
||||
* @param latLng {Leaflet latLng}
|
||||
* @return nearestPoint {Leaflet latLng}
|
||||
*/
|
||||
_nearestFeature(latLng) {
|
||||
const self = this;
|
||||
let nearest;
|
||||
|
||||
if (latLng.lng < -180 || latLng.lng > 180) {
|
||||
return;
|
||||
}
|
||||
|
||||
_.reduce(this._geojsonFeatureCollection.features, function (distance, feature) {
|
||||
const featureLatLng = self._getLatLng(feature);
|
||||
const dist = latLng.distanceTo(featureLatLng);
|
||||
|
||||
if (dist < distance) {
|
||||
nearest = feature;
|
||||
return dist;
|
||||
}
|
||||
|
||||
return distance;
|
||||
}, Infinity);
|
||||
|
||||
return nearest;
|
||||
}
|
||||
|
||||
/**
|
||||
* display tooltip if feature is close enough to event latlng
|
||||
*
|
||||
* @method _tooltipProximity
|
||||
* @param latlng {Leaflet latLng Object}
|
||||
* @param feature {geoJson Object}
|
||||
* @return {Boolean}
|
||||
*/
|
||||
_tooltipProximity(latlng, feature) {
|
||||
if (!feature) return;
|
||||
|
||||
let showTip = false;
|
||||
const featureLatLng = this._getLatLng(feature);
|
||||
|
||||
// zoomScale takes map zoom and returns proximity value for tooltip display
|
||||
// domain (input values) is map zoom (min 1 and max 18)
|
||||
// range (output values) is distance in meters
|
||||
// used to compare proximity of event latlng to feature latlng
|
||||
const zoomScale = d3.scale.linear()
|
||||
.domain([1, 4, 7, 10, 13, 16, 18])
|
||||
.range([1000000, 300000, 100000, 15000, 2000, 150, 50]);
|
||||
|
||||
const proximity = zoomScale(this._zoom);
|
||||
const distance = latlng.distanceTo(featureLatLng);
|
||||
|
||||
// maxLngDif is max difference in longitudes
|
||||
// to prevent feature tooltip from appearing 360°
|
||||
// away from event latlng
|
||||
const maxLngDif = 40;
|
||||
const lngDif = Math.abs(latlng.lng - featureLatLng.lng);
|
||||
|
||||
if (distance < proximity && lngDif < maxLngDif) {
|
||||
showTip = true;
|
||||
}
|
||||
|
||||
d3.scale.pow().exponent(0.2)
|
||||
.domain([1, 18])
|
||||
.range([1500000, 50]);
|
||||
return showTip;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* returns data for data for heat map intensity
|
||||
* if heatNormalizeData attribute is checked/true
|
||||
• normalizes data for heat map intensity
|
||||
*
|
||||
* @method _dataToHeatArray
|
||||
* @param max {Number}
|
||||
* @return {Array}
|
||||
*/
|
||||
function dataToHeatArray(max, heatNormalizeData, featureCollection) {
|
||||
|
||||
return featureCollection.features.map((feature) => {
|
||||
const lat = feature.geometry.coordinates[1];
|
||||
const lng = feature.geometry.coordinates[0];
|
||||
let heatIntensity;
|
||||
if (!heatNormalizeData) {
|
||||
// show bucket value on heatmap
|
||||
heatIntensity = feature.properties.value;
|
||||
} else {
|
||||
// show bucket value normalized to max value
|
||||
heatIntensity = feature.properties.value / max;
|
||||
}
|
||||
|
||||
return [lat, lng, heatIntensity];
|
||||
});
|
||||
}
|
||||
|
231
src/ui/public/vis_maps/markers/scaled_circles.js
Normal file
231
src/ui/public/vis_maps/markers/scaled_circles.js
Normal file
|
@ -0,0 +1,231 @@
|
|||
import L from 'leaflet';
|
||||
import _ from 'lodash';
|
||||
import d3 from 'd3';
|
||||
import $ from 'jquery';
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
export default class ScaledCircles extends EventEmitter {
|
||||
|
||||
constructor(featureCollection, options, targetZoom, kibanaMap) {
|
||||
super();
|
||||
this._geohashGeoJson = featureCollection;
|
||||
this._zoom = targetZoom;
|
||||
|
||||
this._valueFormatter = options.valueFormatter;
|
||||
this._tooltipFormatter = options.tooltipFormatter;
|
||||
this._map = options.map;
|
||||
|
||||
this._legendColors = null;
|
||||
this._legendQuantizer = null;
|
||||
|
||||
this._popups = [];
|
||||
this._leafletLayer = L.geoJson(null, {
|
||||
pointToLayer: this.getMarkerFunction(),
|
||||
style: this.getStyleFunction(),
|
||||
onEachFeature: (feature, layer) => {
|
||||
this._bindPopup(feature, layer);
|
||||
},
|
||||
filter: (feature) => {
|
||||
const bucketRectBounds = _.get(feature, 'properties.rectangle');
|
||||
return kibanaMap.isInside(bucketRectBounds);
|
||||
}
|
||||
});
|
||||
this._leafletLayer.addData(this._geohashGeoJson);
|
||||
}
|
||||
|
||||
getLeafletLayer() {
|
||||
return this._leafletLayer;
|
||||
}
|
||||
|
||||
|
||||
getStyleFunction() {
|
||||
const min = _.get(this._geohashGeoJson, 'properties.min', 0);
|
||||
const max = _.get(this._geohashGeoJson, 'properties.max', 1);
|
||||
|
||||
const quantizeDomain = (min !== max) ? [min, max] : d3.scale.quantize().domain();
|
||||
this._legendColors = makeCircleMarkerLegendColors(min, max);
|
||||
this._legendQuantizer = d3.scale.quantize().domain(quantizeDomain).range(this._legendColors);
|
||||
|
||||
return makeStyleFunction(min, max, this._legendColors, quantizeDomain);
|
||||
}
|
||||
|
||||
|
||||
movePointer() {
|
||||
}
|
||||
|
||||
getLabel() {
|
||||
if (this._popups.length) {
|
||||
return this._popups[0].feature.properties.aggConfigResult.aggConfig.makeLabel();
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
|
||||
appendLegendContents(jqueryDiv) {
|
||||
|
||||
if (!this._legendColors || !this._legendQuantizer) {
|
||||
return;
|
||||
}
|
||||
|
||||
const titleText = this.getLabel();
|
||||
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);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Binds popup and events to each feature on map
|
||||
*
|
||||
* @method bindPopup
|
||||
* @param feature {Object}
|
||||
* @param layer {Object}
|
||||
* return {undefined}
|
||||
*/
|
||||
_bindPopup(feature, layer) {
|
||||
const popup = layer.on({
|
||||
mouseover: (e) => {
|
||||
const layer = e.target;
|
||||
// bring layer to front if not older browser
|
||||
if (!L.Browser.ie && !L.Browser.opera) {
|
||||
layer.bringToFront();
|
||||
}
|
||||
this._showTooltip(feature);
|
||||
},
|
||||
mouseout: () => {
|
||||
this.emit('hideTooltip');
|
||||
}
|
||||
});
|
||||
|
||||
this._popups.push(popup);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if event latlng is within bounds of mapData
|
||||
* features and shows tooltip for that feature
|
||||
*
|
||||
* @method _showTooltip
|
||||
* @param feature {LeafletFeature}
|
||||
* @param latLng? {Leaflet latLng}
|
||||
* @return undefined
|
||||
*/
|
||||
_showTooltip(feature, latLng) {
|
||||
|
||||
const lat = _.get(feature, 'geometry.coordinates.1');
|
||||
const lng = _.get(feature, 'geometry.coordinates.0');
|
||||
latLng = latLng || L.latLng(lat, lng);
|
||||
|
||||
const content = this._tooltipFormatter(feature);
|
||||
if (!content) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.emit('showTooltip', {
|
||||
content: content,
|
||||
position: latLng
|
||||
});
|
||||
}
|
||||
|
||||
getMarkerFunction() {
|
||||
const scaleFactor = 0.6;
|
||||
return (feature, latlng) => {
|
||||
const value = feature.properties.value;
|
||||
const scaledRadius = this._radiusScale(value) * scaleFactor;
|
||||
return L.circleMarker(latlng).setRadius(scaledRadius);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* radiusScale returns a number for scaled circle markers
|
||||
* for relative sizing of markers
|
||||
*
|
||||
* @method _radiusScale
|
||||
* @param value {Number}
|
||||
* @return {Number}
|
||||
*/
|
||||
_radiusScale(value) {
|
||||
|
||||
//magic numbers
|
||||
const precisionBiasBase = 5;
|
||||
const precisionBiasNumerator = 200;
|
||||
|
||||
const precision = _.max(this._geohashGeoJson.features.map((feature) => {
|
||||
return String(feature.properties.geohash).length;
|
||||
}));
|
||||
|
||||
const pct = Math.abs(value) / Math.abs(this._geohashGeoJson.properties.max);
|
||||
const zoomRadius = 0.5 * Math.pow(2, this._zoom);
|
||||
const precisionScale = precisionBiasNumerator / Math.pow(precisionBiasBase, precision);
|
||||
|
||||
// square root value percentage
|
||||
return Math.pow(pct, 0.5) * zoomRadius * precisionScale;
|
||||
}
|
||||
|
||||
getBounds() {
|
||||
return this._leafletLayer.getBounds();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* d3 quantize scale returns a hex color, used for marker fill color
|
||||
*
|
||||
* @method quantizeLegendColors
|
||||
* return {undefined}
|
||||
*/
|
||||
function makeCircleMarkerLegendColors(min, max) {
|
||||
const reds1 = ['#ff6128'];
|
||||
const reds3 = ['#fecc5c', '#fd8d3c', '#e31a1c'];
|
||||
const reds5 = ['#fed976', '#feb24c', '#fd8d3c', '#f03b20', '#bd0026'];
|
||||
const bottomCutoff = 2;
|
||||
const middleCutoff = 24;
|
||||
let legendColors;
|
||||
if (max - min <= bottomCutoff) {
|
||||
legendColors = reds1;
|
||||
} else if (max - min <= middleCutoff) {
|
||||
legendColors = reds3;
|
||||
} else {
|
||||
legendColors = reds5;
|
||||
}
|
||||
return legendColors;
|
||||
}
|
||||
|
||||
function makeColorDarker(color) {
|
||||
const amount = 1.3;//magic number, carry over from earlier
|
||||
return d3.hcl(color).darker(amount).toString();
|
||||
}
|
||||
|
||||
function makeStyleFunction(min, max, legendColors, quantizeDomain) {
|
||||
const legendQuantizer = d3.scale.quantize().domain(quantizeDomain).range(legendColors);
|
||||
return (feature) => {
|
||||
const value = _.get(feature, 'properties.value');
|
||||
const color = legendQuantizer(value);
|
||||
return {
|
||||
fillColor: color,
|
||||
color: makeColorDarker(color),
|
||||
weight: 1.5,
|
||||
opacity: 1,
|
||||
fillOpacity: 0.75
|
||||
};
|
||||
};
|
||||
}
|
46
src/ui/public/vis_maps/markers/shaded_circles.js
Normal file
46
src/ui/public/vis_maps/markers/shaded_circles.js
Normal file
|
@ -0,0 +1,46 @@
|
|||
import L from 'leaflet';
|
||||
import _ from 'lodash';
|
||||
import ScaledCircles from './scaled_circles';
|
||||
|
||||
export default class ShadedCircles extends ScaledCircles {
|
||||
getMarkerFunction() {
|
||||
// multiplier to reduce size of all circles
|
||||
const scaleFactor = 0.8;
|
||||
return (feature, latlng) => {
|
||||
const radius = this._geohashMinDistance(feature) * scaleFactor;
|
||||
return L.circle(latlng, radius);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* _geohashMinDistance returns a min distance in meters for sizing
|
||||
* circle markers to fit within geohash grid rectangle
|
||||
*
|
||||
* @method _geohashMinDistance
|
||||
* @param feature {Object}
|
||||
* @return {Number}
|
||||
*/
|
||||
_geohashMinDistance(feature) {
|
||||
const centerPoint = _.get(feature, 'properties.center');
|
||||
const geohashRect = _.get(feature, 'properties.rectangle');
|
||||
|
||||
// centerPoint is an array of [lat, lng]
|
||||
// geohashRect is the 4 corners of the geoHash rectangle
|
||||
// an array that starts at the southwest corner and proceeds
|
||||
// clockwise, each value being an array of [lat, lng]
|
||||
|
||||
// center lat and southeast lng
|
||||
const east = L.latLng([centerPoint[0], geohashRect[2][1]]);
|
||||
// southwest lat and center lng
|
||||
const north = L.latLng([geohashRect[3][0], centerPoint[1]]);
|
||||
|
||||
// get latLng of geohash center point
|
||||
const center = L.latLng([centerPoint[0], centerPoint[1]]);
|
||||
|
||||
// get smallest radius at center of geohash grid rectangle
|
||||
const eastRadius = Math.floor(center.distanceTo(east));
|
||||
const northRadius = Math.floor(center.distanceTo(north));
|
||||
return _.min([eastRadius, northRadius]);
|
||||
}
|
||||
}
|
|
@ -1,59 +0,0 @@
|
|||
import d3 from 'd3';
|
||||
import VislibLibDispatchProvider from '../lib/dispatch';
|
||||
import TooltipProvider from 'ui/vis/components/tooltip';
|
||||
export default function ChartBaseClass(Private) {
|
||||
|
||||
const Dispatch = Private(VislibLibDispatchProvider);
|
||||
const Tooltip = Private(TooltipProvider);
|
||||
/**
|
||||
* The Base Class for all visualizations.
|
||||
*
|
||||
* @class Chart
|
||||
* @constructor
|
||||
* @param handler {Object} Reference to the Handler Class Constructor
|
||||
* @param el {HTMLElement} HTML element to which the chart will be appended
|
||||
* @param chartData {Object} Elasticsearch query results for this specific chart
|
||||
*/
|
||||
class Chart {
|
||||
constructor(handler, el, chartData) {
|
||||
this.handler = handler;
|
||||
this.chartEl = el;
|
||||
this.chartData = chartData;
|
||||
this.tooltips = [];
|
||||
|
||||
const events = this.events = new Dispatch(handler);
|
||||
|
||||
if (this.handler.visConfig && this.handler.visConfig.get('addTooltip', false)) {
|
||||
const $el = this.handler.el;
|
||||
const formatter = this.handler.data.get('tooltipFormatter');
|
||||
|
||||
// Add tooltip
|
||||
this.tooltip = new Tooltip('chart', $el, formatter, events);
|
||||
this.tooltips.push(this.tooltip);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const selection = d3.select(this.chartEl);
|
||||
selection.selectAll('*').remove();
|
||||
selection.call(this.draw());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Removes all DOM elements from the root element
|
||||
*
|
||||
* @method destroy
|
||||
*/
|
||||
destroy() {
|
||||
const selection = d3.select(this.chartEl);
|
||||
this.events.removeAllListeners();
|
||||
this.tooltips.forEach(function (tooltip) {
|
||||
tooltip.destroy();
|
||||
});
|
||||
selection.remove();
|
||||
}
|
||||
}
|
||||
|
||||
return Chart;
|
||||
}
|
|
@ -1,362 +0,0 @@
|
|||
import _ from 'lodash';
|
||||
import $ from 'jquery';
|
||||
import L from 'leaflet';
|
||||
import VislibVisualizationsMarkerTypesScaledCirclesProvider from './marker_types/scaled_circles';
|
||||
import VislibVisualizationsMarkerTypesShadedCirclesProvider from './marker_types/shaded_circles';
|
||||
import VislibVisualizationsMarkerTypesGeohashGridProvider from './marker_types/geohash_grid';
|
||||
import VislibVisualizationsMarkerTypesHeatmapProvider from './marker_types/heatmap';
|
||||
import '../lib/tilemap_settings';
|
||||
|
||||
export default function MapFactory(Private, tilemapSettings) {
|
||||
const defaultMapZoom = 2;
|
||||
const defaultMapCenter = [15, 5];
|
||||
const defaultMarkerType = 'Scaled Circle Markers';
|
||||
|
||||
const markerTypes = {
|
||||
'Scaled Circle Markers': Private(VislibVisualizationsMarkerTypesScaledCirclesProvider),
|
||||
'Shaded Circle Markers': Private(VislibVisualizationsMarkerTypesShadedCirclesProvider),
|
||||
'Shaded Geohash Grid': Private(VislibVisualizationsMarkerTypesGeohashGridProvider),
|
||||
'Heatmap': Private(VislibVisualizationsMarkerTypesHeatmapProvider),
|
||||
};
|
||||
|
||||
/**
|
||||
* Tile Map Maps
|
||||
*
|
||||
* @class Map
|
||||
* @constructor
|
||||
* @param container {HTML Element} Element to render map into
|
||||
* @param chartData {Object} Elasticsearch query results for this map
|
||||
* @param params {Object} Parameters used to build a map
|
||||
*/
|
||||
class TileMapMap {
|
||||
constructor(container, chartData, params) {
|
||||
|
||||
this._container = $(container).get(0);
|
||||
this._chartData = chartData;
|
||||
|
||||
// keep a reference to all of the optional params
|
||||
this._events = _.get(params, 'events');
|
||||
this._markerType = markerTypes[params.markerType] ? params.markerType : defaultMarkerType;
|
||||
this._valueFormatter = params.valueFormatter || _.identity;
|
||||
this._tooltipFormatter = params.tooltipFormatter;
|
||||
this._geoJson = _.get(this._chartData, 'geoJson');
|
||||
this._attr = params.attr || {};
|
||||
|
||||
const { minZoom, maxZoom } = tilemapSettings.getMinMaxZoom(this._isWMSEnabled());
|
||||
const zoom = typeof params.zoom === 'number' ? params.zoom : defaultMapZoom;
|
||||
this._mapZoom = Math.max(Math.min(zoom, maxZoom), minZoom);
|
||||
this._mapCenter = params.center || defaultMapCenter;
|
||||
this._createMap();
|
||||
}
|
||||
|
||||
addBoundingControl() {
|
||||
if (this._boundingControl) return;
|
||||
|
||||
const self = this;
|
||||
const drawOptions = { draw: {} };
|
||||
|
||||
_.each(['polyline', 'polygon', 'circle', 'marker', 'rectangle'], function (drawShape) {
|
||||
if (self._events && !self._events.listenerCount(drawShape)) {
|
||||
drawOptions.draw[drawShape] = false;
|
||||
} else {
|
||||
drawOptions.draw[drawShape] = {
|
||||
shapeOptions: {
|
||||
stroke: false,
|
||||
color: '#000'
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
this._boundingControl = new L.Control.Draw(drawOptions);
|
||||
this.map.addControl(this._boundingControl);
|
||||
}
|
||||
|
||||
addFitControl() {
|
||||
if (this._fitControl) return;
|
||||
|
||||
const self = this;
|
||||
const fitContainer = L.DomUtil.create('div', 'leaflet-control leaflet-bar leaflet-control-fit');
|
||||
|
||||
// Add button to fit container to points
|
||||
const FitControl = L.Control.extend({
|
||||
options: {
|
||||
position: 'topleft'
|
||||
},
|
||||
onAdd: function () {
|
||||
$(fitContainer).html('<a class="fa fa-crop" href="#" title="Fit Data Bounds"></a>')
|
||||
.on('click', function (e) {
|
||||
e.preventDefault();
|
||||
self._fitBounds();
|
||||
});
|
||||
|
||||
return fitContainer;
|
||||
},
|
||||
onRemove: function () {
|
||||
$(fitContainer).off('click');
|
||||
}
|
||||
});
|
||||
|
||||
this._fitControl = new FitControl();
|
||||
this.map.addControl(this._fitControl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds label div to each map when data is split
|
||||
*
|
||||
* @method addTitle
|
||||
* @param mapLabel {String}
|
||||
* @return {undefined}
|
||||
*/
|
||||
addTitle(mapLabel) {
|
||||
if (this._label) return;
|
||||
|
||||
const label = this._label = L.control();
|
||||
|
||||
label.onAdd = function () {
|
||||
this._div = L.DomUtil.create('div', 'tilemap-info tilemap-label');
|
||||
this.update();
|
||||
return this._div;
|
||||
};
|
||||
label.update = function () {
|
||||
this._div.innerHTML = '<h2>' + _.escape(mapLabel) + '</h2>';
|
||||
};
|
||||
|
||||
// label.addTo(this.map);
|
||||
this.map.addControl(label);
|
||||
}
|
||||
|
||||
/**
|
||||
* remove css class for desat filters on map tiles
|
||||
*
|
||||
* @method saturateTiles
|
||||
* @return undefined
|
||||
*/
|
||||
saturateTiles() {
|
||||
if (!this._attr.isDesaturated) {
|
||||
$('img.leaflet-tile-loaded').addClass('filters-off');
|
||||
}
|
||||
}
|
||||
|
||||
updateSize() {
|
||||
this.map.invalidateSize({
|
||||
debounceMoveend: true
|
||||
});
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this._label) this._label.removeFrom(this.map);
|
||||
if (this._fitControl) this._fitControl.removeFrom(this.map);
|
||||
if (this._boundingControl) this._boundingControl.removeFrom(this.map);
|
||||
if (this._markers) this._markers.destroy();
|
||||
this.map.remove();
|
||||
this.map = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch type of data overlay for map:
|
||||
* creates featurelayer from mapData (geoJson)
|
||||
*
|
||||
* @method _addMarkers
|
||||
*/
|
||||
_addMarkers() {
|
||||
if (!this._geoJson) return;
|
||||
if (this._markers) this._markers.destroy();
|
||||
|
||||
this._markers = this._createMarkers({
|
||||
tooltipFormatter: this._tooltipFormatter,
|
||||
valueFormatter: this._valueFormatter,
|
||||
attr: this._attr
|
||||
});
|
||||
|
||||
if (this._geoJson.features.length > 1) {
|
||||
this._markers.addLegend();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the marker instance using the given options
|
||||
*
|
||||
* @method _createMarkers
|
||||
* @param options {Object} options to give to marker class
|
||||
* @return {Object} marker layer
|
||||
*/
|
||||
_createMarkers(options) {
|
||||
const MarkerType = markerTypes[this._markerType];
|
||||
return new MarkerType(this.map, this._geoJson, options);
|
||||
}
|
||||
|
||||
_attachEvents() {
|
||||
const self = this;
|
||||
const saturateTiles = self.saturateTiles.bind(self);
|
||||
|
||||
this._tileLayer.on('tileload', saturateTiles);
|
||||
this._tileLayer.on('load', () => {
|
||||
|
||||
if (!self._events) {
|
||||
return;
|
||||
}
|
||||
|
||||
self._events.emit('rendered', {
|
||||
chart: self._chartData,
|
||||
map: self.map,
|
||||
center: self._mapCenter,
|
||||
zoom: self._mapZoom,
|
||||
});
|
||||
});
|
||||
|
||||
this.map.on('unload', function () {
|
||||
self._tileLayer.off('tileload', saturateTiles);
|
||||
});
|
||||
|
||||
this.map.on('moveend', function setZoomCenter() {
|
||||
if (!self.map) return;
|
||||
// update internal center and zoom references
|
||||
const uglyCenter = self.map.getCenter();
|
||||
self._mapCenter = [uglyCenter.lat, uglyCenter.lng];
|
||||
self._mapZoom = self.map.getZoom();
|
||||
self._addMarkers();
|
||||
|
||||
if (!self._events) return;
|
||||
|
||||
self._events.emit('mapMoveEnd', {
|
||||
chart: self._chartData,
|
||||
map: self.map,
|
||||
center: self._mapCenter,
|
||||
zoom: self._mapZoom,
|
||||
});
|
||||
});
|
||||
|
||||
this.map.on('draw:created', function (e) {
|
||||
const drawType = e.layerType;
|
||||
if (!self._events || !self._events.listenerCount(drawType)) return;
|
||||
|
||||
// TODO: Different drawTypes need differ info. Need a switch on the object creation
|
||||
const bounds = e.layer.getBounds();
|
||||
|
||||
const southEast = bounds.getSouthEast();
|
||||
const northWest = bounds.getNorthWest();
|
||||
let southEastLng = southEast.lng;
|
||||
if (southEastLng > 180) {
|
||||
southEastLng -= 360;
|
||||
}
|
||||
let northWestLng = northWest.lng;
|
||||
if (northWestLng < -180) {
|
||||
northWestLng += 360;
|
||||
}
|
||||
|
||||
const southEastLat = southEast.lat;
|
||||
const northWestLat = northWest.lat;
|
||||
|
||||
//Bounds cannot be created unless they form a box with larger than 0 dimensions
|
||||
//Invalid areas are rejected by ES.
|
||||
if (southEastLat === northWestLat || southEastLng === northWestLng) {
|
||||
return;
|
||||
}
|
||||
|
||||
self._events.emit(drawType, {
|
||||
e: e,
|
||||
chart: self._chartData,
|
||||
bounds: {
|
||||
bottom_right: {
|
||||
lat: southEastLat,
|
||||
lon: southEastLng
|
||||
},
|
||||
top_left: {
|
||||
lat: northWestLat,
|
||||
lon: northWestLng
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.map.on('zoomend', function () {
|
||||
if (!self.map) return;
|
||||
self._mapZoom = self.map.getZoom();
|
||||
if (!self._events) return;
|
||||
|
||||
self._events.emit('mapZoomEnd', {
|
||||
chart: self._chartData,
|
||||
map: self.map,
|
||||
zoom: self._mapZoom,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_isWMSEnabled() {
|
||||
return this._attr.wms ? this._attr.wms.enabled : false;
|
||||
}
|
||||
|
||||
_createTileLayer() {
|
||||
if (this._isWMSEnabled()) {
|
||||
const wmsOpts = this._attr.wms;
|
||||
const { minZoom, maxZoom } = tilemapSettings.getMinMaxZoom(true);
|
||||
// http://leafletjs.com/reference.html#tilelayer-wms-options
|
||||
return L.tileLayer.wms(wmsOpts.url, {
|
||||
// user settings
|
||||
...wmsOpts.options,
|
||||
minZoom: minZoom,
|
||||
maxZoom: maxZoom,
|
||||
});
|
||||
}
|
||||
|
||||
const tileUrl = tilemapSettings.hasError() ? '' : tilemapSettings.getUrl();
|
||||
const leafletOptions = tilemapSettings.getTMSOptions();
|
||||
return L.tileLayer(tileUrl, leafletOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the leaflet Map object. In our implementation this is basically just
|
||||
* a container for the layer created by `this._createTileLayer()`. User settings
|
||||
* are passed as options to the layer and inherited by the map so we can keep
|
||||
* this function pretty generic.
|
||||
*
|
||||
* The map is responsible for the current center and zoom level though, as those
|
||||
* are global to each map.
|
||||
*
|
||||
* @return undefined
|
||||
*/
|
||||
_createMap() {
|
||||
if (this.map) this.destroy();
|
||||
|
||||
// expose at `this._tileLayer`, `this._attachEvents()` accesses it this way
|
||||
this._tileLayer = this._createTileLayer();
|
||||
|
||||
// http://leafletjs.com/reference.html#map-options
|
||||
this.map = L.map(this._container, {
|
||||
center: this._mapCenter,
|
||||
zoom: this._mapZoom,
|
||||
layers: [this._tileLayer],
|
||||
maxBounds: L.latLngBounds([-90, -220], [90, 220]),
|
||||
scrollWheelZoom: false,
|
||||
fadeAnimation: true,
|
||||
});
|
||||
|
||||
this._attachEvents();
|
||||
this._addMarkers();
|
||||
}
|
||||
|
||||
/**
|
||||
* zoom map to fit all features in featureLayer
|
||||
*
|
||||
* @method _fitBounds
|
||||
* @param map {Leaflet Object}
|
||||
* @return {boolean}
|
||||
*/
|
||||
_fitBounds() {
|
||||
this.map.fitBounds(this._getDataRectangles());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Rectangles representing the geohash grid
|
||||
*
|
||||
* @return {LatLngRectangles[]}
|
||||
*/
|
||||
_getDataRectangles() {
|
||||
if (!this._geoJson) return [];
|
||||
return _.pluck(this._geoJson.features, 'properties.rectangle');
|
||||
}
|
||||
}
|
||||
|
||||
return TileMapMap;
|
||||
}
|
|
@ -1,286 +0,0 @@
|
|||
import d3 from 'd3';
|
||||
import _ from 'lodash';
|
||||
import $ from 'jquery';
|
||||
import L from 'leaflet';
|
||||
export default function MarkerFactory() {
|
||||
|
||||
/**
|
||||
* Base map marker overlay, all other markers inherit from this class
|
||||
*
|
||||
* @param map {Leaflet Object}
|
||||
* @param geoJson {geoJson Object}
|
||||
* @param params {Object}
|
||||
*/
|
||||
class BaseMarker {
|
||||
constructor(map, geoJson, params) {
|
||||
this.map = map;
|
||||
this.geoJson = geoJson;
|
||||
this.popups = [];
|
||||
|
||||
this._tooltipFormatter = params.tooltipFormatter || null;
|
||||
this._valueFormatter = params.valueFormatter || _.identity;
|
||||
this._attr = params.attr || {};
|
||||
|
||||
// set up the default legend colors
|
||||
this.quantizeLegendColors();
|
||||
}
|
||||
|
||||
getLabel() {
|
||||
if (this.popups.length) {
|
||||
return this.popups[0].feature.properties.aggConfigResult.aggConfig.makeLabel();
|
||||
}
|
||||
return '';
|
||||
}
|
||||
/**
|
||||
* Adds legend div to each map when data is split
|
||||
* uses d3 scale from BaseMarker.prototype.quantizeLegendColors
|
||||
*
|
||||
* @method addLegend
|
||||
* @return {undefined}
|
||||
*/
|
||||
addLegend() {
|
||||
// ensure we only ever create 1 legend
|
||||
if (this._legend) return;
|
||||
|
||||
const self = this;
|
||||
|
||||
// create the legend control, keep a reference
|
||||
self._legend = L.control({ position: this._attr.legendPosition });
|
||||
|
||||
self._legend.onAdd = function () {
|
||||
// creates all the neccessary DOM elements for the control, adds listeners
|
||||
// on relevant map events, and returns the element containing the control
|
||||
const $wrapper = $('<div>').addClass('tilemap-legend-wrapper');
|
||||
const $div = $('<div>').addClass('tilemap-legend');
|
||||
$wrapper.append($div);
|
||||
|
||||
const titleText = self.getLabel();
|
||||
const $title = $('<div>').addClass('tilemap-legend-title').text(titleText);
|
||||
$div.append($title);
|
||||
|
||||
_.each(self._legendColors, function (color) {
|
||||
const labelText = self._legendQuantizer
|
||||
.invertExtent(color)
|
||||
.map(self._valueFormatter)
|
||||
.join(' – ');
|
||||
|
||||
const label = $('<div>');
|
||||
|
||||
const icon = $('<i>').css({
|
||||
background: color,
|
||||
'border-color': self.darkerColor(color)
|
||||
});
|
||||
|
||||
const text = $('<span>').text(labelText);
|
||||
|
||||
label.append(icon);
|
||||
label.append(text);
|
||||
$div.append(label);
|
||||
});
|
||||
|
||||
return $wrapper.get(0);
|
||||
};
|
||||
|
||||
self._legend.addTo(self.map);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply style with shading to feature
|
||||
*
|
||||
* @method applyShadingStyle
|
||||
* @param value {Object}
|
||||
* @return {Object}
|
||||
*/
|
||||
applyShadingStyle(value) {
|
||||
const color = this._legendQuantizer(value);
|
||||
|
||||
return {
|
||||
fillColor: color,
|
||||
color: this.darkerColor(color),
|
||||
weight: 1.5,
|
||||
opacity: 1,
|
||||
fillOpacity: 0.75
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds popup and events to each feature on map
|
||||
*
|
||||
* @method bindPopup
|
||||
* @param feature {Object}
|
||||
* @param layer {Object}
|
||||
* return {undefined}
|
||||
*/
|
||||
bindPopup(feature, layer) {
|
||||
const self = this;
|
||||
|
||||
const popup = layer.on({
|
||||
mouseover: function (e) {
|
||||
const layer = e.target;
|
||||
// bring layer to front if not older browser
|
||||
if (!L.Browser.ie && !L.Browser.opera) {
|
||||
layer.bringToFront();
|
||||
}
|
||||
self._showTooltip(feature);
|
||||
},
|
||||
mouseout: function () {
|
||||
self._hidePopup();
|
||||
}
|
||||
});
|
||||
|
||||
self.popups.push(popup);
|
||||
}
|
||||
|
||||
/**
|
||||
* d3 method returns a darker hex color,
|
||||
* used for marker stroke color
|
||||
*
|
||||
* @method darkerColor
|
||||
* @param color {String} hex color
|
||||
* @param amount? {Number} amount to darken by
|
||||
* @return {String} hex color
|
||||
*/
|
||||
darkerColor(color, amount) {
|
||||
amount = amount || 1.3;
|
||||
return d3.hcl(color).darker(amount).toString();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
const self = this;
|
||||
|
||||
// remove popups
|
||||
self.popups = self.popups.filter(function (popup) {
|
||||
popup.off('mouseover').off('mouseout');
|
||||
});
|
||||
|
||||
if (self._legend) {
|
||||
self.map.removeControl(self._legend);
|
||||
self._legend = undefined;
|
||||
}
|
||||
|
||||
// remove marker layer from map
|
||||
if (self._markerGroup) {
|
||||
self.map.removeLayer(self._markerGroup);
|
||||
self._markerGroup = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
_addToMap() {
|
||||
this.map.addLayer(this._markerGroup);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates leaflet marker group, passing options to L.geoJson
|
||||
*
|
||||
* @method _createMarkerGroup
|
||||
* @param options {Object} Options to pass to L.geoJson
|
||||
*/
|
||||
_createMarkerGroup(options) {
|
||||
const self = this;
|
||||
const defaultOptions = {
|
||||
onEachFeature: function (feature, layer) {
|
||||
self.bindPopup(feature, layer);
|
||||
},
|
||||
style: function (feature) {
|
||||
const value = _.get(feature, 'properties.value');
|
||||
return self.applyShadingStyle(value);
|
||||
},
|
||||
filter: self._filterToMapBounds()
|
||||
};
|
||||
|
||||
this._markerGroup = L.geoJson(this.geoJson, _.defaults(defaultOptions, options));
|
||||
this._addToMap();
|
||||
}
|
||||
|
||||
/**
|
||||
* return whether feature is within map bounds
|
||||
*
|
||||
* @method _filterToMapBounds
|
||||
* @param map {Leaflet Object}
|
||||
* @return {boolean}
|
||||
*/
|
||||
_filterToMapBounds() {
|
||||
const self = this;
|
||||
return function (feature) {
|
||||
const mapBounds = self.map.getBounds();
|
||||
const bucketRectBounds = _.get(feature, 'properties.rectangle');
|
||||
return mapBounds.intersects(bucketRectBounds);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if event latlng is within bounds of mapData
|
||||
* features and shows tooltip for that feature
|
||||
*
|
||||
* @method _showTooltip
|
||||
* @param feature {LeafletFeature}
|
||||
* @param latLng? {Leaflet latLng}
|
||||
* @return undefined
|
||||
*/
|
||||
_showTooltip(feature, latLng) {
|
||||
const hasMap = !!this.map;
|
||||
const hasTooltip = !!this._attr.addTooltip;
|
||||
if (!hasMap || !hasTooltip) {
|
||||
return;
|
||||
}
|
||||
const lat = _.get(feature, 'geometry.coordinates.1');
|
||||
const lng = _.get(feature, 'geometry.coordinates.0');
|
||||
latLng = latLng || L.latLng(lat, lng);
|
||||
|
||||
const content = this._tooltipFormatter(feature);
|
||||
|
||||
if (!content) return;
|
||||
this._createTooltip(content, latLng);
|
||||
}
|
||||
|
||||
_createTooltip(content, latLng) {
|
||||
this._popup = L.popup({ autoPan: false });
|
||||
this._popup.setLatLng(latLng)
|
||||
.setContent(content)
|
||||
.openOn(this.map);
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the tooltip on the map
|
||||
*
|
||||
* @method _hidePopup
|
||||
* @return undefined
|
||||
*/
|
||||
_hidePopup() {
|
||||
if (!this.map) return;
|
||||
|
||||
this._popup = null;
|
||||
this.map.closePopup();
|
||||
}
|
||||
|
||||
/**
|
||||
* d3 quantize scale returns a hex color, used for marker fill color
|
||||
*
|
||||
* @method quantizeLegendColors
|
||||
* return {undefined}
|
||||
*/
|
||||
quantizeLegendColors() {
|
||||
const min = _.get(this.geoJson, 'properties.allmin', 0);
|
||||
const max = _.get(this.geoJson, 'properties.allmax', 1);
|
||||
const quantizeDomain = (min !== max) ? [min, max] : d3.scale.quantize().domain();
|
||||
|
||||
const reds1 = ['#ff6128'];
|
||||
const reds3 = ['#fecc5c', '#fd8d3c', '#e31a1c'];
|
||||
const reds5 = ['#fed976', '#feb24c', '#fd8d3c', '#f03b20', '#bd0026'];
|
||||
const bottomCutoff = 2;
|
||||
const middleCutoff = 24;
|
||||
|
||||
if (max - min <= bottomCutoff) {
|
||||
this._legendColors = reds1;
|
||||
} else if (max - min <= middleCutoff) {
|
||||
this._legendColors = reds3;
|
||||
} else {
|
||||
this._legendColors = reds5;
|
||||
}
|
||||
|
||||
this._legendQuantizer = d3.scale.quantize().domain(quantizeDomain).range(this._legendColors);
|
||||
}
|
||||
}
|
||||
|
||||
return BaseMarker;
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
import L from 'leaflet';
|
||||
import VislibVisualizationsMarkerTypesBaseMarkerProvider from './base_marker';
|
||||
export default function GeohashGridMarkerFactory(Private) {
|
||||
|
||||
const BaseMarker = Private(VislibVisualizationsMarkerTypesBaseMarkerProvider);
|
||||
|
||||
/**
|
||||
* Map overlay: rectangles that show the geohash grid bounds
|
||||
*
|
||||
* @param map {Leaflet Object}
|
||||
* @param geoJson {geoJson Object}
|
||||
* @param params {Object}
|
||||
*/
|
||||
class GeohashGridMarker extends BaseMarker {
|
||||
constructor(map, geoJson, params) {
|
||||
super(map, geoJson, params);
|
||||
|
||||
this._createMarkerGroup({
|
||||
pointToLayer: function (feature) {
|
||||
const geohashRect = feature.properties.rectangle;
|
||||
// get bounds from northEast[3] and southWest[1]
|
||||
// corners in geohash rectangle
|
||||
const corners = [
|
||||
[geohashRect[3][0], geohashRect[3][1]],
|
||||
[geohashRect[1][0], geohashRect[1][1]]
|
||||
];
|
||||
return L.rectangle(corners);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return GeohashGridMarker;
|
||||
}
|
|
@ -1,206 +0,0 @@
|
|||
import d3 from 'd3';
|
||||
import _ from 'lodash';
|
||||
import L from 'leaflet';
|
||||
import VislibVisualizationsMarkerTypesBaseMarkerProvider from './base_marker';
|
||||
export default function HeatmapMarkerFactory(Private) {
|
||||
|
||||
const BaseMarker = Private(VislibVisualizationsMarkerTypesBaseMarkerProvider);
|
||||
|
||||
/**
|
||||
* Map overlay: canvas layer with leaflet.heat plugin
|
||||
*
|
||||
* @param map {Leaflet Object}
|
||||
* @param geoJson {geoJson Object}
|
||||
* @param params {Object}
|
||||
*/
|
||||
class HeatmapMarker extends BaseMarker {
|
||||
constructor(map, geoJson, params) {
|
||||
super(map, geoJson, params);
|
||||
this._disableTooltips = false;
|
||||
|
||||
this._createMarkerGroup({
|
||||
radius: +this._attr.heatRadius,
|
||||
blur: +this._attr.heatBlur,
|
||||
maxZoom: +this._attr.heatMaxZoom,
|
||||
minOpacity: +this._attr.heatMinOpacity
|
||||
});
|
||||
|
||||
this.addLegend = _.noop;
|
||||
|
||||
this._getLatLng = _.memoize(function (feature) {
|
||||
return L.latLng(
|
||||
feature.geometry.coordinates[1],
|
||||
feature.geometry.coordinates[0]
|
||||
);
|
||||
}, function (feature) {
|
||||
// turn coords into a string for the memoize cache
|
||||
return [feature.geometry.coordinates[1], feature.geometry.coordinates[0]].join(',');
|
||||
});
|
||||
}
|
||||
|
||||
_createMarkerGroup(options) {
|
||||
const max = _.get(this.geoJson, 'properties.allmax');
|
||||
const points = this._dataToHeatArray(max);
|
||||
|
||||
this._markerGroup = L.heatLayer(points, options);
|
||||
this._fixTooltips();
|
||||
this._addToMap();
|
||||
}
|
||||
|
||||
_fixTooltips() {
|
||||
const self = this;
|
||||
const debouncedMouseMoveLocation = _.debounce(mouseMoveLocation.bind(this), 15, {
|
||||
'leading': true,
|
||||
'trailing': false
|
||||
});
|
||||
|
||||
if (!this._disableTooltips && this._attr.addTooltip) {
|
||||
this.map.on('mousemove', debouncedMouseMoveLocation);
|
||||
this.map.on('mouseout', function () {
|
||||
self.map.closePopup();
|
||||
});
|
||||
this.map.on('mousedown', function () {
|
||||
self._disableTooltips = true;
|
||||
self.map.closePopup();
|
||||
});
|
||||
this.map.on('mouseup', function () {
|
||||
self._disableTooltips = false;
|
||||
});
|
||||
}
|
||||
|
||||
function mouseMoveLocation(e) {
|
||||
const latlng = e.latlng;
|
||||
// unhighlight all svgs
|
||||
d3.selectAll('path.geohash', this.chartEl).classed('geohash-hover', false);
|
||||
|
||||
if (!this.geoJson.features.length || this._disableTooltips) {
|
||||
this._hidePopup();
|
||||
return;
|
||||
}
|
||||
|
||||
// find nearest feature to event latlng
|
||||
const feature = this._nearestFeature(latlng);
|
||||
|
||||
// show tooltip if close enough to event latlng
|
||||
if (this._tooltipProximity(latlng, feature)) {
|
||||
if (this.currentFeature !== feature) {
|
||||
this._hidePopup();
|
||||
this.currentFeature = feature;
|
||||
this._showTooltip(feature, latlng);
|
||||
} else {
|
||||
if (this._popup) {
|
||||
this._popup.setLatLng(latlng);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this._hidePopup();
|
||||
this.currentFeature = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds nearest feature in mapData to event latlng
|
||||
*
|
||||
* @method _nearestFeature
|
||||
* @param latLng {Leaflet latLng}
|
||||
* @return nearestPoint {Leaflet latLng}
|
||||
*/
|
||||
_nearestFeature(latLng) {
|
||||
const self = this;
|
||||
let nearest;
|
||||
|
||||
if (latLng.lng < -180 || latLng.lng > 180) {
|
||||
return;
|
||||
}
|
||||
|
||||
_.reduce(this.geoJson.features, function (distance, feature) {
|
||||
const featureLatLng = self._getLatLng(feature);
|
||||
const dist = latLng.distanceTo(featureLatLng);
|
||||
|
||||
if (dist < distance) {
|
||||
nearest = feature;
|
||||
return dist;
|
||||
}
|
||||
|
||||
return distance;
|
||||
}, Infinity);
|
||||
|
||||
return nearest;
|
||||
}
|
||||
|
||||
/**
|
||||
* display tooltip if feature is close enough to event latlng
|
||||
*
|
||||
* @method _tooltipProximity
|
||||
* @param latlng {Leaflet latLng Object}
|
||||
* @param feature {geoJson Object}
|
||||
* @return {Boolean}
|
||||
*/
|
||||
_tooltipProximity(latlng, feature) {
|
||||
if (!feature) return;
|
||||
|
||||
let showTip = false;
|
||||
const featureLatLng = this._getLatLng(feature);
|
||||
|
||||
// zoomScale takes map zoom and returns proximity value for tooltip display
|
||||
// domain (input values) is map zoom (min 1 and max 18)
|
||||
// range (output values) is distance in meters
|
||||
// used to compare proximity of event latlng to feature latlng
|
||||
const zoomScale = d3.scale.linear()
|
||||
.domain([1, 4, 7, 10, 13, 16, 18])
|
||||
.range([1000000, 300000, 100000, 15000, 2000, 150, 50]);
|
||||
|
||||
const proximity = zoomScale(this.map.getZoom());
|
||||
const distance = latlng.distanceTo(featureLatLng);
|
||||
|
||||
// maxLngDif is max difference in longitudes
|
||||
// to prevent feature tooltip from appearing 360°
|
||||
// away from event latlng
|
||||
const maxLngDif = 40;
|
||||
const lngDif = Math.abs(latlng.lng - featureLatLng.lng);
|
||||
|
||||
if (distance < proximity && lngDif < maxLngDif) {
|
||||
showTip = true;
|
||||
}
|
||||
|
||||
d3.scale.pow().exponent(0.2)
|
||||
.domain([1, 18])
|
||||
.range([1500000, 50]);
|
||||
return showTip;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* returns data for data for heat map intensity
|
||||
* if heatNormalizeData attribute is checked/true
|
||||
• normalizes data for heat map intensity
|
||||
*
|
||||
* @method _dataToHeatArray
|
||||
* @param max {Number}
|
||||
* @return {Array}
|
||||
*/
|
||||
_dataToHeatArray(max) {
|
||||
const self = this;
|
||||
|
||||
return this.geoJson.features.map(function (feature) {
|
||||
const lat = feature.properties.center[0];
|
||||
const lng = feature.properties.center[1];
|
||||
let heatIntensity;
|
||||
|
||||
if (!self._attr.heatNormalizeData) {
|
||||
// show bucket value on heatmap
|
||||
heatIntensity = feature.properties.value;
|
||||
} else {
|
||||
// show bucket value normalized to max value
|
||||
heatIntensity = feature.properties.value / max;
|
||||
}
|
||||
|
||||
return [lat, lng, heatIntensity];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return HeatmapMarker;
|
||||
}
|
|
@ -1,59 +0,0 @@
|
|||
import _ from 'lodash';
|
||||
import L from 'leaflet';
|
||||
import VislibVisualizationsMarkerTypesBaseMarkerProvider from './base_marker';
|
||||
export default function ScaledCircleMarkerFactory(Private) {
|
||||
|
||||
const BaseMarker = Private(VislibVisualizationsMarkerTypesBaseMarkerProvider);
|
||||
|
||||
/**
|
||||
* Map overlay: circle markers that are scaled to illustrate values
|
||||
*
|
||||
* @param map {Leaflet Object}
|
||||
* @param mapData {geoJson Object}
|
||||
* @param params {Object}
|
||||
*/
|
||||
class ScaledCircleMarker extends BaseMarker {
|
||||
constructor(map, geoJson, params) {
|
||||
super(map, geoJson, params);
|
||||
|
||||
// multiplier to reduce size of all circles
|
||||
const scaleFactor = 0.6;
|
||||
|
||||
this._createMarkerGroup({
|
||||
pointToLayer: (feature, latlng) => {
|
||||
const value = feature.properties.value;
|
||||
const scaledRadius = this._radiusScale(value) * scaleFactor;
|
||||
return L.circleMarker(latlng).setRadius(scaledRadius);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* radiusScale returns a number for scaled circle markers
|
||||
* for relative sizing of markers
|
||||
*
|
||||
* @method _radiusScale
|
||||
* @param value {Number}
|
||||
* @return {Number}
|
||||
*/
|
||||
_radiusScale(value) {
|
||||
const precisionBiasBase = 5;
|
||||
const precisionBiasNumerator = 200;
|
||||
const zoom = this.map.getZoom();
|
||||
const maxValue = this.geoJson.properties.allmax;
|
||||
const precision = _.max(this.geoJson.features.map(function (feature) {
|
||||
return String(feature.properties.geohash).length;
|
||||
}));
|
||||
|
||||
const pct = Math.abs(value) / Math.abs(maxValue);
|
||||
const zoomRadius = 0.5 * Math.pow(2, zoom);
|
||||
const precisionScale = precisionBiasNumerator / Math.pow(precisionBiasBase, precision);
|
||||
|
||||
// square root value percentage
|
||||
return Math.pow(pct, 0.5) * zoomRadius * precisionScale;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return ScaledCircleMarker;
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
import _ from 'lodash';
|
||||
import L from 'leaflet';
|
||||
import VislibVisualizationsMarkerTypesBaseMarkerProvider from './base_marker';
|
||||
export default function ShadedCircleMarkerFactory(Private) {
|
||||
|
||||
const BaseMarker = Private(VislibVisualizationsMarkerTypesBaseMarkerProvider);
|
||||
|
||||
/**
|
||||
* Map overlay: circle markers that are shaded to illustrate values
|
||||
*
|
||||
* @param map {Leaflet Object}
|
||||
* @param mapData {geoJson Object}
|
||||
* @return {Leaflet object} featureLayer
|
||||
*/
|
||||
class ShadedCircleMarker extends BaseMarker {
|
||||
constructor(map, geoJson, params) {
|
||||
super(map, geoJson, params);
|
||||
|
||||
// multiplier to reduce size of all circles
|
||||
const scaleFactor = 0.8;
|
||||
|
||||
this._createMarkerGroup({
|
||||
pointToLayer: (feature, latlng) => {
|
||||
const radius = this._geohashMinDistance(feature) * scaleFactor;
|
||||
return L.circle(latlng, radius);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* _geohashMinDistance returns a min distance in meters for sizing
|
||||
* circle markers to fit within geohash grid rectangle
|
||||
*
|
||||
* @method _geohashMinDistance
|
||||
* @param feature {Object}
|
||||
* @return {Number}
|
||||
*/
|
||||
_geohashMinDistance(feature) {
|
||||
const centerPoint = _.get(feature, 'properties.center');
|
||||
const geohashRect = _.get(feature, 'properties.rectangle');
|
||||
|
||||
// centerPoint is an array of [lat, lng]
|
||||
// geohashRect is the 4 corners of the geoHash rectangle
|
||||
// an array that starts at the southwest corner and proceeds
|
||||
// clockwise, each value being an array of [lat, lng]
|
||||
|
||||
// center lat and southeast lng
|
||||
const east = L.latLng([centerPoint[0], geohashRect[2][1]]);
|
||||
// southwest lat and center lng
|
||||
const north = L.latLng([geohashRect[3][0], centerPoint[1]]);
|
||||
|
||||
// get latLng of geohash center point
|
||||
const center = L.latLng([centerPoint[0], centerPoint[1]]);
|
||||
|
||||
// get smallest radius at center of geohash grid rectangle
|
||||
const eastRadius = Math.floor(center.distanceTo(east));
|
||||
const northRadius = Math.floor(center.distanceTo(north));
|
||||
return _.min([eastRadius, northRadius]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return ShadedCircleMarker;
|
||||
}
|
|
@ -1,128 +0,0 @@
|
|||
import _ from 'lodash';
|
||||
import $ from 'jquery';
|
||||
import VislibVisualizationsChartProvider from './_chart';
|
||||
import VislibVisualizationsMapProvider from './_map';
|
||||
export default function TileMapFactory(Private) {
|
||||
|
||||
const Chart = Private(VislibVisualizationsChartProvider);
|
||||
const TileMapMap = Private(VislibVisualizationsMapProvider);
|
||||
|
||||
/**
|
||||
* Tile Map Visualization: renders maps
|
||||
*
|
||||
* @class TileMap
|
||||
* @constructor
|
||||
* @extends Chart
|
||||
* @param handler {Object} Reference to the Handler Class Constructor
|
||||
* @param chartEl {HTMLElement} HTML element to which the map will be appended
|
||||
* @param chartData {Object} Elasticsearch query results for this map
|
||||
*/
|
||||
class TileMap extends Chart {
|
||||
constructor(handler, chartEl, chartData) {
|
||||
super(handler, chartEl, chartData);
|
||||
|
||||
// track the map objects
|
||||
this.maps = [];
|
||||
this._chartData = chartData || {};
|
||||
_.assign(this, this._chartData);
|
||||
|
||||
this._appendGeoExtents();
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws tile map, called on chart render
|
||||
*
|
||||
* @method draw
|
||||
* @return {Function} - function to add a map to a selection
|
||||
*/
|
||||
draw() {
|
||||
const self = this;
|
||||
|
||||
return function (selection) {
|
||||
selection.each(function () {
|
||||
self._appendMap(this);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate the size of the map, so that leaflet will resize to fit.
|
||||
* then moves to center
|
||||
*
|
||||
* @method resizeArea
|
||||
* @return {undefined}
|
||||
*/
|
||||
resizeArea() {
|
||||
this.maps.forEach(function (map) {
|
||||
map.updateSize();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* clean up the maps
|
||||
*
|
||||
* @method destroy
|
||||
* @return {undefined}
|
||||
*/
|
||||
destroy() {
|
||||
this.maps = this.maps.filter(function (map) {
|
||||
map.destroy();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds allmin and allmax properties to geoJson data
|
||||
*
|
||||
* @method _appendMap
|
||||
* @param selection {Object} d3 selection
|
||||
*/
|
||||
_appendGeoExtents() {
|
||||
// add allmin and allmax to geoJson
|
||||
const geoMinMax = this.handler.data.getGeoExtents();
|
||||
this.geoJson.properties.allmin = geoMinMax.min;
|
||||
this.geoJson.properties.allmax = geoMinMax.max;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders map
|
||||
*
|
||||
* @method _appendMap
|
||||
* @param selection {Object} d3 selection
|
||||
*/
|
||||
_appendMap(selection) {
|
||||
const container = $(selection).addClass('tilemap');
|
||||
const uiStateParams = {
|
||||
mapCenter: this.handler.uiState.get('mapCenter'),
|
||||
mapZoom: this.handler.uiState.get('mapZoom')
|
||||
};
|
||||
|
||||
const params = _.assign({}, _.get(this._chartData, 'geoAgg.vis.params'), uiStateParams);
|
||||
|
||||
const tooltipFormatter = this.handler.visConfig.get('addTooltip') ? this.tooltipFormatter : null;
|
||||
const map = new TileMapMap(container, this._chartData, {
|
||||
center: params.mapCenter,
|
||||
zoom: params.mapZoom,
|
||||
events: this.events,
|
||||
markerType: this.handler.visConfig.get('mapType'),
|
||||
tooltipFormatter: tooltipFormatter,
|
||||
valueFormatter: this.valueFormatter,
|
||||
attr: this.handler.visConfig._values
|
||||
});
|
||||
|
||||
// add title for splits
|
||||
if (this.title) {
|
||||
map.addTitle(this.title);
|
||||
}
|
||||
|
||||
// add fit to bounds control
|
||||
if (_.get(this.geoJson, 'features.length') > 0) {
|
||||
map.addFitControl();
|
||||
map.addBoundingControl();
|
||||
}
|
||||
|
||||
this.maps.push(map);
|
||||
}
|
||||
}
|
||||
|
||||
return TileMap;
|
||||
}
|
|
@ -1,330 +0,0 @@
|
|||
|
||||
import expect from 'expect.js';
|
||||
|
||||
import { bdd } from '../../../support';
|
||||
|
||||
import PageObjects from '../../../support/page_objects';
|
||||
|
||||
bdd.describe('visualize app', function describeIndexTests() {
|
||||
const fromTime = '2015-09-19 06:31:44.000';
|
||||
const toTime = '2015-09-23 18:31:44.000';
|
||||
|
||||
bdd.before(function () {
|
||||
|
||||
PageObjects.common.debug('navigateToApp visualize');
|
||||
return PageObjects.common.navigateToUrl('visualize', 'new')
|
||||
.then(function () {
|
||||
PageObjects.common.debug('clickTileMap');
|
||||
return PageObjects.visualize.clickTileMap();
|
||||
})
|
||||
.then(function () {
|
||||
return PageObjects.visualize.clickNewSearch();
|
||||
})
|
||||
.then(function () {
|
||||
PageObjects.common.debug('Set absolute time range from \"' + fromTime + '\" to \"' + toTime + '\"');
|
||||
return PageObjects.header.setAbsoluteRange(fromTime, toTime);
|
||||
})
|
||||
.then(function () {
|
||||
PageObjects.common.debug('select bucket Geo Coordinates');
|
||||
return PageObjects.visualize.clickBucket('Geo Coordinates');
|
||||
})
|
||||
.then(function () {
|
||||
PageObjects.common.debug('Click aggregation Geohash');
|
||||
return PageObjects.visualize.selectAggregation('Geohash');
|
||||
})
|
||||
.then(function () {
|
||||
PageObjects.common.debug('Click field geo.coordinates');
|
||||
return PageObjects.common.try(function tryingForTime() {
|
||||
return PageObjects.visualize.selectField('geo.coordinates');
|
||||
});
|
||||
})
|
||||
.then(function () {
|
||||
return PageObjects.visualize.clickGo();
|
||||
})
|
||||
.then(function () {
|
||||
return PageObjects.header.waitUntilLoadingHasFinished();
|
||||
});
|
||||
});
|
||||
|
||||
bdd.describe('tile map chart', function indexPatternCreation() {
|
||||
|
||||
bdd.it('should show correct tile map data on default zoom level', function () {
|
||||
const expectedTableData = [ 'dn 1,429', 'dp 1,418', '9y 1,215', '9z 1,099', 'dr 1,076',
|
||||
'dj 982', '9v 938', '9q 722', '9w 475', 'cb 457', 'c2 453', '9x 420', 'dq 399',
|
||||
'9r 396', '9t 274', 'c8 271', 'dh 214', 'b6 207', 'bd 206', 'b7 167', 'f0 141',
|
||||
'be 128', '9m 126', 'bf 85', 'de 73', 'bg 71', '9p 71', 'c1 57', 'c4 50', '9u 48',
|
||||
'f2 46', '8e 45', 'b3 38', 'bs 36', 'c0 31', '87 28', 'bk 23', '8f 18', 'b5 14',
|
||||
'84 14', 'dx 9', 'bu 9', 'b1 9', 'b4 6', '9n 3', '8g 3'
|
||||
];
|
||||
|
||||
return PageObjects.visualize.collapseChart()
|
||||
.then(function () {
|
||||
return PageObjects.settings.setPageSize('All');
|
||||
})
|
||||
.then(function getDataTableData() {
|
||||
return PageObjects.visualize.getDataTableData()
|
||||
.then(function showData(data) {
|
||||
expect(data.trim().split('\n')).to.eql(expectedTableData);
|
||||
return PageObjects.visualize.collapseChart();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
bdd.it('should zoom out to level 1 from default level 2', function () {
|
||||
const expectedPrecision2Circles = [ { color: '#750000', radius: 48 },
|
||||
{ color: '#750000', radius: 48 },
|
||||
{ color: '#750000', radius: 44 },
|
||||
{ color: '#a40000', radius: 42 },
|
||||
{ color: '#a40000', radius: 42 },
|
||||
{ color: '#a40000', radius: 40 },
|
||||
{ color: '#a40000', radius: 39 },
|
||||
{ color: '#b45100', radius: 34 },
|
||||
{ color: '#b67501', radius: 28 },
|
||||
{ color: '#b67501', radius: 27 },
|
||||
{ color: '#b67501', radius: 27 },
|
||||
{ color: '#b67501', radius: 26 },
|
||||
{ color: '#b67501', radius: 25 },
|
||||
{ color: '#b67501', radius: 25 },
|
||||
{ color: '#b99939', radius: 21 },
|
||||
{ color: '#b99939', radius: 21 },
|
||||
{ color: '#b99939', radius: 19 },
|
||||
{ color: '#b99939', radius: 18 },
|
||||
{ color: '#b99939', radius: 18 },
|
||||
{ color: '#b99939', radius: 16 },
|
||||
{ color: '#b99939', radius: 15 },
|
||||
{ color: '#b99939', radius: 14 },
|
||||
{ color: '#b99939', radius: 14 },
|
||||
{ color: '#b99939', radius: 12 },
|
||||
{ color: '#b99939', radius: 11 },
|
||||
{ color: '#b99939', radius: 11 },
|
||||
{ color: '#b99939', radius: 11 },
|
||||
{ color: '#b99939', radius: 10 },
|
||||
{ color: '#b99939', radius: 9 },
|
||||
{ color: '#b99939', radius: 9 },
|
||||
{ color: '#b99939', radius: 9 },
|
||||
{ color: '#b99939', radius: 9 },
|
||||
{ color: '#b99939', radius: 8 },
|
||||
{ color: '#b99939', radius: 8 },
|
||||
{ color: '#b99939', radius: 7 },
|
||||
{ color: '#b99939', radius: 7 },
|
||||
{ color: '#b99939', radius: 6 },
|
||||
{ color: '#b99939', radius: 5 },
|
||||
{ color: '#b99939', radius: 5 },
|
||||
{ color: '#b99939', radius: 5 },
|
||||
{ color: '#b99939', radius: 4 },
|
||||
{ color: '#b99939', radius: 4 },
|
||||
{ color: '#b99939', radius: 4 },
|
||||
{ color: '#b99939', radius: 3 },
|
||||
{ color: '#b99939', radius: 2 },
|
||||
{ color: '#b99939', radius: 2 }
|
||||
];
|
||||
|
||||
return PageObjects.visualize.clickMapZoomOut()
|
||||
.then(function () {
|
||||
return PageObjects.visualize.getMapZoomOutEnabled();
|
||||
})
|
||||
// we can tell we're at level 1 because zoom out is disabled
|
||||
.then(function () {
|
||||
return PageObjects.common.try(function tryingForTime() {
|
||||
return PageObjects.visualize.getMapZoomOutEnabled()
|
||||
.then(function (enabled) {
|
||||
//should be able to zoom more as current config has 0 as min level.
|
||||
expect(enabled).to.be(true);
|
||||
});
|
||||
});
|
||||
})
|
||||
.then(function () {
|
||||
return PageObjects.common.try(function tryingForTime() {
|
||||
return PageObjects.visualize.getTileMapData()
|
||||
.then(function (data) {
|
||||
expect(data).to.eql(expectedPrecision2Circles);
|
||||
});
|
||||
});
|
||||
})
|
||||
.then(function takeScreenshot() {
|
||||
PageObjects.common.debug('Take screenshot (success)');
|
||||
PageObjects.common.saveScreenshot('map-after-zoom-from-1-to-2');
|
||||
});
|
||||
});
|
||||
|
||||
bdd.it('Fit data bounds should zoom to level 3', function () {
|
||||
const expectedPrecision2ZoomCircles = [ { color: '#750000', radius: 192 },
|
||||
{ color: '#750000', radius: 191 },
|
||||
{ color: '#750000', radius: 177 },
|
||||
{ color: '#a40000', radius: 168 },
|
||||
{ color: '#a40000', radius: 167 },
|
||||
{ color: '#a40000', radius: 159 },
|
||||
{ color: '#a40000', radius: 156 },
|
||||
{ color: '#b45100', radius: 136 },
|
||||
{ color: '#b67501', radius: 111 },
|
||||
{ color: '#b67501', radius: 109 },
|
||||
{ color: '#b67501', radius: 108 },
|
||||
{ color: '#b67501', radius: 104 },
|
||||
{ color: '#b67501', radius: 101 },
|
||||
{ color: '#b67501', radius: 101 },
|
||||
{ color: '#b99939', radius: 84 },
|
||||
{ color: '#b99939', radius: 84 },
|
||||
{ color: '#b99939', radius: 74 },
|
||||
{ color: '#b99939', radius: 73 },
|
||||
{ color: '#b99939', radius: 73 },
|
||||
{ color: '#b99939', radius: 66 },
|
||||
{ color: '#b99939', radius: 60 },
|
||||
{ color: '#b99939', radius: 57 },
|
||||
{ color: '#b99939', radius: 57 },
|
||||
{ color: '#b99939', radius: 47 },
|
||||
{ color: '#b99939', radius: 43 },
|
||||
{ color: '#b99939', radius: 43 },
|
||||
{ color: '#b99939', radius: 43 },
|
||||
{ color: '#b99939', radius: 38 },
|
||||
{ color: '#b99939', radius: 36 },
|
||||
{ color: '#b99939', radius: 35 },
|
||||
{ color: '#b99939', radius: 34 },
|
||||
{ color: '#b99939', radius: 34 },
|
||||
{ color: '#b99939', radius: 31 },
|
||||
{ color: '#b99939', radius: 30 },
|
||||
{ color: '#b99939', radius: 28 },
|
||||
{ color: '#b99939', radius: 27 },
|
||||
{ color: '#b99939', radius: 24 },
|
||||
{ color: '#b99939', radius: 22 },
|
||||
{ color: '#b99939', radius: 19 },
|
||||
{ color: '#b99939', radius: 19 },
|
||||
{ color: '#b99939', radius: 15 },
|
||||
{ color: '#b99939', radius: 15 },
|
||||
{ color: '#b99939', radius: 15 },
|
||||
{ color: '#b99939', radius: 12 },
|
||||
{ color: '#b99939', radius: 9 },
|
||||
{ color: '#b99939', radius: 9 }
|
||||
];
|
||||
|
||||
return PageObjects.visualize.clickMapFitDataBounds()
|
||||
.then(function () {
|
||||
return PageObjects.visualize.getTileMapData();
|
||||
})
|
||||
.then(function (data) {
|
||||
expect(data).to.eql(expectedPrecision2ZoomCircles);
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
** NOTE: Since we don't have a reliable way to know the zoom level, we can
|
||||
** check some data after we save the viz, then zoom in and check that the data
|
||||
** changed, then open the saved viz and check that it's back to the original data.
|
||||
*/
|
||||
bdd.it('should save with zoom level and load, take screenshot', function () {
|
||||
const vizName1 = 'Visualization TileMap';
|
||||
const expectedTableData = [ 'dr4 127', 'dr7 92', '9q5 91', '9qc 89', 'drk 87',
|
||||
'dps 82', 'dph 82', 'dp3 79', 'dpe 78', 'dp8 77'
|
||||
];
|
||||
|
||||
const expectedTableDataZoomed = [ 'dr5r 21', 'dps8 20', '9q5b 19', 'b6uc 17',
|
||||
'9y63 17', 'c20g 16', 'dqfz 15', 'dr8h 14', 'dp8p 14', 'dp3k 14'
|
||||
];
|
||||
|
||||
return PageObjects.visualize.clickMapZoomIn()
|
||||
.then(function () {
|
||||
return PageObjects.visualize.clickMapZoomIn();
|
||||
})
|
||||
.then(function () {
|
||||
return PageObjects.visualize.saveVisualization(vizName1);
|
||||
})
|
||||
.then(function (message) {
|
||||
PageObjects.common.debug('Saved viz message = ' + message);
|
||||
expect(message).to.be('Visualization Editor: Saved Visualization \"' + vizName1 + '\"');
|
||||
})
|
||||
.then(function testVisualizeWaitForToastMessageGone() {
|
||||
return PageObjects.visualize.waitForToastMessageGone();
|
||||
})
|
||||
.then(function () {
|
||||
return PageObjects.visualize.collapseChart();
|
||||
})
|
||||
// we're not selecting page size all, so we only have to verify the first page of data
|
||||
.then(function getDataTableData() {
|
||||
PageObjects.common.debug('first get the zoom level 5 page data and verify it');
|
||||
return PageObjects.visualize.getDataTableData();
|
||||
})
|
||||
.then(function showData(data) {
|
||||
expect(data.trim().split('\n')).to.eql(expectedTableData);
|
||||
return PageObjects.visualize.collapseChart();
|
||||
})
|
||||
.then(function () {
|
||||
// zoom to level 6, and make sure we go back to the saved level 5
|
||||
return PageObjects.visualize.clickMapZoomIn();
|
||||
})
|
||||
.then(function () {
|
||||
return PageObjects.visualize.collapseChart();
|
||||
})
|
||||
.then(function getDataTableData() {
|
||||
PageObjects.common.debug('second get the zoom level 6 page data and verify it');
|
||||
return PageObjects.visualize.getDataTableData();
|
||||
})
|
||||
.then(function showData(data) {
|
||||
expect(data.trim().split('\n')).to.eql(expectedTableDataZoomed);
|
||||
return PageObjects.visualize.collapseChart();
|
||||
})
|
||||
.then(function () {
|
||||
return PageObjects.visualize.loadSavedVisualization(vizName1);
|
||||
})
|
||||
.then(function waitForVisualization() {
|
||||
return PageObjects.visualize.waitForVisualization();
|
||||
})
|
||||
// sleep a bit before taking the screenshot or it won't show data
|
||||
.then(function sleep() {
|
||||
return PageObjects.common.sleep(4000);
|
||||
})
|
||||
.then(function () {
|
||||
return PageObjects.visualize.collapseChart();
|
||||
})
|
||||
.then(function getDataTableData() {
|
||||
PageObjects.common.debug('third get the zoom level 5 page data and verify it');
|
||||
return PageObjects.visualize.getDataTableData();
|
||||
})
|
||||
.then(function showData(data) {
|
||||
expect(data.trim().split('\n')).to.eql(expectedTableData);
|
||||
return PageObjects.visualize.collapseChart();
|
||||
})
|
||||
.then(function takeScreenshot() {
|
||||
PageObjects.common.debug('Take screenshot');
|
||||
PageObjects.common.saveScreenshot('Visualize-site-map');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
bdd.it('should zoom in to level 10', function () {
|
||||
// 6
|
||||
return PageObjects.visualize.clickMapZoomIn()
|
||||
.then(function () {
|
||||
// 7
|
||||
return PageObjects.visualize.clickMapZoomIn();
|
||||
})
|
||||
.then(function () {
|
||||
// 8
|
||||
return PageObjects.visualize.clickMapZoomIn();
|
||||
})
|
||||
.then(function () {
|
||||
// 9
|
||||
return PageObjects.visualize.clickMapZoomIn();
|
||||
})
|
||||
.then(function () {
|
||||
return PageObjects.common.try(function tryingForTime() {
|
||||
return PageObjects.visualize.getMapZoomInEnabled()
|
||||
.then(function (enabled) {
|
||||
expect(enabled).to.be(true);
|
||||
});
|
||||
});
|
||||
})
|
||||
.then(function () {
|
||||
return PageObjects.visualize.clickMapZoomIn();
|
||||
})
|
||||
.then(function () {
|
||||
return PageObjects.visualize.getMapZoomInEnabled();
|
||||
})
|
||||
// now we're at level 10 and zoom out should be disabled
|
||||
.then(function (enabled) {
|
||||
expect(enabled).to.be(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
});
|
|
@ -35,7 +35,6 @@ bdd.describe('visualize app', function () {
|
|||
require('./_data_table');
|
||||
require('./_metric_chart');
|
||||
require('./_pie_chart');
|
||||
require('./_tile_map');
|
||||
require('./_vertical_bar_chart');
|
||||
require('./_heatmap_chart');
|
||||
require('./_point_series_options');
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue