diff --git a/x-pack/plugins/gis/public/actions/store_actions.js b/x-pack/plugins/gis/public/actions/store_actions.js index 257734beee39a..c7a2571695393 100644 --- a/x-pack/plugins/gis/public/actions/store_actions.js +++ b/x-pack/plugins/gis/public/actions/store_actions.js @@ -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, diff --git a/x-pack/plugins/gis/public/components/map/mb/index.js b/x-pack/plugins/gis/public/components/map/mb/index.js index bc4418ba2005a..be7f44ba1908a 100644 --- a/x-pack/plugins/gis/public/components/map/mb/index.js +++ b/x-pack/plugins/gis/public/components/map/mb/index.js @@ -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)) }; } diff --git a/x-pack/plugins/gis/public/components/map/mb/utils.js b/x-pack/plugins/gis/public/components/map/mb/utils.js index 4403a992a4eb0..fe08537c85131 100644 --- a/x-pack/plugins/gis/public/components/map/mb/utils.js +++ b/x-pack/plugins/gis/public/components/map/mb/utils.js @@ -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) => { diff --git a/x-pack/plugins/gis/public/components/map/mb/view.js b/x-pack/plugins/gis/public/components/map/mb/view.js index ab7b456e2c9d0..b0192d6f67a65 100644 --- a/x-pack/plugins/gis/public/components/map/mb/view.js +++ b/x-pack/plugins/gis/public/components/map/mb/view.js @@ -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); }; diff --git a/x-pack/plugins/gis/public/components/widget_overlay/layer_control/layer_toc/toc_entry/view.js b/x-pack/plugins/gis/public/components/widget_overlay/layer_control/layer_toc/toc_entry/view.js index 0ad1a9e10fa52..6b933b147f4c4 100644 --- a/x-pack/plugins/gis/public/components/widget_overlay/layer_control/layer_toc/toc_entry/view.js +++ b/x-pack/plugins/gis/public/components/widget_overlay/layer_control/layer_toc/toc_entry/view.js @@ -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' } > diff --git a/x-pack/plugins/gis/public/shared/components/layer_toc_actions.js b/x-pack/plugins/gis/public/shared/components/layer_toc_actions.js index da07fbfc335c9..7fa88e1b9da25 100644 --- a/x-pack/plugins/gis/public/shared/components/layer_toc_actions.js +++ b/x-pack/plugins/gis/public/shared/components/layer_toc_actions.js @@ -77,14 +77,14 @@ export class LayerTocActions extends Component { _renderIcon() { const { zoom, layer } = this.props; let smallLegendIcon; - if (layer.dataHasLoadError()) { + if (layer.hasErrors()) { smallLegendIcon = ( ); } else if (layer.isLayerLoading()) { diff --git a/x-pack/plugins/gis/public/shared/layers/layer.js b/x-pack/plugins/gis/public/shared/layers/layer.js index 552d701819f51..8be7a06e86af0 100644 --- a/x-pack/plugins/gis/public/shared/layers/layer.js +++ b/x-pack/plugins/gis/public/shared/layers/layer.js @@ -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 []; } diff --git a/x-pack/plugins/gis/public/shared/layers/sources/ems_tms_source/ems_tms_source.js b/x-pack/plugins/gis/public/shared/layers/sources/ems_tms_source/ems_tms_source.js index 360c48e346904..129f00ee6a541 100644 --- a/x-pack/plugins/gis/public/shared/layers/sources/ems_tms_source/ems_tms_source.js +++ b/x-pack/plugins/gis/public/shared/layers/sources/ems_tms_source/ems_tms_source.js @@ -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; } - - } diff --git a/x-pack/plugins/gis/public/shared/layers/tile_layer.js b/x-pack/plugins/gis/public/shared/layers/tile_layer.js index 8c10489e9f7e7..75e42d8123025 100644 --- a/x-pack/plugins/gis/public/shared/layers/tile_layer.js +++ b/x-pack/plugins/gis/public/shared/layers/tile_layer.js @@ -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; + } + }; + } + }); + + return new Promise((resolve, reject) => { + let tileLoadTimer = null; - syncLayerWithMB(mbMap) { + 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; + } } diff --git a/x-pack/plugins/gis/public/shared/layers/util/data_request.js b/x-pack/plugins/gis/public/shared/layers/util/data_request.js index f7d917476241b..5abc2f7d088a7 100644 --- a/x-pack/plugins/gis/public/shared/layers/util/data_request.js +++ b/x-pack/plugins/gis/public/shared/layers/util/data_request.js @@ -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; } diff --git a/x-pack/plugins/gis/public/store/map.js b/x-pack/plugins/gis/public/store/map.js index 2c2abc2ab44ed..3aa6146486b39 100644 --- a/x-pack/plugins/gis/public/store/map.js +++ b/x-pack/plugins/gis/public/store/map.js @@ -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) { diff --git a/x-pack/plugins/gis/server/routes.js b/x-pack/plugins/gis/server/routes.js index 01576da3c7c1f..d23bb1dd2d446 100644 --- a/x-pack/plugins/gis/server/routes.js +++ b/x-pack/plugins/gis/server/routes.js @@ -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();