I am working on a Custom Filter Widget that takes a Feature Layer with a Unique Value Renderer and turns it into a set of filters that includes the Legend swatches. It works. I love it. I'll put it on Community soon. But it's not my end goal. I am trying to build a series of Widgets on top of it and I'm stuck on the technical challenge of finding the DataSource of a FeatureLayer at runtime.
The current goal is a Widget that contains a dropdown menu. The user selects a layer in it and it makes the filter. The closest version I have so far is to create a DataSource in the higher level Widget and then pass it down to the lower level Filter Widget. This will read all the necessary data from the FeatureLayer and handle map interactions, but it won't filter the data either on the Map or in a Table. I think I'm actually creating a duplicate of the existing DataSource and filtering that, but I haven't found a way to grab the existing DataSource directly.
Here's my current code to create a DataSource in the wrapping Widget:
const fetchDataSourceJson = async (dsId: string, layer: object): Promise<IMDataSourceJson> => {
const dsJson = await dataSourceJsonCreator.createDataSourceJsonByJSAPILayer(dsId, layer)
return dsJson
}
const createDataSource = async (dsId: string, layer: object): Promise<DataSource> => {
const dsJson = await fetchDataSourceJson(dsId, layer)
const dsOptions: FeatureLayerDataSourceConstructorOptions = {
id: dsId,
dataSourceJson: dsJson
}
return DataSourceManager.getInstance().createDataSource(dsOptions)
}
const getDataSources = () => {
const featureLayers = []
const allLayers = jimuMapView.view.map.layers
allLayers.forEach( async layer => {
if (layer.type === 'feature') {
console.log(layer)
if (layer.renderer?.uniqueValueInfos) {
const ds = await createDataSource(layer.id, layer)
ds.layer = layer
console.log(ds)
featureLayers.push(ds)
}
}
})
setTimeout(() => {
setAllDataSources(featureLayers)
}, 500)
}The completed Widget will look something like this.
Solved! Go to Solution.
Here is another version with a clause that captures an FeatureLayers added at runtime. Getting the DataSource for these was simple, but the layer contained within would always return undefined. I used the layer's title to search for the the layer in the mapView and associated it with the DataSource. This clause also had the side effect of duplicating the FeatureLayers in the Map inside a MapService. I added an additional clause to filter the duplicates back out.
const getDataSources = () => {
const featureLayers = []
const dsManager = DataSourceManager.getInstance()
const sources = dsManager.getDataSourcesAsArray()
//find each webmap
for (let i = 0; i < sources.length; i++) {
const source = sources[i]
if (source.type === 'WEB_MAP') {
//createChildDataSources in webmap
source.createChildDataSources().then(dss => {
//loop through childDataSource
dss.forEach(layerSource => {
//if featureLayer check render type in map, get dataSource if uniqueValues
if (layerSource.type === 'FEATURE_LAYER') {
const layer = jimuMapView.view.map.findLayerById(layerSource.layer.id)
if (layer.renderer.uniqueValueInfos) {
featureLayers.push(layerSource)
}
}
//if mapService createChildDataSources
if (layerSource.type === 'MAP_SERVICE') {
layerSource.createChildDataSources().then(childSource => {
//loop through childDataSources, check if uniqueValues in map
layerSource.layer.allSublayers.forEach((sublayer, j) => {
if (sublayer.renderer.uniqueValueInfos) {
featureLayers.push(childSource[j])
}
})
})
}
})
})
//This clause is for sources added at runtime. The source is created, but the layer will be undefined. This finds the correct layer and associates it with the source. The source does not have an id that can be used to retrieve the layer from the mapView, so we match the title.
} else if (source.type === 'FEATURE_LAYER' && !source.isDataView) {
console.log(source)
const name = source.dataSourceJson.sourceLabel
const mapLayers = jimuMapView.view.map.layers.items
let foundLayer = null
for (let j = 0; j < mapLayers.length; j++) {
const layer = mapLayers[j]
if (layer.title === name) {
foundLayer = layer
break
}
if (layer.sublayers) {
const subLayers = layer.allSublayers.items
for (let k = 0; k < subLayers.length; k++) {
if (subLayers[k].title === name) {
foundLayer = subLayers[k]
break
}
}
if (foundLayer) {
break
}
}
}
if (foundLayer) {
if (foundLayer.renderer.uniqueValueInfos) {
source.layer = foundLayer
featureLayers.push(source)
}
}
}
}
//wait for load to memory
setTimeout(() => {
//layers from MapService in WebMap will be duplicated. this removes the duplicates. I know it's stupid. I really can't find a simplier way without blocking all runtime added layers.
const filteredLayers = featureLayers.filter((obj, index, self) =>
index === self.findIndex((t) => (
t.id === obj.id
))
)
setAllDataSources(filteredLayers)
}, 500)
}
const getDataSources = () => {
const featureLayers = []
const dsManager = DataSourceManager.getInstance()
const sources = dsManager.getDataSourcesAsArray()
console.log(sources)
sources.forEach(source => {
if (source.type === 'WEB_MAP') {
console.log(source)
source.createChildDataSources().then(dss => {
console.log(dss)
dss.forEach(layerSource => {
if (layerSource.type === 'FEATURE_LAYER') {
console.log(layerSource.layer)
const layer = jimuMapView.view.map.findLayerById(layerSource.layer.id)
if (layer.renderer.uniqueValueInfos) {
featureLayers.push(layerSource)
}
}
if (layerSource.type === 'MAP_SERVICE') {
layerSource.createChildDataSources().then(childSource => {
//console.log(childSource)
layerSource.layer.allSublayers.forEach((sublayer, i) => {
if (sublayer.renderer.uniqueValueInfos) {
console.log(childSource[i])
featureLayers.push(childSource[i])
}
})
})
}
})
})
}
})
setTimeout(() => {
setAllDataSources(featureLayers)
}, 500)
}After three days of tinkering, here is a function that will find all FeatureLayer DataSources within Map Widgets, including those inside a Map Service and check that they have a uniqueValueRenderer.
Here is another version with a clause that captures an FeatureLayers added at runtime. Getting the DataSource for these was simple, but the layer contained within would always return undefined. I used the layer's title to search for the the layer in the mapView and associated it with the DataSource. This clause also had the side effect of duplicating the FeatureLayers in the Map inside a MapService. I added an additional clause to filter the duplicates back out.
const getDataSources = () => {
const featureLayers = []
const dsManager = DataSourceManager.getInstance()
const sources = dsManager.getDataSourcesAsArray()
//find each webmap
for (let i = 0; i < sources.length; i++) {
const source = sources[i]
if (source.type === 'WEB_MAP') {
//createChildDataSources in webmap
source.createChildDataSources().then(dss => {
//loop through childDataSource
dss.forEach(layerSource => {
//if featureLayer check render type in map, get dataSource if uniqueValues
if (layerSource.type === 'FEATURE_LAYER') {
const layer = jimuMapView.view.map.findLayerById(layerSource.layer.id)
if (layer.renderer.uniqueValueInfos) {
featureLayers.push(layerSource)
}
}
//if mapService createChildDataSources
if (layerSource.type === 'MAP_SERVICE') {
layerSource.createChildDataSources().then(childSource => {
//loop through childDataSources, check if uniqueValues in map
layerSource.layer.allSublayers.forEach((sublayer, j) => {
if (sublayer.renderer.uniqueValueInfos) {
featureLayers.push(childSource[j])
}
})
})
}
})
})
//This clause is for sources added at runtime. The source is created, but the layer will be undefined. This finds the correct layer and associates it with the source. The source does not have an id that can be used to retrieve the layer from the mapView, so we match the title.
} else if (source.type === 'FEATURE_LAYER' && !source.isDataView) {
console.log(source)
const name = source.dataSourceJson.sourceLabel
const mapLayers = jimuMapView.view.map.layers.items
let foundLayer = null
for (let j = 0; j < mapLayers.length; j++) {
const layer = mapLayers[j]
if (layer.title === name) {
foundLayer = layer
break
}
if (layer.sublayers) {
const subLayers = layer.allSublayers.items
for (let k = 0; k < subLayers.length; k++) {
if (subLayers[k].title === name) {
foundLayer = subLayers[k]
break
}
}
if (foundLayer) {
break
}
}
}
if (foundLayer) {
if (foundLayer.renderer.uniqueValueInfos) {
source.layer = foundLayer
featureLayers.push(source)
}
}
}
}
//wait for load to memory
setTimeout(() => {
//layers from MapService in WebMap will be duplicated. this removes the duplicates. I know it's stupid. I really can't find a simplier way without blocking all runtime added layers.
const filteredLayers = featureLayers.filter((obj, index, self) =>
index === self.findIndex((t) => (
t.id === obj.id
))
)
setAllDataSources(filteredLayers)
}, 500)
}