[Maps] Handle unavailable tilemap services (#28852) (#29542)

* Update ems utils to better handle no service results. Prevent excess attribution errors

* Update tile layer sync to return promise and handle errors related to both obtaining url and tile loading

* Add flow for updating tms layers with error status/message

* Handle promises, if returned, on syncLayerWithMB. Update TMS error status

* Exclude layers that mapbox didn't add to map but are tracked in layer list from reordering logic

* Move datarequest handling to vector layer. Use relevant data load/error logic for tile and vector layers

* Don't try to get attributions on errored layer

* Handle 'includeElasticMapsService' configuration

* Move data requests back to layer level for heatmap usage

* Update all layers to set top-level layer error status and message. Consolidate redundant code

* Update tile sync function to more reliably confirm load status after loading via callback. Add interval to cancel timer

* Remove unnecessary, and annoying, clear temp layers on tms error

* Clean up

* More clean up

* Review feedback

* Review feedback. Test cleanup

* Test fixes and review feedback
This commit is contained in:
Aaron Caldwell 2019-01-29 14:39:59 -07:00 committed by GitHub
parent c763b2e11c
commit 079cff24a3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 149 additions and 92 deletions

View file

@ -21,6 +21,7 @@ import { timeService } from '../kibana_services';
export const SET_SELECTED_LAYER = 'SET_SELECTED_LAYER';
export const UPDATE_LAYER_ORDER = 'UPDATE_LAYER_ORDER';
export const ADD_LAYER = 'ADD_LAYER';
export const SET_LAYER_ERROR_STATUS = 'SET_LAYER_ERROR_STATUS';
export const ADD_WAITING_FOR_MAP_READY_LAYER = 'ADD_WAITING_FOR_MAP_READY_LAYER';
export const CLEAR_WAITING_FOR_MAP_READY_LAYER_LIST = 'CLEAR_WAITING_FOR_MAP_READY_LAYER_LIST';
export const REMOVE_LAYER = 'REMOVE_LAYER';
@ -108,6 +109,16 @@ export function addLayer(layerDescriptor) {
};
}
export function setLayerErrorStatus(id, errorMessage) {
return dispatch => {
dispatch({
type: SET_LAYER_ERROR_STATUS,
layerId: id,
errorMessage,
});
};
}
export function toggleLayerVisible(layerId) {
return {
type: TOGGLE_LAYER_VISIBLE,

View file

@ -12,7 +12,8 @@ import {
mapDestroyed,
setMouseCoordinates,
clearMouseCoordinates,
clearGoto
clearGoto,
setLayerErrorStatus,
} from '../../../actions/store_actions';
import { getLayerList, getMapReady, getGoto } from "../../../selectors/map_selectors";
@ -45,7 +46,9 @@ function mapDispatchToProps(dispatch) {
},
clearGoto: () => {
dispatch(clearGoto());
}
},
setLayerErrorStatus: (id, msg) =>
dispatch(setLayerErrorStatus(id, msg))
};
}

View file

@ -63,7 +63,8 @@ export function syncLayerOrder(mbMap, layerList) {
const mbLayers = mbMap.getStyle().layers.slice();
const currentLayerOrder = _.uniq( // Consolidate layers and remove suffix
mbLayers.map(({ id }) => id.substring(0, id.lastIndexOf('_'))));
const newLayerOrder = layerList.map(l => l.getId());
const newLayerOrder = layerList.map(l => l.getId())
.filter(layerId => currentLayerOrder.includes(layerId));
let netPos = 0;
let netNeg = 0;
const movementArr = currentLayerOrder.reduce((accu, id, idx) => {

View file

@ -200,9 +200,14 @@ export class MBMapContainer extends React.Component {
if (!isMapReady) {
return;
}
removeOrphanedSourcesAndLayers(this._mbMap, layerList);
layerList.forEach((layer) => {
layer.syncLayerWithMB(this._mbMap);
layerList.forEach(layer => {
if (!layer.hasErrors()) {
Promise.resolve(layer.syncLayerWithMB(this._mbMap))
.catch(({ message }) =>
this.props.setLayerErrorStatus(layer.getId(), message));
}
});
syncLayerOrder(this._mbMap, layerList);
};

View file

@ -82,7 +82,8 @@ export class TOCEntry extends React.Component {
alignItems="center"
responsive={false}
className={
layer.isVisible() && layer.showAtZoomLevel(zoom) && !layer.dataHasLoadError() ? 'gisTocEntry-visible' : 'gisTocEntry-notVisible'
layer.isVisible() && layer.showAtZoomLevel(zoom)
&& !layer.hasErrors() ? 'gisTocEntry-visible' : 'gisTocEntry-notVisible'
}
>
<EuiFlexItem grow={false}>

View file

@ -77,14 +77,14 @@ export class LayerTocActions extends Component {
_renderIcon() {
const { zoom, layer } = this.props;
let smallLegendIcon;
if (layer.dataHasLoadError()) {
if (layer.hasErrors()) {
smallLegendIcon = (
<EuiIconTip
aria-label="Load warning"
size="m"
type="alert"
color="warning"
content={layer.getDataLoadError()}
content={layer.getErrors()}
/>
);
} else if (layer.isLayerLoading()) {

View file

@ -17,7 +17,6 @@ export class AbstractLayer {
this._descriptor = AbstractLayer.createDescriptor(layerDescriptor);
this._source = source;
this._style = style;
if (this._descriptor.dataRequests) {
this._dataRequests = this._descriptor.dataRequests.map(dataRequest => new DataRequest(dataRequest));
} else {
@ -64,7 +63,10 @@ export class AbstractLayer {
}
async getAttributions() {
return await this._source.getAttributions();
if (!this.hasErrors()) {
return await this._source.getAttributions();
}
return [];
}
getLabel() {
@ -139,21 +141,20 @@ export class AbstractLayer {
return this._source.renderSourceSettingsEditor({ onChange });
};
getSourceDataRequest() {
return this._dataRequests.find(dataRequest => dataRequest.getDataId() === 'source');
}
isLayerLoading() {
return this._dataRequests.some(dataRequest => dataRequest.isLoading());
}
dataHasLoadError() {
return this._dataRequests.some(dataRequest => dataRequest.hasLoadError());
hasErrors() {
return _.get(this._descriptor, 'isInErrorState', false);
}
getDataLoadError() {
const loadErrors = this._dataRequests
.filter(dataRequest => dataRequest.hasLoadError())
.map(dataRequest => {
return dataRequest._descriptor.dataLoadError;
});
return loadErrors.join(',');
getErrors() {
return this.hasErrors() ? this._descriptor.errorMessage : '';
}
toLayerDescriptor() {
@ -219,10 +220,6 @@ export class AbstractLayer {
return style.renderEditor(options);
}
getSourceDataRequest() {
return this._dataRequests.find(dataRequest => dataRequest.getDataId() === 'source');
}
getIndexPatternIds() {
return [];
}

View file

@ -65,6 +65,10 @@ export class EMSTMSSource extends AbstractTMSSource {
}
_getTMSOptions() {
if(!this._emsTileServices) {
return;
}
return this._emsTileServices.find(service => {
return service.id === this._descriptor.id;
});
@ -90,9 +94,11 @@ export class EMSTMSSource extends AbstractTMSSource {
async getAttributions() {
const service = this._getTMSOptions();
const attributions = service.attributionMarkdown.split('|');
if (!service || !service.attributionMarkdown) {
return [];
}
return attributions.map((attribution) => {
return service.attributionMarkdown.split('|').map((attribution) => {
attribution = attribution.trim();
//this assumes attribution is plain markdown link
const extractLink = /\[(.*)\]\((.*)\)/;
@ -106,8 +112,9 @@ export class EMSTMSSource extends AbstractTMSSource {
getUrlTemplate() {
const service = this._getTMSOptions();
if (!service || !service.url) {
throw new Error('Can not generate EMS TMS url template');
}
return service.url;
}
}

View file

@ -10,6 +10,8 @@ import React from 'react';
import { EuiIcon } from '@elastic/eui';
import { TileStyle } from '../layers/styles/tile_style';
const TMS_LOAD_TIMEOUT = 32000;
export class TileLayer extends AbstractLayer {
static type = "TILE";
@ -30,29 +32,65 @@ export class TileLayer extends AbstractLayer {
return tileLayerDescriptor;
}
_tileLoadErrorTracker(map, url) {
let tileLoad;
map.on('dataloading', ({ tile }) => {
if (tile && tile.request) {
// If at least one tile loads, endpoint/resource is valid
tile.request.onloadend = ({ loaded }) => {
if (loaded) {
tileLoad = true;
}
};
}
});
syncLayerWithMB(mbMap) {
return new Promise((resolve, reject) => {
let tileLoadTimer = null;
const clearChecks = () => {
clearTimeout(tileLoadTimer);
map.off('dataloading');
};
tileLoadTimer = setTimeout(() => {
if (!tileLoad) {
reject(new Error(`Tiles from "${url}" could not be loaded`));
} else {
resolve();
}
clearChecks();
}, TMS_LOAD_TIMEOUT);
});
}
async syncLayerWithMB(mbMap) {
const source = mbMap.getSource(this.getId());
const layerId = this.getId() + '_raster';
if (!source) {
const url = this._source.getUrlTemplate();
mbMap.addSource(this.getId(), {
type: 'raster',
tiles: [url],
tileSize: 256,
scheme: 'xyz',
});
mbMap.addLayer({
id: layerId,
type: 'raster',
source: this.getId(),
minzoom: 0,
maxzoom: 22,
});
if (source) {
return;
}
const url = this._source.getUrlTemplate();
const sourceId = this.getId();
mbMap.addSource(sourceId, {
type: 'raster',
tiles: [url],
tileSize: 256,
scheme: 'xyz',
});
mbMap.addLayer({
id: layerId,
type: 'raster',
source: sourceId,
minzoom: 0,
maxzoom: 22,
});
await this._tileLoadErrorTracker(mbMap, url);
mbMap.setLayoutProperty(layerId, 'visibility', this.isVisible() ? 'visible' : 'none');
mbMap.setLayerZoomRange(layerId, this._descriptor.minZoom, this._descriptor.maxZoom);
this._style && this._style.setMBPaintProperties({
@ -73,5 +111,8 @@ export class TileLayer extends AbstractLayer {
/>
);
}
isLayerLoading() {
return false;
}
}

View file

@ -9,14 +9,6 @@ export class DataRequest {
this._descriptor = descriptor;
}
hasLoadError() {
return !!this._descriptor.dataHasLoadError;
}
getLoadError() {
return this._descriptor.dataLoadError;
}
getData() {
return this._descriptor.data;
}

View file

@ -11,6 +11,7 @@ import {
LAYER_DATA_LOAD_ENDED,
LAYER_DATA_LOAD_ERROR,
ADD_LAYER,
SET_LAYER_ERROR_STATUS,
ADD_WAITING_FOR_MAP_READY_LAYER,
CLEAR_WAITING_FOR_MAP_READY_LAYER_LIST,
REMOVE_LAYER,
@ -132,8 +133,11 @@ export function map(state = INITIAL_STATE, action) {
};
case LAYER_DATA_LOAD_STARTED:
return updateWithDataRequest(state, action);
case SET_LAYER_ERROR_STATUS:
return setErrorStatus(state, action);
case LAYER_DATA_LOAD_ERROR:
return updateWithDataLoadError(state, action);
const errorRequestResetState = resetDataRequest(state, action);
return setErrorStatus(errorRequestResetState, action);
case LAYER_DATA_LOAD_ENDED:
return updateWithDataResponse(state, action);
case TOUCH_LAYER:
@ -263,6 +267,15 @@ export function map(state = INITIAL_STATE, action) {
}
}
function setErrorStatus(state, { layerId, errorMessage }) {
const tmsErrorLayer = state.layerList.find(({ id }) => id === layerId);
return tmsErrorLayer
? updateLayerInList(
updateLayerInList(state, tmsErrorLayer.id, 'isInErrorState', true),
tmsErrorLayer.id, 'errorMessage', errorMessage)
: state;
}
function findDataRequest(layerDescriptor, dataRequestAction) {
if (!layerDescriptor.dataRequests) {
@ -276,24 +289,17 @@ function findDataRequest(layerDescriptor, dataRequestAction) {
function updateWithDataRequest(state, action) {
let dataRequest = getValidDataRequest(state, action, false);
const layerRequestingData = findLayerById(state, action.layerId);
if (!layerRequestingData) {
return state;
}
if (!layerRequestingData.dataRequests) {
layerRequestingData.dataRequests = [];
}
let dataRequest = findDataRequest(layerRequestingData, action);
if (!dataRequest) {
dataRequest = {
dataId: action.dataId
};
layerRequestingData.dataRequests.push(dataRequest);
layerRequestingData.dataRequests = [
...(layerRequestingData.dataRequests
? layerRequestingData.dataRequests : []), dataRequest ];
}
dataRequest.dataHasLoadError = false;
dataRequest.dataLoadError = null;
dataRequest.dataMetaAtStart = action.meta;
dataRequest.dataRequestToken = action.requestToken;
const layerList = [...state.layerList];
@ -301,59 +307,45 @@ function updateWithDataRequest(state, action) {
}
function updateWithDataResponse(state, action) {
const layerReceivingData = findLayerById(state, action.layerId);
if (!layerReceivingData) {
return state;
}
const dataRequest = findDataRequest(layerReceivingData, action);
if (!dataRequest) {
throw new Error('Data request should be initialized. Cannot call stopLoading before startLoading');
}
if (
dataRequest.dataRequestToken &&
dataRequest.dataRequestToken !== action.requestToken
) {
// ignore responses to outdated requests
return { ...state };
}
const dataRequest = getValidDataRequest(state, action);
if (!dataRequest) { return state; }
dataRequest.data = action.data;
dataRequest.dataMeta = { ...dataRequest.dataMetaAtStart, ...action.meta };
dataRequest.dataMetaAtStart = null;
return resetDataRequest(state, action, dataRequest);
}
function resetDataRequest(state, action, request) {
const dataRequest = request || getValidDataRequest(state, action);
if (!dataRequest) { return state; }
dataRequest.dataRequestToken = null;
dataRequest.dataId = action.dataId;
const layerList = [...state.layerList];
return { ...state, layerList };
}
function updateWithDataLoadError(state, action) {
function getValidDataRequest(state, action, checkRequestToken = true) {
const layer = findLayerById(state, action.layerId);
if (!layer) {
return state;
return;
}
const dataRequest = findDataRequest(layer, action);
if (!dataRequest) {
throw new Error('Data request should be initialized. Cannot call loadError before startLoading');
return;
}
if (
checkRequestToken &&
dataRequest.dataRequestToken &&
dataRequest.dataRequestToken !== action.requestToken
) {
// ignore responses to outdated requests
return state;
return;
}
dataRequest.dataHasLoadError = true;
dataRequest.dataLoadError = action.errorMessage;
dataRequest.dataRequestToken = null;
dataRequest.dataId = action.dataId;
const layerList = [...state.layerList];
return { ...state, layerList };
return dataRequest;
}
function findLayerById(state, id) {

View file

@ -79,6 +79,13 @@ export function initRoutes(server, licenseUid) {
async function getEMSResources(licenseUid) {
if (!mapConfig.includeElasticMapsService) {
return {
fileLayers: [],
tmsServices: []
};
}
emsClient.addQueryParams({ license: licenseUid });
const fileLayerObjs = await emsClient.getFileLayers();
const tmsServicesObjs = await emsClient.getTMSServices();