Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
1 change: 1 addition & 0 deletions src/actions/src/action-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ export const ActionTypes = {
REMOVE_NOTIFICATION: `${ACTION_PREFIX}REMOVE_NOTIFICATION`,
SET_LOCALE: `${ACTION_PREFIX}SET_LOCALE`,
LAYER_FILTERED_ITEMS_CHANGE: `${ACTION_PREFIX}LAYER_FILTERED_ITEMS_CHANGE`,
WMS_FEATURE_INFO: `${ACTION_PREFIX}WMS_FEATURE_INFO`,
SYNC_TIME_FILTER_WITH_LAYER_TIMELINE: `${ACTION_PREFIX}SYNC_TIME_FILTER_WITH_LAYER_TIMELINE`,
SYNC_TIME_FILTER_TIMELINE_MODE: `${ACTION_PREFIX}SYNC_TIME_FILTER_TIMELINE_MODE`,
TOGGLE_PANEL_LIST_VIEW: `${ACTION_PREFIX}TOGGLE_PANEL_LIST_VIEW`,
Expand Down
27 changes: 27 additions & 0 deletions src/actions/src/vis-state-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1629,6 +1629,33 @@ export function layerFilteredItemsChange(
};
}

export type WMSFeatureInfoAction = {
layer: Layer;
featureInfo: Array<{name: string; value: string}> | string | null;
coordinate?: [number, number] | null;
};

/**
* WMS layer feature info callback
* @memberof visStateActions
* @param layer
* @param featureInfo
* @param coordinate
* @return action
*/
export function wmsFeatureInfo(
layer: WMSFeatureInfoAction['layer'],
featureInfo: WMSFeatureInfoAction['featureInfo'],
coordinate?: WMSFeatureInfoAction['coordinate']
): Merge<WMSFeatureInfoAction, {type: typeof ActionTypes.WMS_FEATURE_INFO}> {
return {
type: ActionTypes.WMS_FEATURE_INFO,
layer,
featureInfo,
coordinate
};
}

export type SyncTimeFilterWithLayerTimelineAction = {
idx: number;
enable: boolean;
Expand Down
17 changes: 16 additions & 1 deletion src/components/src/map-container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,20 @@ export default function MapContainerFactory(
this.props.visStateActions.layerFilteredItemsChange(this.props.visState.layers[idx], event);
};

_onWMSFeatureInfo = (
idx: number,
data: {
featureInfo: Array<{name: string; value: string}> | string | null;
coordinate?: [number, number] | null;
}
) => {
this.props.visStateActions.wmsFeatureInfo(
this.props.visState.layers[idx],
data.featureInfo,
data.coordinate
);
};

_handleMapToggleLayer = layerId => {
const {index: mapIndex = 0, visStateActions} = this.props;
visStateActions.toggleLayerForMap(mapIndex, layerId);
Expand Down Expand Up @@ -823,7 +837,8 @@ export default function MapContainerFactory(
{
onLayerHover: this._onLayerHover,
onSetLayerDomain: this._onLayerSetDomain,
onFilteredItemsChange: this._onLayerFilteredItemsChange
onFilteredItemsChange: this._onLayerFilteredItemsChange,
onWMSFeatureInfo: this._onWMSFeatureInfo
},
deckGlProps
);
Expand Down
54 changes: 38 additions & 16 deletions src/components/src/map/layer-hover-info.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -138,22 +138,37 @@ const EntryInfoRow: React.FC<EntryInfoRowProps> = ({
const field = fields[fieldIdx];
const fieldValueAccessor = layer.accessVSFieldValue(field, currentTime);
const value = fieldValueAccessor(field, data instanceof DataRow ? {index: data._rowIndex} : data);
const primaryValue = primaryData
? fieldValueAccessor(
field,
primaryData instanceof DataRow ? {index: primaryData._rowIndex} : primaryData
)
: null;
const displayValue = getTooltipDisplayValue({item, field, value});

const displayDeltaValue = primaryData
? getTooltipDisplayDeltaValue({
field,
value,
primaryValue,
compareType
})
: null;
// Handle WMS layer data in comparison mode - WMS layers don't have comparable field data
let primaryValue = null;
let displayDeltaValue: string | null = null;

if (primaryData) {
try {
// Only calculate primary value if primaryData has a compatible structure
if (
primaryData instanceof DataRow ||
(primaryData && typeof primaryData === 'object' && 'index' in primaryData)
) {
primaryValue = fieldValueAccessor(
field,
primaryData instanceof DataRow ? {index: primaryData._rowIndex} : primaryData
);

displayDeltaValue = getTooltipDisplayDeltaValue({
field,
value,
primaryValue,
compareType
});
}
} catch (error) {
// If there's an error accessing primaryData (e.g., WMS layer data), skip comparison
primaryValue = null;
}
}

const displayValue = getTooltipDisplayValue({item, field, value});

return (
<Row
Expand Down Expand Up @@ -236,6 +251,7 @@ const LayerHoverInfoFactory = () => {

const hasFieldsToShow =
(data.fieldValues && Object.keys(data.fieldValues).length > 0) ||
(data.wmsFeatureData && data.wmsFeatureData.length > 0) ||
(props.fieldsToShow && props.fieldsToShow.length > 0);

return (
Expand All @@ -246,7 +262,13 @@ const LayerHoverInfoFactory = () => {
</StyledLayerName>
{hasFieldsToShow && <StyledDivider />}
<StyledTable>
{data.fieldValues ? (
{data.wmsFeatureData ? (
<tbody>
{data.wmsFeatureData.map(({name, value}, i) => (
<Row key={i} name={name} value={value} />
))}
</tbody>
) : data.fieldValues ? (
<tbody>
{data.fieldValues.map(({labelMessage, value}, i) => (
<Row key={i} name={intl.formatMessage({id: labelMessage})} value={value} />
Expand Down
3 changes: 2 additions & 1 deletion src/deckgl-layers/src/wms/wms-layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,8 @@ export default class WMSLayer extends CompositeLayer<Required<_WMSLayerProps>> {
? COORDINATE_SYSTEM.LNGLAT
: COORDINATE_SYSTEM.CARTESIAN,
bounds,
image
image,
pickable: this.props.pickable
})
);
}
Expand Down
183 changes: 167 additions & 16 deletions src/layers/src/wms-layer/wms-layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import {DatasetType, WMSDatasetMetadata, LAYER_TYPES} from '@kepler.gl/constants
import {notNullorUndefined} from '@kepler.gl/common-utils';
import {WMSLayer as DeckWMSLayer} from '@kepler.gl/deckgl-layers';
import {KeplerTable as KeplerDataset} from '@kepler.gl/table';
import {DataContainerInterface} from '@kepler.gl/utils';
import {AnimationConfig} from '@kepler.gl/types';

// Types
export type WMSTile = {
Expand All @@ -40,6 +42,7 @@ export type WMSLayerVisConfig = {
name: string;
title: string;
boundingBox: number[][];
queryable: boolean;
} | null;
};

Expand All @@ -65,6 +68,9 @@ export default class WMSLayer extends AbstractTileLayer<WMSTile, any[]> {
declare config: WMSLayerConfig;
declare visConfigSettings: WMSLayerVisConfigSettings;

// Store reference to the deck layer for feature info access
private deckLayerRef: DeckWMSLayer | null = null;

// Constructor
constructor(
props: ConstructorParameters<typeof AbstractTileLayer>[0] & {
Expand Down Expand Up @@ -149,42 +155,187 @@ export default class WMSLayer extends AbstractTileLayer<WMSTile, any[]> {
};
}

_getCurrentServiceLayer(dataset: KeplerDataset) {
_getCurrentServiceLayer() {
const {visConfig} = this.config;
return visConfig.wmsLayer ?? dataset.metadata?.layers?.[0] ?? null;
return visConfig.wmsLayer ?? null;
}

updateLayerMeta(dataset: KeplerDataset): void {
if (dataset.type !== DatasetType.WMS_TILE) {
return;
}

const currentLayer = this._getCurrentServiceLayer(dataset);
const currentLayer = this._getCurrentServiceLayer();
if (currentLayer && currentLayer.boundingBox) {
this.updateMeta({
bounds: currentLayer.boundingBox
});
}
}

hasHoveredObject(objectInfo: any) {
// For WMS layers, we consider it hovered if the layer is picked
// The actual feature info will be retrieved via getHoverData
if (this.isLayerHovered(objectInfo)) {
return {
index: 0, // WMS layers don't have discrete data points, so we use index 0
...objectInfo
};
}
return null;
}

getHoverData(
object: any,
dataContainer: DataContainerInterface,
fields: Field[],
animationConfig: AnimationConfig,
hoverInfo: {index: number; x?: number; y?: number}
) {
// Check if this is a WMS feature info object from clicked state
if (object?.wmsFeatureInfo) {
if (Array.isArray(object.wmsFeatureInfo)) {
return {
wmsFeatureData: object.wmsFeatureInfo
};
}

return {
wmsFeatureData: [
{
name: 'WMS Feature Info',
value: object.wmsFeatureInfo
}
]
};
}

if (hoverInfo.x !== undefined && hoverInfo.y !== undefined) {
return {
fieldValues: [
{
labelMessage: 'layer.wms.hover',
value: 'Click to query WMS feature info'
Comment thread
igorDykhta marked this conversation as resolved.
}
]
};
}

return null;
}

renderLayer(opts) {
const {visConfig} = this.config;
const {data} = opts;
const wmsLayer = visConfig.wmsLayer?.name;
const {data, interactionConfig, layerCallbacks} = opts;
const wmsLayer = this._getCurrentServiceLayer();
if (!wmsLayer) {
return [];
}
const {name: wmsLayerName, queryable} = wmsLayer;
const defaultLayerProps = this.getDefaultDeckLayerProps(opts);
const pickable = interactionConfig?.tooltip?.enabled && queryable;

const deckLayer = new DeckWMSLayer({
id: `${this.id}-WMSLayer` as string,
idx: defaultLayerProps.idx,
serviceType: 'wms',
data: data.tilesetDataUrl,
layers: [wmsLayerName],
opacity: visConfig.opacity,
transparent: visConfig.transparent,
pickable,
// @ts-ignore
onClick: pickable ? this._onClick.bind(this, layerCallbacks) : null
});

return [
new DeckWMSLayer({
id: `${this.id}-WMSLayer` as string,
serviceType: 'wms',
data: data.tilesetDataUrl,
layers: [wmsLayer],
opacity: visConfig.opacity,
transparent: visConfig.transparent,
pickable: false
})
];
// Store reference to the deck layer for feature info access
this.deckLayerRef = deckLayer;

return [deckLayer];
}

protected async _onClick(layerCallbacks, {bitmap, coordinate}) {
if (!bitmap) return null;

const x = bitmap.pixel[0];
const y = bitmap.pixel[1];
const featureInfo = await this.getWMSFeatureInfo(x, y);

// Call the callback to update state with coordinate
if (layerCallbacks?.onWMSFeatureInfo) {
layerCallbacks.onWMSFeatureInfo({featureInfo, coordinate});
}

return featureInfo;
}

// Method to retrieve WMS feature info asynchronously
protected async getWMSFeatureInfo(
x: number,
y: number
): Promise<Array<{name: string; value: string}> | null> {
try {
if (this.deckLayerRef && typeof this.deckLayerRef.getFeatureInfoText === 'function') {
const featureInfoXml = await this.deckLayerRef.getFeatureInfoText(x, y);
if (featureInfoXml) {
// Parse the XML response to extract attributes
const parsedAttributes = this.parseWMSFeatureInfo(featureInfoXml);
return parsedAttributes.length > 0 ? parsedAttributes : null;
}
}
return null;
} catch (error) {
console.warn('Failed to get WMS feature info:', error);
return null;
}
}

// Helper method to parse WMS XML response
protected parseWMSFeatureInfo(xmlString: string): Array<{name: string; value: string}> {
try {
// Simple XML parsing to extract feature attributes
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(xmlString, 'text/xml');

const attributes: Array<{name: string; value: string}> = [];

// Look for feature members
const featureMembers = xmlDoc.getElementsByTagName('gml:featureMember');

for (let i = 0; i < featureMembers.length; i++) {
const featureMember = featureMembers[i];

// Get all child elements that contain attribute data
const children = featureMember.children;

for (let j = 0; j < children.length; j++) {
const feature = children[j];
const featureChildren = feature.children;

// Extract attribute name-value pairs
for (let k = 0; k < featureChildren.length; k++) {
const attr = featureChildren[k];
const tagName = attr.tagName;
const value = attr.textContent || '';

// Clean up the tag name (remove namespace prefix)
const cleanName = tagName.includes(':') ? tagName.split(':')[1] : tagName;

// Skip empty values and geometry elements
if (value.trim() && !cleanName.toLowerCase().includes('geom')) {
attributes.push({
name: cleanName.replace(/_/g, ' ').toUpperCase(),
value: value.trim()
});
}
}
}
}

return attributes;
} catch (error) {
console.warn('Error parsing WMS feature info XML:', error);
return [];
}
}
}
3 changes: 3 additions & 0 deletions src/localization/src/translations/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,9 @@ export default {
rastertile: 'raster tile',
wms: 'WMS'
},
wms: {
hover: 'Value:'
},
Comment thread
igorDykhta marked this conversation as resolved.
layerUpdateError:
'An error occurred during layer update: {errorMessage}. Make sure the format of the input data is valid.',
interaction: 'Interaction'
Expand Down
Loading