diff --git a/package.json b/package.json index dff1fb105e497..d8979be4b7806 100644 --- a/package.json +++ b/package.json @@ -179,6 +179,7 @@ "redux": "3.0.0", "redux-thunk": "0.1.0", "request": "2.61.0", + "resize-observer-polyfill": "1.2.1", "rimraf": "2.4.3", "rison-node": "1.0.0", "rjs-repack-loader": "1.0.6", diff --git a/src/core_plugins/kbn_vislib_vis_types/public/editors/tile_map.html b/src/core_plugins/kbn_vislib_vis_types/public/editors/tile_map.html index 258d35fd23fb0..4820b987fe888 100644 --- a/src/core_plugins/kbn_vislib_vis_types/public/editors/tile_map.html +++ b/src/core_plugins/kbn_vislib_vis_types/public/editors/tile_map.html @@ -56,7 +56,7 @@
Joi.object({ }), profile: Joi.boolean().default(false) }).default(), - status: Joi.object({ allowAnonymous: Joi.boolean().default(false), v6ApiFormat: Joi.boolean().default(false) @@ -157,7 +156,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(), diff --git a/src/ui/public/agg_response/geo_json/__tests__/geo_json.js b/src/ui/public/agg_response/geo_json/__tests__/geo_json.js index 0d7b18f02abea..218069dc37ae3 100644 --- a/src/ui/public/agg_response/geo_json/__tests__/geo_json.js +++ b/src/ui/public/agg_response/geo_json/__tests__/geo_json.js @@ -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 () { diff --git a/src/ui/public/agg_response/geo_json/geo_json.js b/src/ui/public/agg_response/geo_json/geo_json.js index 2bb6fbafdb81f..0bd247e53c6f2 100644 --- a/src/ui/public/agg_response/geo_json/geo_json.js +++ b/src/ui/public/agg_response/geo_json/geo_json.js @@ -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; }); diff --git a/src/ui/public/agg_response/geo_json/rows_to_features.js b/src/ui/public/agg_response/geo_json/rows_to_features.js index 38e2757a50466..ffb80ac64ac2f 100644 --- a/src/ui/public/agg_response/geo_json/rows_to_features.js +++ b/src/ui/public/agg_response/geo_json/rows_to_features.js @@ -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, diff --git a/src/ui/public/agg_types/__tests__/buckets/_geo_hash.js b/src/ui/public/agg_types/__tests__/buckets/_geo_hash.js index b296b67929341..c08575d45d6be 100644 --- a/src/ui/public/agg_types/__tests__/buckets/_geo_hash.js +++ b/src/ui/public/agg_types/__tests__/buckets/_geo_hash.js @@ -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 () { diff --git a/src/ui/public/agg_types/agg_params.js b/src/ui/public/agg_types/agg_params.js index 251a0e9d64e5b..837f27c3a3843 100644 --- a/src/ui/public/agg_types/agg_params.js +++ b/src/ui/public/agg_types/agg_params.js @@ -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), diff --git a/src/ui/public/agg_types/agg_type.js b/src/ui/public/agg_types/agg_type.js index 53e3850d3dadf..ba59f24007254 100644 --- a/src/ui/public/agg_types/agg_type.js +++ b/src/ui/public/agg_types/agg_type.js @@ -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 diff --git a/src/ui/public/agg_types/buckets/geo_hash.js b/src/ui/public/agg_types/buckets/geo_hash.js index a9a164fbb4a40..1c71fb4da20fe 100644 --- a/src/ui/public/agg_types/buckets/geo_hash.js +++ b/src/ui/public/agg_types/buckets/geo_hash.js @@ -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' + })]; + } }); } diff --git a/src/ui/public/agg_types/controls/precision.html b/src/ui/public/agg_types/controls/precision.html index c72fef8a85b9f..adee607784631 100644 --- a/src/ui/public/agg_types/controls/precision.html +++ b/src/ui/public/agg_types/controls/precision.html @@ -18,7 +18,7 @@
-
+
+
+ +
+ diff --git a/src/ui/public/agg_types/index.js b/src/ui/public/agg_types/index.js index da5a5a6c94521..bdb8421f0d2a8 100644 --- a/src/ui/public/agg_types/index.js +++ b/src/ui/public/agg_types/index.js @@ -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), ] }; diff --git a/src/ui/public/agg_types/metrics/geo_centroid.js b/src/ui/public/agg_types/metrics/geo_centroid.js new file mode 100644 index 0000000000000..8a6d5dae38803 --- /dev/null +++ b/src/ui/public/agg_types/metrics/geo_centroid.js @@ -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; + } + }); +} diff --git a/src/ui/public/resize_checker/__tests__/resize_checker.js b/src/ui/public/resize_checker/__tests__/resize_checker.js new file mode 100644 index 0000000000000..8791df57b85df --- /dev/null +++ b/src/ui/public/resize_checker/__tests__/resize_checker.js @@ -0,0 +1,145 @@ +import $ from 'jquery'; +import { delay } from 'bluebird'; +import expect from 'expect.js'; +import sinon from 'auto-release-sinon'; + +import ngMock from 'ng_mock'; +import EventsProvider from 'ui/events'; +import NoDigestPromises from 'test_utils/no_digest_promises'; + +import { ResizeCheckerProvider } from '../resize_checker'; + +describe('Resize Checker', () => { + NoDigestPromises.activateForSuite(); + + const teardown = []; + let setup; + + beforeEach(ngMock.module('kibana')); + beforeEach(ngMock.inject(($injector) => { + setup = () => { + const Private = $injector.get('Private'); + const ResizeChecker = Private(ResizeCheckerProvider); + const EventEmitter = Private(EventsProvider); + + const createEl = () => { + const el = $('
').appendTo('body').get(0); + teardown.push(() => $(el).remove()); + return el; + }; + + const createChecker = el => { + const checker = new ResizeChecker(el); + teardown.push(() => checker.destroy()); + return checker; + }; + + const createListener = () => { + let resolveFirstCallPromise; + const listener = sinon.spy(() => resolveFirstCallPromise()); + listener.firstCallPromise = new Promise(resolve => (resolveFirstCallPromise = resolve)); + return listener; + }; + + return { EventEmitter, createEl, createChecker, createListener }; + }; + })); + + afterEach(() => { + teardown.splice(0).forEach(fn => { + fn(); + }); + }); + + describe('contruction', () => { + it('accepts a jQuery wrapped element', () => { + const { createChecker } = setup(); + + createChecker($('
')); + }); + }); + + describe('events', () => { + it('is an event emitter', () => { + const { createEl, createChecker, EventEmitter } = setup(); + + const checker = createChecker(createEl()); + expect(checker).to.be.a(EventEmitter); + }); + + it('emits a "resize" event asynchronously', async () => { + const { createEl, createChecker, createListener } = setup(); + + const el = createEl(); + const checker = createChecker(el); + const listener = createListener(); + + checker.on('resize', listener); + $(el).height(100); + sinon.assert.notCalled(listener); + await listener.firstCallPromise; + sinon.assert.calledOnce(listener); + }); + }); + + describe('#modifySizeWithoutTriggeringResize()', () => { + it(`does not emit "resize" events caused by the block`, async () => { + const { createChecker, createEl, createListener } = setup(); + + const el = createEl(); + const checker = createChecker(el); + const listener = createListener(); + + checker.on('resize', listener); + checker.modifySizeWithoutTriggeringResize(() => { + $(el).height(100); + }); + await delay(1000); + sinon.assert.notCalled(listener); + }); + + it('does emit "resize" when modification is made between the block and resize notification', async () => { + const { createChecker, createEl, createListener } = setup(); + + const el = createEl(); + const checker = createChecker(el); + const listener = createListener(); + + checker.on('resize', listener); + checker.modifySizeWithoutTriggeringResize(() => { + $(el).height(100); + }); + sinon.assert.notCalled(listener); + $(el).height(200); + await listener.firstCallPromise; + sinon.assert.calledOnce(listener); + }); + }); + + describe('#destroy()', () => { + it('destroys internal observer instance', () => { + const { createChecker, createEl, createListener } = setup(); + + const checker = createChecker(createEl()); + createListener(); + + checker.destroy(); + expect(!checker._observer).to.be(true); + }); + + it('does not emit future resize events', async () => { + const { createChecker, createEl, createListener } = setup(); + + const el = createEl(); + const checker = createChecker(el); + const listener = createListener(); + + checker.on('resize', listener); + checker.destroy(); + + $(el).height(100); + await delay(1000); + sinon.assert.notCalled(listener); + }); + }); +}); diff --git a/src/ui/public/resize_checker/index.js b/src/ui/public/resize_checker/index.js new file mode 100644 index 0000000000000..fbc47d1f9afd7 --- /dev/null +++ b/src/ui/public/resize_checker/index.js @@ -0,0 +1 @@ +export { ResizeCheckerProvider } from './resize_checker'; diff --git a/src/ui/public/resize_checker/resize_checker.js b/src/ui/public/resize_checker/resize_checker.js new file mode 100644 index 0000000000000..c112a3f801060 --- /dev/null +++ b/src/ui/public/resize_checker/resize_checker.js @@ -0,0 +1,94 @@ +import $ from 'jquery'; +import ResizeObserver from 'resize-observer-polyfill'; +import { isEqual } from 'lodash'; + +import EventsProvider from 'ui/events'; + +export function ResizeCheckerProvider(Private) { + const EventEmitter = Private(EventsProvider); + + function validateElArg(el) { + // the ResizeChecker historically accepted jquery elements, + // so we wrap in jQuery then extract the element + const $el = $(el); + + if ($el.size() !== 1) { + throw new TypeError('ResizeChecker must be constructed with a single DOM element.'); + } + + return $el.get(0); + } + + function getSize(el) { + return [el.clientWidth, el.clientHeight]; + } + + /** + * ResizeChecker receives an element and emits a "resize" + * event every time it changes size. Used by the vislib to re-render + * visualizations on resize as well as the console for the + * same reason, but for the editors. + */ + return class ResizeChecker extends EventEmitter { + constructor(el) { + super(); + + this._el = validateElArg(el); + + // the width and height of the element that we expect to see + // on the next resize notification. If it matches the size at + // the time of the notifications then it we will be ignored. + this._expectedSize = getSize(this._el); + + this._observer = new ResizeObserver(() => { + if (this._expectedSize) { + const sameSize = isEqual(getSize(this._el), this._expectedSize); + this._expectedSize = null; + + if (sameSize) { + // don't trigger resize notification if the size is what we expect + return; + } + } + + this.emit('resize'); + }); + + this._observer.observe(this._el); + } + + /** + * Run a function and ignore all resizes that occur + * while it's running. + * + * @return {undefined} + */ + modifySizeWithoutTriggeringResize(block) { + try { + block(); + } finally { + this._expectedSize = getSize(this._el); + } + } + + /** + * Tell the ResizeChecker to shutdown, stop listenings, and never + * emit another resize event. + * + * Cleans up it's listeners and timers. + * + * @method destroy + * @return {void} + */ + destroy() { + if (this._destroyed) return; + this._destroyed = true; + + this._observer.disconnect(); + this._observer = null; + this._expectedSize = null; + this._el = null; + this.removeAllListeners(); + } + }; +} diff --git a/src/ui/public/utils/zoom_to_precision.js b/src/ui/public/utils/zoom_to_precision.js new file mode 100644 index 0000000000000..375a032d63613 --- /dev/null +++ b/src/ui/public/utils/zoom_to_precision.js @@ -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); +} diff --git a/src/ui/public/vis/agg_config.js b/src/ui/public/vis/agg_config.js index 310ec5546ce34..6bfc0179aeb37 100644 --- a/src/ui/public/vis/agg_config.js +++ b/src/ui/public/vis/agg_config.js @@ -266,6 +266,11 @@ export default function AggConfigFactory(Private, fieldTypeFilter) { ); }; + 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]; diff --git a/src/ui/public/vis/agg_configs.js b/src/ui/public/vis/agg_configs.js index 3099a243d0feb..0797ceb8f4810 100644 --- a/src/ui/public/vis/agg_configs.js +++ b/src/ui/public/vis/agg_configs.js @@ -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; }); }; diff --git a/src/ui/public/vis_maps/__tests__/geohash_layer.js b/src/ui/public/vis_maps/__tests__/geohash_layer.js new file mode 100644 index 0000000000000..9502107253aac --- /dev/null +++ b/src/ui/public/vis_maps/__tests__/geohash_layer.js @@ -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); +} diff --git a/src/ui/public/vis_maps/__tests__/geohash_sample_data.js b/src/ui/public/vis_maps/__tests__/geohash_sample_data.js new file mode 100644 index 0000000000000..d814298d131aa --- /dev/null +++ b/src/ui/public/vis_maps/__tests__/geohash_sample_data.js @@ -0,0 +1,1341 @@ +const sampleData = `{ + "type": "FeatureCollection", + "features": [{ + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [-84.81215765699744, 36.289477944374084]}, + "properties": { + "geohash": "dn", + "value": 1418, + "aggConfigResult": { + "key": 1418, + "value": 1418, + "aggConfig": {"id": "1", "enabled": true, "type": "count", "schema": "metric", "params": {}}, + "$parent": { + "key": "dn", + "value": "dn", + "aggConfig": { + "id": "2", + "enabled": true, + "type": "geohash_grid", + "schema": "segment", + "params": {"field": "geo.coordinates", "autoPrecision": true, "useGeocentroid": true, "precision": 2} + }, + "$order": 4, + "type": "bucket" + }, + "$order": 5, + "type": "metric" + }, + "center": [36.5625, -84.375], + "rectangle": [[33.75, -90], [33.75, -78.75], [39.375, -78.75], [39.375, -90]] + } + }, { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [-84.8004243336618, 41.63311270996928]}, + "properties": { + "geohash": "dp", + "value": 1383, + "aggConfigResult": { + "key": 1383, + "value": 1383, + "aggConfig": {"id": "1", "enabled": true, "type": "count", "schema": "metric", "params": {}}, + "$parent": { + "key": "dp", + "value": "dp", + "aggConfig": { + "id": "2", + "enabled": true, + "type": "geohash_grid", + "schema": "segment", + "params": {"field": "geo.coordinates", "autoPrecision": true, "useGeocentroid": true, "precision": 2} + }, + "$order": 7, + "type": "bucket" + }, + "$order": 8, + "type": "metric" + }, + "center": [42.1875, -84.375], + "rectangle": [[39.375, -90], [39.375, -78.75], [45, -78.75], [45, -90]] + } + }, { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [-95.20564651116729, 36.4947619009763]}, + "properties": { + "geohash": "9y", + "value": 1219, + "aggConfigResult": { + "key": 1219, + "value": 1219, + "aggConfig": {"id": "1", "enabled": true, "type": "count", "schema": "metric", "params": {}}, + "$parent": { + "key": "9y", + "value": "9y", + "aggConfig": { + "id": "2", + "enabled": true, + "type": "geohash_grid", + "schema": "segment", + "params": {"field": "geo.coordinates", "autoPrecision": true, "useGeocentroid": true, "precision": 2} + }, + "$order": 10, + "type": "bucket" + }, + "$order": 11, + "type": "metric" + }, + "center": [36.5625, -95.625], + "rectangle": [[33.75, -101.25], [33.75, -90], [39.375, -90], [39.375, -101.25]] + } + }, { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [-73.8917376101017, 42.086046701297164]}, + "properties": { + "geohash": "dr", + "value": 1076, + "aggConfigResult": { + "key": 1076, + "value": 1076, + "aggConfig": {"id": "1", "enabled": true, "type": "count", "schema": "metric", "params": {}}, + "$parent": { + "key": "dr", + "value": "dr", + "aggConfig": { + "id": "2", + "enabled": true, + "type": "geohash_grid", + "schema": "segment", + "params": {"field": "geo.coordinates", "autoPrecision": true, "useGeocentroid": true, "precision": 2} + }, + "$order": 13, + "type": "bucket" + }, + "$order": 14, + "type": "metric" + }, + "center": [42.1875, -73.125], + "rectangle": [[39.375, -78.75], [39.375, -67.5], [45, -67.5], [45, -78.75]] + } + }, { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [-94.9999794177711, 42.19089978374541]}, + "properties": { + "geohash": "9z", + "value": 1047, + "aggConfigResult": { + "key": 1047, + "value": 1047, + "aggConfig": {"id": "1", "enabled": true, "type": "count", "schema": "metric", "params": {}}, + "$parent": { + "key": "9z", + "value": "9z", + "aggConfig": { + "id": "2", + "enabled": true, + "type": "geohash_grid", + "schema": "segment", + "params": {"field": "geo.coordinates", "autoPrecision": true, "useGeocentroid": true, "precision": 2} + }, + "$order": 16, + "type": "bucket" + }, + "$order": 17, + "type": "metric" + }, + "center": [42.1875, -95.625], + "rectangle": [[39.375, -101.25], [39.375, -90], [45, -90], [45, -101.25]] + } + }, { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [-84.72070790827274, 31.68308235704899]}, + "properties": { + "geohash": "dj", + "value": 972, + "aggConfigResult": { + "key": 972, + "value": 972, + "aggConfig": {"id": "1", "enabled": true, "type": "count", "schema": "metric", "params": {}}, + "$parent": { + "key": "dj", + "value": "dj", + "aggConfig": { + "id": "2", + "enabled": true, + "type": "geohash_grid", + "schema": "segment", + "params": {"field": "geo.coordinates", "autoPrecision": true, "useGeocentroid": true, "precision": 2} + }, + "$order": 19, + "type": "bucket" + }, + "$order": 20, + "type": "metric" + }, + "center": [30.9375, -84.375], + "rectangle": [[28.125, -90], [28.125, -78.75], [33.75, -78.75], [33.75, -90]] + } + }, { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [-95.22422080859542, 31.44715240225196]}, + "properties": { + "geohash": "9v", + "value": 950, + "aggConfigResult": { + "key": 950, + "value": 950, + "aggConfig": {"id": "1", "enabled": true, "type": "count", "schema": "metric", "params": {}}, + "$parent": { + "key": "9v", + "value": "9v", + "aggConfig": { + "id": "2", + "enabled": true, + "type": "geohash_grid", + "schema": "segment", + "params": {"field": "geo.coordinates", "autoPrecision": true, "useGeocentroid": true, "precision": 2} + }, + "$order": 22, + "type": "bucket" + }, + "$order": 23, + "type": "metric" + }, + "center": [30.9375, -95.625], + "rectangle": [[28.125, -101.25], [28.125, -90], [33.75, -90], [33.75, -101.25]] + } + }, { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [-119.02438038960099, 36.617594081908464]}, + "properties": { + "geohash": "9q", + "value": 751, + "aggConfigResult": { + "key": 751, + "value": 751, + "aggConfig": {"id": "1", "enabled": true, "type": "count", "schema": "metric", "params": {}}, + "$parent": { + "key": "9q", + "value": "9q", + "aggConfig": { + "id": "2", + "enabled": true, + "type": "geohash_grid", + "schema": "segment", + "params": {"field": "geo.coordinates", "autoPrecision": true, "useGeocentroid": true, "precision": 2} + }, + "$order": 25, + "type": "bucket" + }, + "$order": 26, + "type": "metric" + }, + "center": [36.5625, -118.125], + "rectangle": [[33.75, -123.75], [33.75, -112.5], [39.375, -112.5], [39.375, -123.75]] + } + }, { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [-106.54198246076703, 36.47509602829814]}, + "properties": { + "geohash": "9w", + "value": 516, + "aggConfigResult": { + "key": 516, + "value": 516, + "aggConfig": {"id": "1", "enabled": true, "type": "count", "schema": "metric", "params": {}}, + "$parent": { + "key": "9w", + "value": "9w", + "aggConfig": { + "id": "2", + "enabled": true, + "type": "geohash_grid", + "schema": "segment", + "params": {"field": "geo.coordinates", "autoPrecision": true, "useGeocentroid": true, "precision": 2} + }, + "$order": 28, + "type": "bucket" + }, + "$order": 29, + "type": "metric" + }, + "center": [36.5625, -106.875], + "rectangle": [[33.75, -112.5], [33.75, -101.25], [39.375, -101.25], [39.375, -112.5]] + } + }, { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [-119.28373273462057, 47.07595920190215]}, + "properties": { + "geohash": "c2", + "value": 497, + "aggConfigResult": { + "key": 497, + "value": 497, + "aggConfig": {"id": "1", "enabled": true, "type": "count", "schema": "metric", "params": {}}, + "$parent": { + "key": "c2", + "value": "c2", + "aggConfig": { + "id": "2", + "enabled": true, + "type": "geohash_grid", + "schema": "segment", + "params": {"field": "geo.coordinates", "autoPrecision": true, "useGeocentroid": true, "precision": 2} + }, + "$order": 31, + "type": "bucket" + }, + "$order": 32, + "type": "metric" + }, + "center": [47.8125, -118.125], + "rectangle": [[45, -123.75], [45, -112.5], [50.625, -112.5], [50.625, -123.75]] + } + }, { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [-95.67718841135502, 46.75232579000294]}, + "properties": { + "geohash": "cb", + "value": 468, + "aggConfigResult": { + "key": 468, + "value": 468, + "aggConfig": {"id": "1", "enabled": true, "type": "count", "schema": "metric", "params": {}}, + "$parent": { + "key": "cb", + "value": "cb", + "aggConfig": { + "id": "2", + "enabled": true, + "type": "geohash_grid", + "schema": "segment", + "params": {"field": "geo.coordinates", "autoPrecision": true, "useGeocentroid": true, "precision": 2} + }, + "$order": 34, + "type": "bucket" + }, + "$order": 35, + "type": "metric" + }, + "center": [47.8125, -95.625], + "rectangle": [[45, -101.25], [45, -90], [50.625, -90], [50.625, -101.25]] + } + }, { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [-106.2923239544034, 41.907251570373774]}, + "properties": { + "geohash": "9x", + "value": 396, + "aggConfigResult": { + "key": 396, + "value": 396, + "aggConfig": {"id": "1", "enabled": true, "type": "count", "schema": "metric", "params": {}}, + "$parent": { + "key": "9x", + "value": "9x", + "aggConfig": { + "id": "2", + "enabled": true, + "type": "geohash_grid", + "schema": "segment", + "params": {"field": "geo.coordinates", "autoPrecision": true, "useGeocentroid": true, "precision": 2} + }, + "$order": 37, + "type": "bucket" + }, + "$order": 38, + "type": "metric" + }, + "center": [42.1875, -106.875], + "rectangle": [[39.375, -112.5], [39.375, -101.25], [45, -101.25], [45, -112.5]] + } + }, { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [-119.63544443249702, 42.04197423532605]}, + "properties": { + "geohash": "9r", + "value": 370, + "aggConfigResult": { + "key": 370, + "value": 370, + "aggConfig": {"id": "1", "enabled": true, "type": "count", "schema": "metric", "params": {}}, + "$parent": { + "key": "9r", + "value": "9r", + "aggConfig": { + "id": "2", + "enabled": true, + "type": "geohash_grid", + "schema": "segment", + "params": {"field": "geo.coordinates", "autoPrecision": true, "useGeocentroid": true, "precision": 2} + }, + "$order": 40, + "type": "bucket" + }, + "$order": 41, + "type": "metric" + }, + "center": [42.1875, -118.125], + "rectangle": [[39.375, -123.75], [39.375, -112.5], [45, -112.5], [45, -123.75]] + } + }, { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [-76.97201896458864, 37.06826982088387]}, + "properties": { + "geohash": "dq", + "value": 364, + "aggConfigResult": { + "key": 364, + "value": 364, + "aggConfig": {"id": "1", "enabled": true, "type": "count", "schema": "metric", "params": {}}, + "$parent": { + "key": "dq", + "value": "dq", + "aggConfig": { + "id": "2", + "enabled": true, + "type": "geohash_grid", + "schema": "segment", + "params": {"field": "geo.coordinates", "autoPrecision": true, "useGeocentroid": true, "precision": 2} + }, + "$order": 43, + "type": "bucket" + }, + "$order": 44, + "type": "metric" + }, + "center": [36.5625, -73.125], + "rectangle": [[33.75, -78.75], [33.75, -67.5], [39.375, -67.5], [39.375, -78.75]] + } + }, { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [-106.92424703389406, 47.192871160805225]}, + "properties": { + "geohash": "c8", + "value": 305, + "aggConfigResult": { + "key": 305, + "value": 305, + "aggConfig": {"id": "1", "enabled": true, "type": "count", "schema": "metric", "params": {}}, + "$parent": { + "key": "c8", + "value": "c8", + "aggConfig": { + "id": "2", + "enabled": true, + "type": "geohash_grid", + "schema": "segment", + "params": {"field": "geo.coordinates", "autoPrecision": true, "useGeocentroid": true, "precision": 2} + }, + "$order": 46, + "type": "bucket" + }, + "$order": 47, + "type": "metric" + }, + "center": [47.8125, -106.875], + "rectangle": [[45, -112.5], [45, -101.25], [50.625, -101.25], [50.625, -112.5]] + } + }, { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [-106.78505832329392, 32.50790253281593]}, + "properties": { + "geohash": "9t", + "value": 284, + "aggConfigResult": { + "key": 284, + "value": 284, + "aggConfig": {"id": "1", "enabled": true, "type": "count", "schema": "metric", "params": {}}, + "$parent": { + "key": "9t", + "value": "9t", + "aggConfig": { + "id": "2", + "enabled": true, + "type": "geohash_grid", + "schema": "segment", + "params": {"field": "geo.coordinates", "autoPrecision": true, "useGeocentroid": true, "precision": 2} + }, + "$order": 49, + "type": "bucket" + }, + "$order": 50, + "type": "metric" + }, + "center": [30.9375, -106.875], + "rectangle": [[28.125, -112.5], [28.125, -101.25], [33.75, -101.25], [33.75, -112.5]] + } + }, { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [-152.9292524792254, 59.777277521789074]}, + "properties": { + "geohash": "bd", + "value": 217, + "aggConfigResult": { + "key": 217, + "value": 217, + "aggConfig": {"id": "1", "enabled": true, "type": "count", "schema": "metric", "params": {}}, + "$parent": { + "key": "bd", + "value": "bd", + "aggConfig": { + "id": "2", + "enabled": true, + "type": "geohash_grid", + "schema": "segment", + "params": {"field": "geo.coordinates", "autoPrecision": true, "useGeocentroid": true, "precision": 2} + }, + "$order": 52, + "type": "bucket" + }, + "$order": 53, + "type": "metric" + }, + "center": [59.0625, -151.875], + "rectangle": [[56.25, -157.5], [56.25, -146.25], [61.875, -146.25], [61.875, -157.5]] + } + }, { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [-81.13159038126469, 26.815882762894034]}, + "properties": { + "geohash": "dh", + "value": 214, + "aggConfigResult": { + "key": 214, + "value": 214, + "aggConfig": {"id": "1", "enabled": true, "type": "count", "schema": "metric", "params": {}}, + "$parent": { + "key": "dh", + "value": "dh", + "aggConfig": { + "id": "2", + "enabled": true, + "type": "geohash_grid", + "schema": "segment", + "params": {"field": "geo.coordinates", "autoPrecision": true, "useGeocentroid": true, "precision": 2} + }, + "$order": 55, + "type": "bucket" + }, + "$order": 56, + "type": "metric" + }, + "center": [25.3125, -84.375], + "rectangle": [[22.5, -90], [22.5, -78.75], [28.125, -78.75], [28.125, -90]] + } + }, { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [-162.1049272455275, 64.38826035708189]}, + "properties": { + "geohash": "b7", + "value": 194, + "aggConfigResult": { + "key": 194, + "value": 194, + "aggConfig": {"id": "1", "enabled": true, "type": "count", "schema": "metric", "params": {}}, + "$parent": { + "key": "b7", + "value": "b7", + "aggConfig": { + "id": "2", + "enabled": true, + "type": "geohash_grid", + "schema": "segment", + "params": {"field": "geo.coordinates", "autoPrecision": true, "useGeocentroid": true, "precision": 2} + }, + "$order": 58, + "type": "bucket" + }, + "$order": 59, + "type": "metric" + }, + "center": [64.6875, -163.125], + "rectangle": [[61.875, -168.75], [61.875, -157.5], [67.5, -157.5], [67.5, -168.75]] + } + }, { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [-161.59194018691778, 60.06503529846668]}, + "properties": { + "geohash": "b6", + "value": 168, + "aggConfigResult": { + "key": 168, + "value": 168, + "aggConfig": {"id": "1", "enabled": true, "type": "count", "schema": "metric", "params": {}}, + "$parent": { + "key": "b6", + "value": "b6", + "aggConfig": { + "id": "2", + "enabled": true, + "type": "geohash_grid", + "schema": "segment", + "params": {"field": "geo.coordinates", "autoPrecision": true, "useGeocentroid": true, "precision": 2} + }, + "$order": 61, + "type": "bucket" + }, + "$order": 62, + "type": "metric" + }, + "center": [59.0625, -163.125], + "rectangle": [[56.25, -168.75], [56.25, -157.5], [61.875, -157.5], [61.875, -168.75]] + } + }, { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [-86.82362716645002, 45.665992330759764]}, + "properties": { + "geohash": "f0", + "value": 166, + "aggConfigResult": { + "key": 166, + "value": 166, + "aggConfig": {"id": "1", "enabled": true, "type": "count", "schema": "metric", "params": {}}, + "$parent": { + "key": "f0", + "value": "f0", + "aggConfig": { + "id": "2", + "enabled": true, + "type": "geohash_grid", + "schema": "segment", + "params": {"field": "geo.coordinates", "autoPrecision": true, "useGeocentroid": true, "precision": 2} + }, + "$order": 64, + "type": "bucket" + }, + "$order": 65, + "type": "metric" + }, + "center": [47.8125, -84.375], + "rectangle": [[45, -90], [45, -78.75], [50.625, -78.75], [50.625, -90]] + } + }, { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [-152.04110082238913, 65.17680524848402]}, + "properties": { + "geohash": "be", + "value": 158, + "aggConfigResult": { + "key": 158, + "value": 158, + "aggConfig": {"id": "1", "enabled": true, "type": "count", "schema": "metric", "params": {}}, + "$parent": { + "key": "be", + "value": "be", + "aggConfig": { + "id": "2", + "enabled": true, + "type": "geohash_grid", + "schema": "segment", + "params": {"field": "geo.coordinates", "autoPrecision": true, "useGeocentroid": true, "precision": 2} + }, + "$order": 67, + "type": "bucket" + }, + "$order": 68, + "type": "metric" + }, + "center": [64.6875, -151.875], + "rectangle": [[61.875, -157.5], [61.875, -146.25], [67.5, -146.25], [67.5, -157.5]] + } + }, { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [-116.37748382985592, 33.16976627334952]}, + "properties": { + "geohash": "9m", + "value": 100, + "aggConfigResult": { + "key": 100, + "value": 100, + "aggConfig": {"id": "1", "enabled": true, "type": "count", "schema": "metric", "params": {}}, + "$parent": { + "key": "9m", + "value": "9m", + "aggConfig": { + "id": "2", + "enabled": true, + "type": "geohash_grid", + "schema": "segment", + "params": {"field": "geo.coordinates", "autoPrecision": true, "useGeocentroid": true, "precision": 2} + }, + "$order": 70, + "type": "bucket" + }, + "$order": 71, + "type": "metric" + }, + "center": [30.9375, -118.125], + "rectangle": [[28.125, -123.75], [28.125, -112.5], [33.75, -112.5], [33.75, -123.75]] + } + }, { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [-139.12713261321187, 59.41271326504648]}, + "properties": { + "geohash": "bf", + "value": 72, + "aggConfigResult": { + "key": 72, + "value": 72, + "aggConfig": {"id": "1", "enabled": true, "type": "count", "schema": "metric", "params": {}}, + "$parent": { + "key": "bf", + "value": "bf", + "aggConfig": { + "id": "2", + "enabled": true, + "type": "geohash_grid", + "schema": "segment", + "params": {"field": "geo.coordinates", "autoPrecision": true, "useGeocentroid": true, "precision": 2} + }, + "$order": 73, + "type": "bucket" + }, + "$order": 74, + "type": "metric" + }, + "center": [59.0625, -140.625], + "rectangle": [[56.25, -146.25], [56.25, -135], [61.875, -135], [61.875, -146.25]] + } + }, { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [-97.89513738825917, 26.928304536268115]}, + "properties": { + "geohash": "9u", + "value": 68, + "aggConfigResult": { + "key": 68, + "value": 68, + "aggConfig": {"id": "1", "enabled": true, "type": "count", "schema": "metric", "params": {}}, + "$parent": { + "key": "9u", + "value": "9u", + "aggConfig": { + "id": "2", + "enabled": true, + "type": "geohash_grid", + "schema": "segment", + "params": {"field": "geo.coordinates", "autoPrecision": true, "useGeocentroid": true, "precision": 2} + }, + "$order": 76, + "type": "bucket" + }, + "$order": 77, + "type": "metric" + }, + "center": [25.3125, -95.625], + "rectangle": [[22.5, -101.25], [22.5, -90], [28.125, -90], [28.125, -101.25]] + } + }, { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [-132.52599561586976, 55.60743710026145]}, + "properties": { + "geohash": "c1", + "value": 67, + "aggConfigResult": { + "key": 67, + "value": 67, + "aggConfig": {"id": "1", "enabled": true, "type": "count", "schema": "metric", "params": {}}, + "$parent": { + "key": "c1", + "value": "c1", + "aggConfig": { + "id": "2", + "enabled": true, + "type": "geohash_grid", + "schema": "segment", + "params": {"field": "geo.coordinates", "autoPrecision": true, "useGeocentroid": true, "precision": 2} + }, + "$order": 79, + "type": "bucket" + }, + "$order": 80, + "type": "metric" + }, + "center": [53.4375, -129.375], + "rectangle": [[50.625, -135], [50.625, -123.75], [56.25, -123.75], [56.25, -135]] + } + }, { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [-124.13590382784605, 42.24034773185849]}, + "properties": { + "geohash": "9p", + "value": 58, + "aggConfigResult": { + "key": 58, + "value": 58, + "aggConfig": {"id": "1", "enabled": true, "type": "count", "schema": "metric", "params": {}}, + "$parent": { + "key": "9p", + "value": "9p", + "aggConfig": { + "id": "2", + "enabled": true, + "type": "geohash_grid", + "schema": "segment", + "params": {"field": "geo.coordinates", "autoPrecision": true, "useGeocentroid": true, "precision": 2} + }, + "$order": 82, + "type": "bucket" + }, + "$order": 83, + "type": "metric" + }, + "center": [42.1875, -129.375], + "rectangle": [[39.375, -135], [39.375, -123.75], [45, -123.75], [45, -135]] + } + }, { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [-65.72741221636534, 18.170374436303973]}, + "properties": { + "geohash": "de", + "value": 57, + "aggConfigResult": { + "key": 57, + "value": 57, + "aggConfig": {"id": "1", "enabled": true, "type": "count", "schema": "metric", "params": {}}, + "$parent": { + "key": "de", + "value": "de", + "aggConfig": { + "id": "2", + "enabled": true, + "type": "geohash_grid", + "schema": "segment", + "params": {"field": "geo.coordinates", "autoPrecision": true, "useGeocentroid": true, "precision": 2} + }, + "$order": 85, + "type": "bucket" + }, + "$order": 86, + "type": "metric" + }, + "center": [19.6875, -61.875], + "rectangle": [[16.875, -67.5], [16.875, -56.25], [22.5, -56.25], [22.5, -67.5]] + } + }, { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [-133.79055473953485, 57.08371731452644]}, + "properties": { + "geohash": "c4", + "value": 56, + "aggConfigResult": { + "key": 56, + "value": 56, + "aggConfig": {"id": "1", "enabled": true, "type": "count", "schema": "metric", "params": {}}, + "$parent": { + "key": "c4", + "value": "c4", + "aggConfig": { + "id": "2", + "enabled": true, + "type": "geohash_grid", + "schema": "segment", + "params": {"field": "geo.coordinates", "autoPrecision": true, "useGeocentroid": true, "precision": 2} + }, + "$order": 88, + "type": "bucket" + }, + "$order": 89, + "type": "metric" + }, + "center": [59.0625, -129.375], + "rectangle": [[56.25, -135], [56.25, -123.75], [61.875, -123.75], [61.875, -135]] + } + }, { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [-152.2658603824675, 69.64116730727255]}, + "properties": { + "geohash": "bs", + "value": 51, + "aggConfigResult": { + "key": 51, + "value": 51, + "aggConfig": {"id": "1", "enabled": true, "type": "count", "schema": "metric", "params": {}}, + "$parent": { + "key": "bs", + "value": "bs", + "aggConfig": { + "id": "2", + "enabled": true, + "type": "geohash_grid", + "schema": "segment", + "params": {"field": "geo.coordinates", "autoPrecision": true, "useGeocentroid": true, "precision": 2} + }, + "$order": 91, + "type": "bucket" + }, + "$order": 92, + "type": "metric" + }, + "center": [70.3125, -151.875], + "rectangle": [[67.5, -157.5], [67.5, -146.25], [73.125, -146.25], [73.125, -157.5]] + } + }, { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [-143.8043469004333, 64.64996575377882]}, + "properties": { + "geohash": "bg", + "value": 49, + "aggConfigResult": { + "key": 49, + "value": 49, + "aggConfig": {"id": "1", "enabled": true, "type": "count", "schema": "metric", "params": {}}, + "$parent": { + "key": "bg", + "value": "bg", + "aggConfig": { + "id": "2", + "enabled": true, + "type": "geohash_grid", + "schema": "segment", + "params": {"field": "geo.coordinates", "autoPrecision": true, "useGeocentroid": true, "precision": 2} + }, + "$order": 94, + "type": "bucket" + }, + "$order": 95, + "type": "metric" + }, + "center": [64.6875, -140.625], + "rectangle": [[61.875, -146.25], [61.875, -135], [67.5, -135], [67.5, -146.25]] + } + }, { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [-162.65227859839797, 54.967785738408566]}, + "properties": { + "geohash": "b3", + "value": 43, + "aggConfigResult": { + "key": 43, + "value": 43, + "aggConfig": {"id": "1", "enabled": true, "type": "count", "schema": "metric", "params": {}}, + "$parent": { + "key": "b3", + "value": "b3", + "aggConfig": { + "id": "2", + "enabled": true, + "type": "geohash_grid", + "schema": "segment", + "params": {"field": "geo.coordinates", "autoPrecision": true, "useGeocentroid": true, "precision": 2} + }, + "$order": 97, + "type": "bucket" + }, + "$order": 98, + "type": "metric" + }, + "center": [53.4375, -163.125], + "rectangle": [[50.625, -168.75], [50.625, -157.5], [56.25, -157.5], [56.25, -168.75]] + } + }, { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [-156.20294423773885, 20.63592097721994]}, + "properties": { + "geohash": "8e", + "value": 40, + "aggConfigResult": { + "key": 40, + "value": 40, + "aggConfig": {"id": "1", "enabled": true, "type": "count", "schema": "metric", "params": {}}, + "$parent": { + "key": "8e", + "value": "8e", + "aggConfig": { + "id": "2", + "enabled": true, + "type": "geohash_grid", + "schema": "segment", + "params": {"field": "geo.coordinates", "autoPrecision": true, "useGeocentroid": true, "precision": 2} + }, + "$order": 100, + "type": "bucket" + }, + "$order": 101, + "type": "metric" + }, + "center": [19.6875, -151.875], + "rectangle": [[16.875, -157.5], [16.875, -146.25], [22.5, -146.25], [22.5, -157.5]] + } + }, { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [-68.71966263279319, 45.89407338760793]}, + "properties": { + "geohash": "f2", + "value": 37, + "aggConfigResult": { + "key": 37, + "value": 37, + "aggConfig": {"id": "1", "enabled": true, "type": "count", "schema": "metric", "params": {}}, + "$parent": { + "key": "f2", + "value": "f2", + "aggConfig": { + "id": "2", + "enabled": true, + "type": "geohash_grid", + "schema": "segment", + "params": {"field": "geo.coordinates", "autoPrecision": true, "useGeocentroid": true, "precision": 2} + }, + "$order": 103, + "type": "bucket" + }, + "$order": 104, + "type": "metric" + }, + "center": [47.8125, -73.125], + "rectangle": [[45, -78.75], [45, -67.5], [50.625, -67.5], [50.625, -78.75]] + } + }, { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [-159.04649432748556, 21.810192000120878]}, + "properties": { + "geohash": "87", + "value": 31, + "aggConfigResult": { + "key": 31, + "value": 31, + "aggConfig": {"id": "1", "enabled": true, "type": "count", "schema": "metric", "params": {}}, + "$parent": { + "key": "87", + "value": "87", + "aggConfig": { + "id": "2", + "enabled": true, + "type": "geohash_grid", + "schema": "segment", + "params": {"field": "geo.coordinates", "autoPrecision": true, "useGeocentroid": true, "precision": 2} + }, + "$order": 106, + "type": "bucket" + }, + "$order": 107, + "type": "metric" + }, + "center": [19.6875, -163.125], + "rectangle": [[16.875, -168.75], [16.875, -157.5], [22.5, -157.5], [22.5, -168.75]] + } + }, { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [-124.07574724406004, 46.70505428686738]}, + "properties": { + "geohash": "c0", + "value": 30, + "aggConfigResult": { + "key": 30, + "value": 30, + "aggConfig": {"id": "1", "enabled": true, "type": "count", "schema": "metric", "params": {}}, + "$parent": { + "key": "c0", + "value": "c0", + "aggConfig": { + "id": "2", + "enabled": true, + "type": "geohash_grid", + "schema": "segment", + "params": {"field": "geo.coordinates", "autoPrecision": true, "useGeocentroid": true, "precision": 2} + }, + "$order": 109, + "type": "bucket" + }, + "$order": 110, + "type": "metric" + }, + "center": [47.8125, -129.375], + "rectangle": [[45, -135], [45, -123.75], [50.625, -123.75], [50.625, -135]] + } + }, { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [-170.66843625158072, 64.42178352735937]}, + "properties": { + "geohash": "b5", + "value": 18, + "aggConfigResult": { + "key": 18, + "value": 18, + "aggConfig": {"id": "1", "enabled": true, "type": "count", "schema": "metric", "params": {}}, + "$parent": { + "key": "b5", + "value": "b5", + "aggConfig": { + "id": "2", + "enabled": true, + "type": "geohash_grid", + "schema": "segment", + "params": {"field": "geo.coordinates", "autoPrecision": true, "useGeocentroid": true, "precision": 2} + }, + "$order": 112, + "type": "bucket" + }, + "$order": 113, + "type": "metric" + }, + "center": [64.6875, -174.375], + "rectangle": [[61.875, -180], [61.875, -168.75], [67.5, -168.75], [67.5, -180]] + } + }, { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [-164.1237143240869, 68.94954898394644]}, + "properties": { + "geohash": "bk", + "value": 17, + "aggConfigResult": { + "key": 17, + "value": 17, + "aggConfig": {"id": "1", "enabled": true, "type": "count", "schema": "metric", "params": {}}, + "$parent": { + "key": "bk", + "value": "bk", + "aggConfig": { + "id": "2", + "enabled": true, + "type": "geohash_grid", + "schema": "segment", + "params": {"field": "geo.coordinates", "autoPrecision": true, "useGeocentroid": true, "precision": 2} + }, + "$order": 115, + "type": "bucket" + }, + "$order": 116, + "type": "metric" + }, + "center": [70.3125, -163.125], + "rectangle": [[67.5, -168.75], [67.5, -157.5], [73.125, -157.5], [73.125, -168.75]] + } + }, { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [-145.23947272449732, 14.257271960377693]}, + "properties": { + "geohash": "8f", + "value": 17, + "aggConfigResult": { + "key": 17, + "value": 17, + "aggConfig": {"id": "1", "enabled": true, "type": "count", "schema": "metric", "params": {}}, + "$parent": { + "key": "8f", + "value": "8f", + "aggConfig": { + "id": "2", + "enabled": true, + "type": "geohash_grid", + "schema": "segment", + "params": {"field": "geo.coordinates", "autoPrecision": true, "useGeocentroid": true, "precision": 2} + }, + "$order": 118, + "type": "bucket" + }, + "$order": 119, + "type": "metric" + }, + "center": [14.0625, -140.625], + "rectangle": [[11.25, -146.25], [11.25, -135], [16.875, -135], [16.875, -146.25]] + } + }, { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [-169.90729674696922, 56.83546897955239]}, + "properties": { + "geohash": "b4", + "value": 16, + "aggConfigResult": { + "key": 16, + "value": 16, + "aggConfig": {"id": "1", "enabled": true, "type": "count", "schema": "metric", "params": {}}, + "$parent": { + "key": "b4", + "value": "b4", + "aggConfig": { + "id": "2", + "enabled": true, + "type": "geohash_grid", + "schema": "segment", + "params": {"field": "geo.coordinates", "autoPrecision": true, "useGeocentroid": true, "precision": 2} + }, + "$order": 121, + "type": "bucket" + }, + "$order": 122, + "type": "metric" + }, + "center": [59.0625, -174.375], + "rectangle": [[56.25, -180], [56.25, -168.75], [61.875, -168.75], [61.875, -180]] + } + }, { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [-170.12874579057097, 14.265542635694146]}, + "properties": { + "geohash": "84", + "value": 12, + "aggConfigResult": { + "key": 12, + "value": 12, + "aggConfig": {"id": "1", "enabled": true, "type": "count", "schema": "metric", "params": {}}, + "$parent": { + "key": "84", + "value": "84", + "aggConfig": { + "id": "2", + "enabled": true, + "type": "geohash_grid", + "schema": "segment", + "params": {"field": "geo.coordinates", "autoPrecision": true, "useGeocentroid": true, "precision": 2} + }, + "$order": 124, + "type": "bucket" + }, + "$order": 125, + "type": "metric" + }, + "center": [14.0625, -174.375], + "rectangle": [[11.25, -180], [11.25, -168.75], [16.875, -168.75], [16.875, -180]] + } + }, { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [-144.66744488105178, 69.03327229432762]}, + "properties": { + "geohash": "bu", + "value": 11, + "aggConfigResult": { + "key": 11, + "value": 11, + "aggConfig": {"id": "1", "enabled": true, "type": "count", "schema": "metric", "params": {}}, + "$parent": { + "key": "bu", + "value": "bu", + "aggConfig": { + "id": "2", + "enabled": true, + "type": "geohash_grid", + "schema": "segment", + "params": {"field": "geo.coordinates", "autoPrecision": true, "useGeocentroid": true, "precision": 2} + }, + "$order": 127, + "type": "bucket" + }, + "$order": 128, + "type": "metric" + }, + "center": [70.3125, -140.625], + "rectangle": [[67.5, -146.25], [67.5, -135], [73.125, -135], [73.125, -146.25]] + } + }, { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [-67.10587805137038, 44.86871098168194]}, + "properties": { + "geohash": "dx", + "value": 5, + "aggConfigResult": { + "key": 5, + "value": 5, + "aggConfig": {"id": "1", "enabled": true, "type": "count", "schema": "metric", "params": {}}, + "$parent": { + "key": "dx", + "value": "dx", + "aggConfig": { + "id": "2", + "enabled": true, + "type": "geohash_grid", + "schema": "segment", + "params": {"field": "geo.coordinates", "autoPrecision": true, "useGeocentroid": true, "precision": 2} + }, + "$order": 130, + "type": "bucket" + }, + "$order": 131, + "type": "metric" + }, + "center": [42.1875, -61.875], + "rectangle": [[39.375, -67.5], [39.375, -56.25], [45, -56.25], [45, -67.5]] + } + }, { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [-174.69428664073348, 52.15187128633261]}, + "properties": { + "geohash": "b1", + "value": 5, + "aggConfigResult": { + "key": 5, + "value": 5, + "aggConfig": {"id": "1", "enabled": true, "type": "count", "schema": "metric", "params": {}}, + "$parent": { + "key": "b1", + "value": "b1", + "aggConfig": { + "id": "2", + "enabled": true, + "type": "geohash_grid", + "schema": "segment", + "params": {"field": "geo.coordinates", "autoPrecision": true, "useGeocentroid": true, "precision": 2} + }, + "$order": 133, + "type": "bucket" + }, + "$order": 134, + "type": "metric" + }, + "center": [53.4375, -174.375], + "rectangle": [[50.625, -180], [50.625, -168.75], [56.25, -168.75], [56.25, -180]] + } + }, { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [-123.75373480841517, 39.26203776150942]}, + "properties": { + "geohash": "9n", + "value": 5, + "aggConfigResult": { + "key": 5, + "value": 5, + "aggConfig": {"id": "1", "enabled": true, "type": "count", "schema": "metric", "params": {}}, + "$parent": { + "key": "9n", + "value": "9n", + "aggConfig": { + "id": "2", + "enabled": true, + "type": "geohash_grid", + "schema": "segment", + "params": {"field": "geo.coordinates", "autoPrecision": true, "useGeocentroid": true, "precision": 2} + }, + "$order": 136, + "type": "bucket" + }, + "$order": 137, + "type": "metric" + }, + "center": [36.5625, -129.375], + "rectangle": [[33.75, -135], [33.75, -123.75], [39.375, -123.75], [39.375, -135]] + } + }, { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [-145.7686112076044, 18.124444372951984]}, + "properties": { + "geohash": "8g", + "value": 2, + "aggConfigResult": { + "key": 2, + "value": 2, + "aggConfig": {"id": "1", "enabled": true, "type": "count", "schema": "metric", "params": {}}, + "$parent": { + "key": "8g", + "value": "8g", + "aggConfig": { + "id": "2", + "enabled": true, + "type": "geohash_grid", + "schema": "segment", + "params": {"field": "geo.coordinates", "autoPrecision": true, "useGeocentroid": true, "precision": 2} + }, + "$order": 139, + "type": "bucket" + }, + "$order": 140, + "type": "metric" + }, + "center": [19.6875, -140.625], + "rectangle": [[16.875, -146.25], [16.875, -135], [22.5, -135], [22.5, -146.25]] + } + }], + "properties": {"min": 2, "max": 1418, "zoom": 3, "center": [39.57182223734374, -109.51171875]} +}`; + +export default JSON.parse(sampleData); diff --git a/src/ui/public/vis_maps/__tests__/kibana_map.js b/src/ui/public/vis_maps/__tests__/kibana_map.js new file mode 100644 index 0000000000000..72f9401a7e538 --- /dev/null +++ b/src/ui/public/vis_maps/__tests__/kibana_map.js @@ -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 + }); + }); + }); + + }); + + +}); diff --git a/src/ui/public/vis_maps/__tests__/tile_maps/map.js b/src/ui/public/vis_maps/__tests__/tile_maps/map.js deleted file mode 100644 index b12634fca1327..0000000000000 --- a/src/ui/public/vis_maps/__tests__/tile_maps/map.js +++ /dev/null @@ -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 = $('
'); - 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]); - }); - }); - }); -}); diff --git a/src/ui/public/vis_maps/__tests__/tile_maps/markers.js b/src/ui/public/vis_maps/__tests__/tile_maps/markers.js deleted file mode 100644 index 1422bd1a41918..0000000000000 --- a/src/ui/public/vis_maps/__tests__/tile_maps/markers.js +++ /dev/null @@ -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); - }); - }); - }); - -}); diff --git a/src/ui/public/vis_maps/__tests__/tile_maps/tile_map.js b/src/ui/public/vis_maps/__tests__/tile_maps/tile_map.js deleted file mode 100644 index 9b942e22f3bd1..0000000000000 --- a/src/ui/public/vis_maps/__tests__/tile_maps/tile_map.js +++ /dev/null @@ -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 = $('
'); - -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 = $('
'); - 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); - }); - }); - }); -}); diff --git a/src/ui/public/vis_maps/__tests__/tile_maps/tilemap_settings.js b/src/ui/public/vis_maps/__tests__/tilemap_settings.js similarity index 100% rename from src/ui/public/vis_maps/__tests__/tile_maps/tilemap_settings.js rename to src/ui/public/vis_maps/__tests__/tilemap_settings.js diff --git a/src/ui/public/vis_maps/__tests__/tile_maps/tilemap_settings_mocked.js b/src/ui/public/vis_maps/__tests__/tilemap_settings_mocked.js similarity index 100% rename from src/ui/public/vis_maps/__tests__/tile_maps/tilemap_settings_mocked.js rename to src/ui/public/vis_maps/__tests__/tilemap_settings_mocked.js diff --git a/src/ui/public/vis_maps/geohash_layer.js b/src/ui/public/vis_maps/geohash_layer.js new file mode 100644 index 0000000000000..288cfcef4cd4f --- /dev/null +++ b/src/ui/public/vis_maps/geohash_layer.js @@ -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; + } + } +} + + + diff --git a/src/ui/public/vis_maps/kibana_map.js b/src/ui/public/vis_maps/kibana_map.js new file mode 100644 index 0000000000000..1234b1d725d8a --- /dev/null +++ b/src/ui/public/vis_maps/kibana_map.js @@ -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('') + .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 = $('
').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 = $('
').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; + diff --git a/src/ui/public/vis_maps/kibana_map_layer.js b/src/ui/public/vis_maps/kibana_map_layer.js new file mode 100644 index 0000000000000..97a068ca25013 --- /dev/null +++ b/src/ui/public/vis_maps/kibana_map_layer.js @@ -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() { + } +} + + + + + + + + diff --git a/src/ui/public/vis_maps/lib/data.js b/src/ui/public/vis_maps/lib/data.js deleted file mode 100644 index fea6e2363dbd8..0000000000000 --- a/src/ui/public/vis_maps/lib/data.js +++ /dev/null @@ -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; -} diff --git a/src/ui/public/vis_maps/lib/dispatch.js b/src/ui/public/vis_maps/lib/dispatch.js deleted file mode 100644 index 68eec629892d9..0000000000000 --- a/src/ui/public/vis_maps/lib/dispatch.js +++ /dev/null @@ -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; -} diff --git a/src/ui/public/vis_maps/lib/layout.js b/src/ui/public/vis_maps/lib/layout.js deleted file mode 100644 index 24402f960c136..0000000000000 --- a/src/ui/public/vis_maps/lib/layout.js +++ /dev/null @@ -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; -} diff --git a/src/ui/public/vis_maps/lib/maps_config.js b/src/ui/public/vis_maps/lib/maps_config.js deleted file mode 100644 index 3e5a036a4ca55..0000000000000 --- a/src/ui/public/vis_maps/lib/maps_config.js +++ /dev/null @@ -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; -} diff --git a/src/ui/public/vis_maps/lib/splits/map_split.js b/src/ui/public/vis_maps/lib/splits/map_split.js deleted file mode 100644 index 710c77083927a..0000000000000 --- a/src/ui/public/vis_maps/lib/splits/map_split.js +++ /dev/null @@ -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); - } - }); - }; - }; -}); diff --git a/src/ui/public/vis_maps/lib/tilemap_settings.js b/src/ui/public/vis_maps/lib/tilemap_settings.js index 0cda8efd7444e..610c4e480f197 100644 --- a/src/ui/public/vis_maps/lib/tilemap_settings.js +++ b/src/ui/public/vis_maps/lib/tilemap_settings.js @@ -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 diff --git a/src/ui/public/vis_maps/maps.js b/src/ui/public/vis_maps/maps.js deleted file mode 100644 index 5a23cf13a48d5..0000000000000 --- a/src/ui/public/vis_maps/maps.js +++ /dev/null @@ -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; -} diff --git a/src/ui/public/vis_maps/maps_renderbot.js b/src/ui/public/vis_maps/maps_renderbot.js index 19a3cd7921655..b2c34c5dc9aaa 100644 --- a/src/ui/public/vis_maps/maps_renderbot.js +++ b/src/ui/public/vis_maps/maps_renderbot.js @@ -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' - }); - - _.class(MapsRenderbot).inherits(Renderbot); - function MapsRenderbot(vis, $el, uiState) { - MapsRenderbot.Super.call(this, vis, $el, uiState); - this._createVis(); - } + const notify = new Notifier({ location: 'Tilemap' }); + + 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; - 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 - //Even though the basemap will not display, the user will at least still see the overlay data - notify.warning(tilemapSettings.getError().message); + + this._resizeChecker = new ResizeChecker($el); + this._resizeChecker.on('resize', () => { + if (this._kibanaMap) { + this._kibanaMap.resize(); + } + }); } - 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); - }); + async _makeKibanaMap($el) { + + if (!tilemapSettings.isInitialized()) { + await tilemapSettings.loadSettings(); + } + + if (tilemapSettings.getError()) { + //Still allow the visualization to be built, but show a toast that there was a problem retrieving map settings + //Even though the basemap will not display, the user will at least still see the overlay data + notify.warning(tilemapSettings.getError().message); + } + + 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(); + } + }); + + + 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; + }); + } - if (this.mapsData) { - this.mapsVis.render(this.mapsData, this.uiState); + _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); } - }; - MapsRenderbot.prototype._getMapsParams = function () { - const self = this; - return _.assign( - {}, - self.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() - }, - self.vis.params - ); - }; + /** + * 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(); + }); + } - 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); - }); - }; + destroy() { + if (this._kibanaMap) { + this._kibanaMap.destroy(); + } + } - MapsRenderbot.prototype.destroy = function () { - const self = this; + /** + * 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(); + }); + } - const mapsVis = self.mapsVis; + _getMapsParams() { + return _.assign( + {}, + this.vis.type.params.defaults, + { + 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. + }, + 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 + } + }; + } + + _doRenderComplete() { + if (this._paramsDirty || this._dataDirty || this._baseLayerDirty) { + return; + } + $(this.el).trigger('renderComplete'); + } - _.forOwn(self.vis.listeners, function (listener, event) { - mapsVis.off(event, listener); - }); + } - mapsVis.destroy(); - }; + function addSpatialFilter(agg, filterName, filterData) { + if (!agg) { + return; + } - MapsRenderbot.prototype.updateParams = function () { - const self = this; + const indexPatternName = agg.vis.indexPattern.id; + const field = agg.fieldName(); + const filter = {}; + filter[filterName] = {}; + filter[filterName][field] = filterData; - // get full maps params object - const newParams = self._getMapsParams(); + const putFilter = Private(FilterBarPushFilterProvider)(getAppState()); + return putFilter(filter, false, indexPatternName); + } - // if there's been a change, replace the vis - if (!_.isEqual(newParams, self.mapsParams)) self._createVis(); - }; return MapsRenderbot; }; + + diff --git a/src/ui/public/vis_maps/markers/geohash_grid.js b/src/ui/public/vis_maps/markers/geohash_grid.js new file mode 100644 index 0000000000000..753125384c5ba --- /dev/null +++ b/src/ui/public/vis_maps/markers/geohash_grid.js @@ -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); + }; + } +} diff --git a/src/ui/public/vis_maps/markers/heatmap.js b/src/ui/public/vis_maps/markers/heatmap.js new file mode 100644 index 0000000000000..769899ff35096 --- /dev/null +++ b/src/ui/public/vis_maps/markers/heatmap.js @@ -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]; + }); +} + diff --git a/src/ui/public/vis_maps/markers/scaled_circles.js b/src/ui/public/vis_maps/markers/scaled_circles.js new file mode 100644 index 0000000000000..cc1c2504128de --- /dev/null +++ b/src/ui/public/vis_maps/markers/scaled_circles.js @@ -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 = $('
').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 = $('
'); + const icon = $('').css({ + background: color, + 'border-color': makeColorDarker(color) + }); + + const text = $('').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 + }; + }; +} diff --git a/src/ui/public/vis_maps/markers/shaded_circles.js b/src/ui/public/vis_maps/markers/shaded_circles.js new file mode 100644 index 0000000000000..8c67f4fcdf396 --- /dev/null +++ b/src/ui/public/vis_maps/markers/shaded_circles.js @@ -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]); + } +} diff --git a/src/ui/public/vis_maps/visualizations/_chart.js b/src/ui/public/vis_maps/visualizations/_chart.js deleted file mode 100644 index 59fec8338b314..0000000000000 --- a/src/ui/public/vis_maps/visualizations/_chart.js +++ /dev/null @@ -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; -} diff --git a/src/ui/public/vis_maps/visualizations/_map.js b/src/ui/public/vis_maps/visualizations/_map.js deleted file mode 100644 index f77279dec63c5..0000000000000 --- a/src/ui/public/vis_maps/visualizations/_map.js +++ /dev/null @@ -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('') - .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 = '

' + _.escape(mapLabel) + '

'; - }; - - // 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; -} diff --git a/src/ui/public/vis_maps/visualizations/marker_types/base_marker.js b/src/ui/public/vis_maps/visualizations/marker_types/base_marker.js deleted file mode 100644 index 7a6e1deeffc10..0000000000000 --- a/src/ui/public/vis_maps/visualizations/marker_types/base_marker.js +++ /dev/null @@ -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 = $('
').addClass('tilemap-legend-wrapper'); - const $div = $('
').addClass('tilemap-legend'); - $wrapper.append($div); - - const titleText = self.getLabel(); - const $title = $('
').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 = $('
'); - - const icon = $('').css({ - background: color, - 'border-color': self.darkerColor(color) - }); - - const text = $('').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; -} diff --git a/src/ui/public/vis_maps/visualizations/marker_types/geohash_grid.js b/src/ui/public/vis_maps/visualizations/marker_types/geohash_grid.js deleted file mode 100644 index 99f63311b6873..0000000000000 --- a/src/ui/public/vis_maps/visualizations/marker_types/geohash_grid.js +++ /dev/null @@ -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; -} diff --git a/src/ui/public/vis_maps/visualizations/marker_types/heatmap.js b/src/ui/public/vis_maps/visualizations/marker_types/heatmap.js deleted file mode 100644 index 46507c47c9607..0000000000000 --- a/src/ui/public/vis_maps/visualizations/marker_types/heatmap.js +++ /dev/null @@ -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; -} diff --git a/src/ui/public/vis_maps/visualizations/marker_types/scaled_circles.js b/src/ui/public/vis_maps/visualizations/marker_types/scaled_circles.js deleted file mode 100644 index 7eed1d6653977..0000000000000 --- a/src/ui/public/vis_maps/visualizations/marker_types/scaled_circles.js +++ /dev/null @@ -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; -} diff --git a/src/ui/public/vis_maps/visualizations/marker_types/shaded_circles.js b/src/ui/public/vis_maps/visualizations/marker_types/shaded_circles.js deleted file mode 100644 index a498056af1070..0000000000000 --- a/src/ui/public/vis_maps/visualizations/marker_types/shaded_circles.js +++ /dev/null @@ -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; -} diff --git a/src/ui/public/vis_maps/visualizations/tile_map.js b/src/ui/public/vis_maps/visualizations/tile_map.js deleted file mode 100644 index 1c423d6fee24a..0000000000000 --- a/src/ui/public/vis_maps/visualizations/tile_map.js +++ /dev/null @@ -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; -} diff --git a/test/functional/apps/visualize/_tile_map.js b/test/functional/apps/visualize/_tile_map.js deleted file mode 100644 index 586e121049797..0000000000000 --- a/test/functional/apps/visualize/_tile_map.js +++ /dev/null @@ -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); - }); - }); - - - }); -}); diff --git a/test/functional/apps/visualize/index.js b/test/functional/apps/visualize/index.js index d4cf50d7b7fe3..f37c917bd870d 100644 --- a/test/functional/apps/visualize/index.js +++ b/test/functional/apps/visualize/index.js @@ -36,7 +36,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');