Select to view content in your preferred language

Customise symbol based on data separate from feature layer

786
4
Jump to solution
09-01-2022 10:03 PM
JasonDoingMaps
Emerging Contributor

Using ArcGIS JS API 4.x, I'm presenting a FeatureLayer, and want to customise the symbol used to render the features based on data that is not part of the layer, it's a completely separate not-ArcGIS data source.

More specifically, I have a feature layer on ArcGIS server, and the only object attribute saved on that layer is the unique ID for features. Those unique IDs are referenceed in a completely different data storage, which has the detail of what symbol I want to display. I can access that separate data storage client-side with JS, but I don't know how to "combine" the two sources of data.

I've looked at the option of subclassing SimpleRenderer, but I've been drawing a blank there as I can't figure out what functions I'd need to override that can let me specify a different symbol based on the unique ID of the feature. I might be missing something?

Alternatively, I wonder whether I could do some sort of client-side combining of the FeatureLayer along with the separate data source, and then use visualVariables in the SimpleRenderer. Whether that would require subclassing FeatureLayer, or there's some other way to combine data sources, I don't know.

0 Kudos
1 Solution

Accepted Solutions
ReneRubalcava
Honored Contributor

The JSAPI isn't open-source, so we don't provide the unminified code anywhere.

We only have custom layer extensibility for Tiled layers, not FeatureLayers.

We also have request interceptors, where you can modify the response before the layers use it, but no guarantee we don't use the service defined fields for other parts of the drawing and query stuff, you might be able to do some stuff here, but you're just adding to query time here too by pulling in new data requests.

https://developers.arcgis.com/javascript/latest/api-reference/esri-config.html#RequestInterceptor

If you have Enterprise portal, you can do this via a SOE. Doing this on the client is probably going to require a custom worker one way or another

View solution in original post

0 Kudos
4 Replies
ReneRubalcava
Honored Contributor

There is a similar question here

https://community.esri.com/t5/arcgis-api-for-javascript-questions/joining-api-response-json-data-to-...

You would need to create a client-side FeatureLayer and manually do the join. It might be worth using a web worker as linked in that post too if the operation blocks page interaction. I've had to do this in the past working an admin who was pretty adamant at keeping "business data" outside the GIS data, and it was always a headache, lol.

JasonDoingMaps
Emerging Contributor

Thanks for the input @ReneRubalcava , some great insights there.

As you pointed out in the linked "similar question" (emphasis mine):

Query all the features from your source FeatureLayer. This could be troublesome if you have a large amount of data, so you might need to paginate your requests.

Yes, that's an unfortunate downside of this approach. I do have enough data in my instance that pagination would be necessary. And the inefficiency of having to load all geometry upfront, rather than letting the users zoom + pan actions load geometry on the fly is far from ideal.

Two favourable alternatives would be:

  1. A customised renderer. E.g. I can set a callback function that is provided the Graphic object as an input parameter, and allows me to set properties of the renderer (size, colour, etc.). Or;
  2. Data "pipeline" functionality for the FeatureLayer. So whenever the JS API makes a call to the server-side feature layer URL, I have the opportunity to amend the request and / or response. I.e. when the response comes back, add some attributes to the Graphic objects.

I'm a little surprised that there appears to be no straightforward way to accomplish either of these. I suppose subclassing, or even monkey-patching the JS could make it happen. But it's difficult to reverse engineer the JS API from the minified version to figure out how such subclassing or patching could accomplish the goal.

Regarding the JS being minified, per here - https://github.com/Esri/arcgis-js-api - "A minified, unbuilt version of the ArcGIS API for JavaScript ES modules." Am I missing something (i.e. is there a *not* minified version available?), or is it intentional that there's only a minified version available?

Here's a good example of a custom layer created using subclassing - https://developers.arcgis.com/javascript/latest/sample-code/layers-custom-blendlayer/ But, even knowing that it's the "load" function that should be overridden is not obvious without this very direct example. I mean you go look at the code for the BaseTileLayer class and its class declaration is:

let y = class extends(n(c(m(a))))

So it extends... what exactly? Very difficult to read, figure out behaviour, and now what / where to override to get customised behaviour.

0 Kudos
ReneRubalcava
Honored Contributor

The JSAPI isn't open-source, so we don't provide the unminified code anywhere.

We only have custom layer extensibility for Tiled layers, not FeatureLayers.

We also have request interceptors, where you can modify the response before the layers use it, but no guarantee we don't use the service defined fields for other parts of the drawing and query stuff, you might be able to do some stuff here, but you're just adding to query time here too by pulling in new data requests.

https://developers.arcgis.com/javascript/latest/api-reference/esri-config.html#RequestInterceptor

If you have Enterprise portal, you can do this via a SOE. Doing this on the client is probably going to require a custom worker one way or another

0 Kudos
JasonDoingMaps
Emerging Contributor

RequestInterceptor looks like just the thing I need.

Firstly, the JS API queries the feature layer to get metadata (spatial reference, extents, etc.). I listen for that response, checking for presence of the "supportedQueryFormats" property in the response. The layer I'm querying supports PBF format, which isn't easy to manipulate, so I override the "supportedQueryFormats" property in the response to only list "JSON", so subsequent feature queries request JSON responses. Like this:

const myFeatureUrl = 'https://....';
esriConfig.request.interceptors.push({
    after: (response) => {
        if (response.url.indexOf(myFeatureUrl) !== -1) {
            if (response.data.supportedQueryFormats) {
                // Force feature querying to use JSON instead of PBF,
                // to make it easier to intercept and manipulate.
                response.data.supportedQueryFormats = 'JSON';
            }
        }
    }
});

 

Subsequent responses, despite being JSON, come in an ArrayBuffer, but I can at least convert that, manipulate the content, then convert it back to an ArrayBuffer so later parts of the ArcGIS JS query pipeline are happy. Like this:

// ab2str and str2ab are external functions for ArrayBuffer / string conversion.

const myFeatureUrl = 'https://....';
const otherData = []; // Populated otherwise...

esriConfig.request.interceptors.push({
    after: (response) => {
        if (response.url.indexOf(myFeatureUrl) !== -1) {
            if (response.data instanceof ArrayBuffer) {

                // Convert the response into a JS object, add extra attributes to
                // the features, then convert back to ArrayBuffer for the sake
                // of later parts in the ArcGIS JS API pipeline.

                let responseData = JSON.parse(ab2str(response.data));

                if (responseData.features) {
                    for (let feature of responseData.features) {
                        let keyValue = feature.attributes.keyProperty;

                        if (keyValue) {
                            let data = otherData.filter(d => d.keyProperty === keyValue)[0];
                            if (data) {
                                feature.attributes.extraAttribute1 = data.extraAttribute1;
                                feature.attributes.extraAttribute2 = data.extraAttribute2;
                            }
                        }
                    }

                    response.data = str2ab(JSON.stringify(responseData));
                }
            }
        }
    }
});

 

 

The JSAPI isn't open-source, so we don't provide the unminified code anywhere.

Combined with limited documentation on the subject, this does unfortunately make extensibility difficult.

 

We only have custom layer extensibility for Tiled layers, not FeatureLayers.

Thanks for clarifying. It's not easy to discover this otherwise. I've found documentation for Tiled layer extensibility, there's a good example of the "blend layer" approach. But a lack of similar documentation for Feature layers doesn't make it obvious that it's not intended to be extended from. I imagine if I could dig deep enough, and reverse engineer the minified code, it would be possible, but it feels like swimming upstream.

From the outset, I hoped, and kind of expected that for feature rendering I'd find a function callback where I'd receive the graphic / feature to be rendered as input, and be able to provide the rendering properties / symbol on that basis. Arcade expressions, visual variables, etc. are OK, but if I've got a use case they don't fit, it looks like I'd be left with few (no?) alternatives. I guess this is because the arcade expressions approach is a means to provide a common abstraction across all platforms? But it would be great to still have the option to get one step deeper and have more control over the rendering. What's the preferred avenue for providing such feedback to the product team?

0 Kudos