[Maps] add initial location option that fits to data bounds (#74583)
* [Maps] add initial location option that fits to data bounds * update navigation_panel snapshot * add functional test to ensure sync is called when auto fit to bounds with no data * add functional test for auto fit to bounds on map load Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
0ef17f92a6
commit
75b8a3cb71
|
@ -234,6 +234,7 @@ export enum INITIAL_LOCATION {
|
|||
LAST_SAVED_LOCATION = 'LAST_SAVED_LOCATION',
|
||||
FIXED_LOCATION = 'FIXED_LOCATION',
|
||||
BROWSER_LOCATION = 'BROWSER_LOCATION',
|
||||
AUTO_FIT_TO_BOUNDS = 'AUTO_FIT_TO_BOUNDS',
|
||||
}
|
||||
|
||||
export enum LAYER_WIZARD_CATEGORY {
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { Dispatch } from 'redux';
|
||||
import bbox from '@turf/bbox';
|
||||
import uuid from 'uuid/v4';
|
||||
import { multiPoint } from '@turf/helpers';
|
||||
import { FeatureCollection } from 'geojson';
|
||||
import { MapStoreState } from '../reducers/store';
|
||||
|
@ -133,7 +134,7 @@ export function syncDataForAllLayers() {
|
|||
};
|
||||
}
|
||||
|
||||
export function syncDataForAllJoinLayers() {
|
||||
function syncDataForAllJoinLayers() {
|
||||
return async (dispatch: Dispatch, getState: () => MapStoreState) => {
|
||||
const syncPromises = getLayerList(getState())
|
||||
.filter((layer) => {
|
||||
|
@ -318,7 +319,7 @@ export function fitToLayerExtent(layerId: string) {
|
|||
};
|
||||
}
|
||||
|
||||
export function fitToDataBounds() {
|
||||
export function fitToDataBounds(onNoBounds?: () => void) {
|
||||
return async (dispatch: Dispatch, getState: () => MapStoreState) => {
|
||||
const layerList = getFittableLayers(getState());
|
||||
|
||||
|
@ -365,6 +366,9 @@ export function fitToDataBounds() {
|
|||
}
|
||||
|
||||
if (!corners.length) {
|
||||
if (onNoBounds) {
|
||||
onNoBounds();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -374,6 +378,32 @@ export function fitToDataBounds() {
|
|||
};
|
||||
}
|
||||
|
||||
let lastSetQueryCallId: string = '';
|
||||
export function autoFitToBounds() {
|
||||
return async (dispatch: Dispatch) => {
|
||||
// Method can be triggered before async actions complete
|
||||
// Use localSetQueryCallId to only continue execution path if method has not been re-triggered.
|
||||
const localSetQueryCallId = uuid();
|
||||
lastSetQueryCallId = localSetQueryCallId;
|
||||
|
||||
// Joins are performed on the client.
|
||||
// As a result, bounds for join layers must also be performed on the client.
|
||||
// Therefore join layers need to fetch data prior to auto fitting bounds.
|
||||
await dispatch<any>(syncDataForAllJoinLayers());
|
||||
|
||||
if (localSetQueryCallId === lastSetQueryCallId) {
|
||||
// In cases where there are no bounds, such as no matching documents, fitToDataBounds does not trigger setGotoWithBounds.
|
||||
// Ensure layer syncing occurs when setGotoWithBounds is not triggered.
|
||||
function onNoBounds() {
|
||||
if (localSetQueryCallId === lastSetQueryCallId) {
|
||||
dispatch<any>(syncDataForAllLayers());
|
||||
}
|
||||
}
|
||||
dispatch<any>(fitToDataBounds(onNoBounds));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function setGotoWithBounds(bounds: MapExtent) {
|
||||
return {
|
||||
type: SET_GOTO,
|
||||
|
|
|
@ -125,8 +125,6 @@ export function addLayer(layerDescriptor: LayerDescriptor) {
|
|||
};
|
||||
}
|
||||
|
||||
// Do not use when rendering a map. Method exists to enable selectors for getLayerList when
|
||||
// rendering is not needed.
|
||||
export function addLayerWithoutDataSync(layerDescriptor: LayerDescriptor) {
|
||||
return {
|
||||
type: ADD_LAYER,
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
import { Dispatch } from 'redux';
|
||||
import turfBboxPolygon from '@turf/bbox-polygon';
|
||||
import turfBooleanContains from '@turf/boolean-contains';
|
||||
import uuid from 'uuid/v4';
|
||||
|
||||
import { Filter, Query, TimeRange } from 'src/plugins/data/public';
|
||||
import { MapStoreState } from '../reducers/store';
|
||||
|
@ -44,12 +43,8 @@ import {
|
|||
UPDATE_DRAW_STATE,
|
||||
UPDATE_MAP_SETTING,
|
||||
} from './map_action_constants';
|
||||
import {
|
||||
fitToDataBounds,
|
||||
syncDataForAllJoinLayers,
|
||||
syncDataForAllLayers,
|
||||
} from './data_request_actions';
|
||||
import { addLayer } from './layer_actions';
|
||||
import { autoFitToBounds, syncDataForAllLayers } from './data_request_actions';
|
||||
import { addLayer, addLayerWithoutDataSync } from './layer_actions';
|
||||
import { MapSettings } from '../reducers/map';
|
||||
import {
|
||||
DrawState,
|
||||
|
@ -57,6 +52,7 @@ import {
|
|||
MapExtent,
|
||||
MapRefreshConfig,
|
||||
} from '../../common/descriptor_types';
|
||||
import { INITIAL_LOCATION } from '../../common/constants';
|
||||
import { scaleBounds } from '../elasticsearch_geo_utils';
|
||||
|
||||
export function setMapInitError(errorMessage: string) {
|
||||
|
@ -98,13 +94,21 @@ export function mapReady() {
|
|||
type: MAP_READY,
|
||||
});
|
||||
|
||||
getWaitingForMapReadyLayerListRaw(getState()).forEach((layerDescriptor) => {
|
||||
dispatch<any>(addLayer(layerDescriptor));
|
||||
});
|
||||
|
||||
const waitingForMapReadyLayerList = getWaitingForMapReadyLayerListRaw(getState());
|
||||
dispatch({
|
||||
type: CLEAR_WAITING_FOR_MAP_READY_LAYER_LIST,
|
||||
});
|
||||
|
||||
if (getMapSettings(getState()).initialLocation === INITIAL_LOCATION.AUTO_FIT_TO_BOUNDS) {
|
||||
waitingForMapReadyLayerList.forEach((layerDescriptor) => {
|
||||
dispatch<any>(addLayerWithoutDataSync(layerDescriptor));
|
||||
});
|
||||
dispatch<any>(autoFitToBounds());
|
||||
} else {
|
||||
waitingForMapReadyLayerList.forEach((layerDescriptor) => {
|
||||
dispatch<any>(addLayer(layerDescriptor));
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -196,7 +200,6 @@ function generateQueryTimestamp() {
|
|||
return new Date().toISOString();
|
||||
}
|
||||
|
||||
let lastSetQueryCallId: string = '';
|
||||
export function setQuery({
|
||||
query,
|
||||
timeFilters,
|
||||
|
@ -227,18 +230,7 @@ export function setQuery({
|
|||
});
|
||||
|
||||
if (getMapSettings(getState()).autoFitToDataBounds) {
|
||||
// Joins are performed on the client.
|
||||
// As a result, bounds for join layers must also be performed on the client.
|
||||
// Therefore join layers need to fetch data prior to auto fitting bounds.
|
||||
const localSetQueryCallId = uuid();
|
||||
lastSetQueryCallId = localSetQueryCallId;
|
||||
await dispatch<any>(syncDataForAllJoinLayers());
|
||||
|
||||
// setQuery can be triggered before async data fetching completes
|
||||
// Only continue execution path if setQuery has not been re-triggered.
|
||||
if (localSetQueryCallId === lastSetQueryCallId) {
|
||||
dispatch<any>(fitToDataBounds());
|
||||
}
|
||||
dispatch<any>(autoFitToBounds());
|
||||
} else {
|
||||
await dispatch<any>(syncDataForAllLayers());
|
||||
}
|
||||
|
|
|
@ -41,5 +41,10 @@ export async function getInitialView(
|
|||
});
|
||||
}
|
||||
|
||||
if (settings.initialLocation === INITIAL_LOCATION.AUTO_FIT_TO_BOUNDS) {
|
||||
// map bounds pulled from data sources. Just use default map location
|
||||
return null;
|
||||
}
|
||||
|
||||
return goto && goto.center ? goto.center : null;
|
||||
}
|
||||
|
|
|
@ -75,6 +75,10 @@ exports[`should render 1`] = `
|
|||
"id": "LAST_SAVED_LOCATION",
|
||||
"label": "Map location at save",
|
||||
},
|
||||
Object {
|
||||
"id": "AUTO_FIT_TO_BOUNDS",
|
||||
"label": "Auto fit map to data bounds",
|
||||
},
|
||||
Object {
|
||||
"id": "FIXED_LOCATION",
|
||||
"label": "Fixed location",
|
||||
|
@ -165,6 +169,10 @@ exports[`should render browser location form when initialLocation is BROWSER_LOC
|
|||
"id": "LAST_SAVED_LOCATION",
|
||||
"label": "Map location at save",
|
||||
},
|
||||
Object {
|
||||
"id": "AUTO_FIT_TO_BOUNDS",
|
||||
"label": "Auto fit map to data bounds",
|
||||
},
|
||||
Object {
|
||||
"id": "FIXED_LOCATION",
|
||||
"label": "Fixed location",
|
||||
|
@ -275,6 +283,10 @@ exports[`should render fixed location form when initialLocation is FIXED_LOCATIO
|
|||
"id": "LAST_SAVED_LOCATION",
|
||||
"label": "Map location at save",
|
||||
},
|
||||
Object {
|
||||
"id": "AUTO_FIT_TO_BOUNDS",
|
||||
"label": "Auto fit map to data bounds",
|
||||
},
|
||||
Object {
|
||||
"id": "FIXED_LOCATION",
|
||||
"label": "Fixed location",
|
||||
|
|
|
@ -41,6 +41,12 @@ const initialLocationOptions = [
|
|||
defaultMessage: 'Map location at save',
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: INITIAL_LOCATION.AUTO_FIT_TO_BOUNDS,
|
||||
label: i18n.translate('xpack.maps.mapSettingsPanel.autoFitToBoundsLocationLabel', {
|
||||
defaultMessage: 'Auto fit map to data bounds',
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: INITIAL_LOCATION.FIXED_LOCATION,
|
||||
label: i18n.translate('xpack.maps.mapSettingsPanel.fixedLocationLabel', {
|
||||
|
@ -125,7 +131,10 @@ export function NavigationPanel({ center, settings, updateMapSetting, zoom }: Pr
|
|||
};
|
||||
|
||||
function renderInitialLocationInputs() {
|
||||
if (settings.initialLocation === INITIAL_LOCATION.LAST_SAVED_LOCATION) {
|
||||
if (
|
||||
settings.initialLocation === INITIAL_LOCATION.LAST_SAVED_LOCATION ||
|
||||
settings.initialLocation === INITIAL_LOCATION.AUTO_FIT_TO_BOUNDS
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
@ -10,6 +10,23 @@ export default function ({ getPageObjects }) {
|
|||
const PageObjects = getPageObjects(['maps']);
|
||||
|
||||
describe('auto fit map to bounds', () => {
|
||||
describe('initial location', () => {
|
||||
before(async () => {
|
||||
await PageObjects.maps.loadSavedMap(
|
||||
'document example - auto fit to bounds for initial location'
|
||||
);
|
||||
});
|
||||
|
||||
it('should automatically fit to bounds on initial map load', async () => {
|
||||
const hits = await PageObjects.maps.getHits();
|
||||
expect(hits).to.equal('6');
|
||||
|
||||
const { lat, lon } = await PageObjects.maps.getView();
|
||||
expect(Math.round(lat)).to.equal(41);
|
||||
expect(Math.round(lon)).to.equal(-99);
|
||||
});
|
||||
});
|
||||
|
||||
describe('without joins', () => {
|
||||
before(async () => {
|
||||
await PageObjects.maps.loadSavedMap('document example');
|
||||
|
@ -25,10 +42,20 @@ export default function ({ getPageObjects }) {
|
|||
await PageObjects.maps.setAndSubmitQuery('machine.os.raw : "ios"');
|
||||
await PageObjects.maps.waitForMapPanAndZoom(origView);
|
||||
|
||||
const hits = await PageObjects.maps.getHits();
|
||||
expect(hits).to.equal('2');
|
||||
|
||||
const { lat, lon } = await PageObjects.maps.getView();
|
||||
expect(Math.round(lat)).to.equal(43);
|
||||
expect(Math.round(lon)).to.equal(-102);
|
||||
});
|
||||
|
||||
it('should sync layers even when there is not data', async () => {
|
||||
await PageObjects.maps.setAndSubmitQuery('machine.os.raw : "fake_os_with_no_matches"');
|
||||
|
||||
const hits = await PageObjects.maps.getHits();
|
||||
expect(hits).to.equal('0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('with joins', () => {
|
||||
|
|
|
@ -979,6 +979,35 @@
|
|||
}
|
||||
}
|
||||
|
||||
{
|
||||
"type": "doc",
|
||||
"value": {
|
||||
"id": "map:13776f20-db37-11ea-8fbb-3da39bb9bff2",
|
||||
"index": ".kibana",
|
||||
"source": {
|
||||
"map" : {
|
||||
"title" : "document example - auto fit to bounds for initial location",
|
||||
"description" : "",
|
||||
"mapStateJSON" : "{\"zoom\":5.2,\"center\":{\"lon\":-67.80052,\"lat\":-55.25331},\"timeFilters\":{\"from\":\"2015-09-20T00:00:00.000Z\",\"to\":\"2015-09-20T01:00:00.000Z\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":1000},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[],\"settings\":{\"autoFitToDataBounds\":false,\"initialLocation\":\"AUTO_FIT_TO_BOUNDS\",\"fixedLocation\":{\"lat\":0,\"lon\":0,\"zoom\":2},\"browserLocation\":{\"zoom\":2},\"maxZoom\":24,\"minZoom\":0,\"showSpatialFilters\":true,\"spatialFiltersAlpa\":0.3,\"spatialFiltersFillColor\":\"#DA8B45\",\"spatialFiltersLineColor\":\"#DA8B45\"}}",
|
||||
"layerListJSON" : "[{\"id\":\"0hmz5\",\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"id\":\"road_map\"},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"TILE\",\"properties\":{}},\"type\":\"VECTOR_TILE\",\"minZoom\":0,\"maxZoom\":24},{\"id\":\"z52lq\",\"label\":\"logstash\",\"minZoom\":0,\"maxZoom\":24,\"sourceDescriptor\":{\"id\":\"e1a5e1a6-676c-4a89-8ea9-0d91d64b73c6\",\"type\":\"ES_SEARCH\",\"geoField\":\"geo.coordinates\",\"limit\":2048,\"filterByMapBounds\":true,\"showTooltip\":true,\"tooltipProperties\":[],\"applyGlobalQuery\":true,\"scalingType\":\"LIMIT\",\"indexPatternRefName\":\"layer_1_source_index_pattern\"},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#e6194b\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"marker\"}}},\"previousStyle\":null},\"type\":\"VECTOR\"}]",
|
||||
"uiStateJSON" : "{\"isLayerTOCOpen\":true,\"openTOCDetails\":[]}"
|
||||
},
|
||||
"type" : "map",
|
||||
"references" : [
|
||||
{
|
||||
"name" : "layer_1_source_index_pattern",
|
||||
"type" : "index-pattern",
|
||||
"id" : "c698b940-e149-11e8-a35a-370a8516603a"
|
||||
}
|
||||
],
|
||||
"migrationVersion" : {
|
||||
"map" : "7.9.0"
|
||||
},
|
||||
"updated_at" : "2020-08-10T18:27:39.805Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
"type": "doc",
|
||||
"value": {
|
||||
|
|
Loading…
Reference in a new issue