Skip to content

Add a visual indicator when layers aren't successfully rendered #462

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
239 changes: 151 additions & 88 deletions packages/base/src/mainview/mainView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ interface IStates {
viewProjection: { code: string; units: string };
loadingLayer: boolean;
scale: number;
loadingErrors: Array<{ id: string; error: any; index: number }>;
loadingErrors: Array<{ id: string; error: any }>;
}

export class MainView extends React.Component<IProps, IStates> {
Expand Down Expand Up @@ -683,17 +683,50 @@ export class MainView extends React.Component<IProps, IStates> {
async updateSource(id: string, source: IJGISSource): Promise<void> {
// get the layer id associated with this source
const layerId = this._sourceToLayerMap.get(id);

// get the OL layer
const mapLayer = this.getLayer(layerId);
let mapLayer = this.getLayer(layerId);

if (!mapLayer) {
return;
// The layer was never added due to an invalid source
const layer = this._model.sharedModel.getLayer(layerId);
if (layer) {
await this.addLayer(layerId, layer, this._Map.getLayers().getLength());
mapLayer = this.getLayer(layerId);
if (!mapLayer) {
console.error(`Failed to add layer: ${layerId}`);
return;
}
} else {
return;
}
}
// remove source being updated

this.removeSource(id);
// create updated source
await this.addSource(id, source, layerId);
// change source of target layer
(mapLayer as Layer).setSource(this._sources[id]);
try {
await this.addSource(id, source, layerId);

// change source of target layer
(mapLayer as Layer).setSource(this._sources[id]);

const layer = this._model.sharedModel.getLayer(layerId);
if (layer) {
layer.failed = false;
layer.visible = true;
mapLayer.setVisible(layer.visible);
this._model.sharedModel.updateLayer(layerId, layer);
}
this.setState(old => ({
...old,
loadingLayer: false,
loadingErrors: old.loadingErrors.filter(item => item.id !== layerId)
}));
} catch (error: any) {
const layer = this._model.sharedModel.getLayer(layerId);
if (layer) {
await this.handleLayerError(layerId, layer, error, mapLayer);
}
}
}

/**
Expand Down Expand Up @@ -945,38 +978,25 @@ export class MainView extends React.Component<IProps, IStates> {
// Layer already exists
return;
}
this._sourceToLayerMap.set(layer.parameters?.source, id);

try {
const newMapLayer = await this._buildMapLayer(id, layer);
if (newMapLayer !== undefined) {
await this._waitForReady();

// Adjust index to ensure it's within bounds
const numLayers = this._Map.getLayers().getLength();
const safeIndex = Math.min(index, numLayers);
this._Map.getLayers().insertAt(safeIndex, newMapLayer);

this.setState(old => ({
...old,
loadingLayer: false,
loadingErrors: old.loadingErrors.filter(item => item.id !== id)
}));
}
} catch (error: any) {
if (
this.state.loadingErrors.find(
item => item.id === id && item.error === error.message
)
) {
this._loadingLayers.delete(id);
return;
}

await showErrorMessage(
`Error Adding ${layer.name}`,
`Failed to add ${layer.name}: ${error.message || 'invalid file path'}`
);
this.setState(old => ({ ...old, loadingLayer: false }));
this.state.loadingErrors.push({
id,
error: error.message || 'invalid file path',
index
});
this._loadingLayers.delete(id);
await this.handleLayerError(id, layer, error);
}
}

Expand Down Expand Up @@ -1109,79 +1129,83 @@ export class MainView extends React.Component<IProps, IStates> {
oldLayer: IDict,
mapLayer: Layer
): Promise<void> {
const sourceId = layer.parameters?.source;
const source = this._model.sharedModel.getLayerSource(sourceId);
if (!source) {
return;
}

if (!this._sources[sourceId]) {
await this.addSource(sourceId, source, id);
}

mapLayer.setVisible(layer.visible);
try {
const sourceId = layer.parameters?.source;
const source = this._model.sharedModel.getLayerSource(sourceId);
if (!source) {
return;
}

switch (layer.type) {
case 'RasterLayer': {
mapLayer.setOpacity(layer.parameters?.opacity || 1);
break;
if (!this._sources[sourceId]) {
await this.addSource(sourceId, source, id);
}
case 'VectorLayer': {
const layerParams = layer.parameters as IVectorLayer;

mapLayer.setOpacity(layerParams.opacity || 1);
mapLayer.setVisible(layer.visible);

(mapLayer as VectorLayer).setStyle(
this.vectorLayerStyleRuleBuilder(layer)
);
switch (layer.type) {
case 'RasterLayer': {
mapLayer.setOpacity(layer.parameters?.opacity || 1);
break;
}
case 'VectorLayer': {
const layerParams = layer.parameters as IVectorLayer;

break;
}
case 'VectorTileLayer': {
const layerParams = layer.parameters as IVectorTileLayer;
mapLayer.setOpacity(layerParams.opacity || 1);

mapLayer.setOpacity(layerParams.opacity || 1);
(mapLayer as VectorLayer).setStyle(
this.vectorLayerStyleRuleBuilder(layer)
);

(mapLayer as VectorTileLayer).setStyle(
this.vectorLayerStyleRuleBuilder(layer)
);
break;
}
case 'VectorTileLayer': {
const layerParams = layer.parameters as IVectorTileLayer;

break;
}
case 'HillshadeLayer': {
// TODO figure out color here
break;
}
case 'ImageLayer': {
break;
}
case 'WebGlLayer': {
mapLayer.setOpacity(layer.parameters?.opacity);
mapLayer.setOpacity(layerParams.opacity || 1);

if (layer?.parameters?.color) {
(mapLayer as WebGlTileLayer).setStyle({
color: layer.parameters.color
});
(mapLayer as VectorTileLayer).setStyle(
this.vectorLayerStyleRuleBuilder(layer)
);

break;
}
break;
}
case 'HeatmapLayer': {
const layerParams = layer.parameters as IHeatmapLayer;
const heatmap = mapLayer as HeatmapLayer;
case 'HillshadeLayer': {
// TODO figure out color here
break;
}
case 'ImageLayer': {
break;
}
case 'WebGlLayer': {
mapLayer.setOpacity(layer.parameters?.opacity);

if (oldLayer.feature !== layerParams.feature) {
// No way to change 'weight' attribute (feature used for heatmap stuff) so need to replace layer
this.replaceLayer(id, layer);
return;
if (layer?.parameters?.color) {
(mapLayer as WebGlTileLayer).setStyle({
color: layer.parameters.color
});
}
break;
}
case 'HeatmapLayer': {
const layerParams = layer.parameters as IHeatmapLayer;
const heatmap = mapLayer as HeatmapLayer;

heatmap.setOpacity(layerParams.opacity || 1);
heatmap.setBlur(layerParams.blur);
heatmap.setRadius(layerParams.radius);
heatmap.setGradient(
layerParams.color ?? ['#00f', '#0ff', '#0f0', '#ff0', '#f00']
);
if (oldLayer.feature !== layerParams.feature) {
// No way to change 'weight' attribute (feature used for heatmap stuff) so need to replace layer
this.replaceLayer(id, layer);
return;
}

heatmap.setOpacity(layerParams.opacity || 1);
heatmap.setBlur(layerParams.blur);
heatmap.setRadius(layerParams.radius);
heatmap.setGradient(
layerParams.color ?? ['#00f', '#0ff', '#0f0', '#ff0', '#f00']
);
}
}
} catch (error: any) {
console.error('Error updating layer:', error);
}
}

Expand Down Expand Up @@ -1714,6 +1738,45 @@ export class MainView extends React.Component<IProps, IStates> {
}
}

private async handleLayerError(
id: string,
layer: IJGISLayer,
error: any,
mapLayer?: Layer
): Promise<void> {
if (!error.message) {
error.message = 'Invalid file path';
}

if (
this.state.loadingErrors.find(
item => item.id === id && item.error === error.message
)
) {
this.setState(old => ({ ...old, loadingLayer: false }));
this._loadingLayers.delete(id);
return;
}

await showErrorMessage(
`Error Adding ${layer.name}`,
`Failed to add ${layer.name}: ${error.message}`
);

layer.visible = false;
mapLayer?.setVisible(layer.visible);
layer.failed = true;
this._model.sharedModel.updateLayer(id, layer);

this.setState(old => ({
...old,
loadingLayer: false,
loadingErrors: [...old.loadingErrors, { id, error: error.message }]
}));

this._loadingLayers.delete(id);
}

private _handleThemeChange = (): void => {
const lightTheme = isLightTheme();

Expand Down
1 change: 1 addition & 0 deletions packages/base/src/panelview/components/layers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,7 @@ function LayerComponent(props: ILayerProps): JSX.Element {
onDragOver={Private.onDragOver}
onDragEnd={Private.onDragEnd}
data-id={layerId}
style={{ textDecoration: layer.failed ? 'line-through' : 'none' }}
Copy link
Member

@mfisher87 mfisher87 Feb 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about displaying a warning icon ⚠️ and when you hover it you can see the error cause(s)?

>
<div
className={LAYER_TITLE_CLASS}
Expand Down
4 changes: 4 additions & 0 deletions packages/schema/src/schema/jgis.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@
},
"filters": {
"$ref": "#/definitions/jGISFilter"
},
"failed": {
"type": "boolean",
"default": false
}
}
},
Expand Down
10 changes: 5 additions & 5 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -775,7 +775,7 @@ __metadata:
pbf: ^4.0.1
pmtiles: ^3.0.7
proj4: ^2.14.0
proj4-list: ^1.0.2
proj4-list: ^1.0.4
react: ^18.0.1
rimraf: ^3.0.2
shpjs: ^6.1.0
Expand Down Expand Up @@ -8994,10 +8994,10 @@ __metadata:
languageName: node
linkType: hard

"proj4-list@npm:^1.0.2":
version: 1.0.2
resolution: "proj4-list@npm:1.0.2"
checksum: 8705bfb92b7572c514d98944e148a6ab8891dc69aa49388677a999180d46d374758ddf0b733068e242b1454de2565c9e9bfcfd1c6b035d93834a0a879cd696d8
"proj4-list@npm:^1.0.4":
version: 1.0.4
resolution: "proj4-list@npm:1.0.4"
checksum: 3833f61bbbc93684e97555fa11db72009954565ad56313d7dd306ab7c9e4fc480ab64280ae418c0108ee83d0ae0810b334a42dbf8f8f8be4d58371a29a8a35ef
languageName: node
linkType: hard

Expand Down