koopjs / koop

Transform, query, and download geospatial data on the web.
http://koopjs.github.io
Other
671 stars 128 forks source link

How to publish sublayer metadata for a FeatureService? #344

Closed mttjhn closed 5 years ago

mttjhn commented 5 years ago

I'm planning to use Koop to expose data from a non-GIS system using multiple sublayers on a FeatureService. Based on the documentation, it seems that I can use the req.params.layer value to do this in the getData() method. When testing various URLs with the query method on specific layer ids, it seems to work fine. For example, I might use this URL: http://localhost:8080/provider/rest/services/FeatureServer/0/query and the data returned is different from http://localhost:8080/provider/rest/services/FeatureServer/1/query.

However, when using the http://localhost:8080/provider/rest/services/FeatureServer/layers route, the output doesn't correspond to the actual layers that I return from getData(). Only a single layer is returned, and it is actually returning the wrong metadata, because req.params.layer is undefined when calling the ../FeatureServer/layers route. Is there some undocumented way to define the layer metadata? It seems that calling ../FeatureServer/layers actually executes getData. How is that part of the output provider intended to work?

My goal here is to provide all the layer metadata so that ArcGIS tools (or other code consuming the Feature Service) can see all of the layers that I intend to support in my Koop service.

rgwozdz commented 5 years ago

Hello @mttjhn and thanks for reaching out. So, this isn't really documented, but I can help explain what to do.

First, req.params.layer is undefined on http://localhost:8080/provider/rest/services/FeatureServer/layers because that route is registered without the layer route param (i.e., /FeatureServer/layers vs /FeatureServer/:layerId. However you can still do what you want with the right code in your provider. I would do a check via a regex in your getData

if (/\/FeatureServer\/layers$/i.test(url)) {
      // Build GeoJSON to support layers output here
}

For details on how to build the GeoJSON you need take a look at where the geojson for this route is processed and rendered. See https://github.com/koopjs/FeatureServer/blob/master/src/info.js#L57-L79

If you get stuck, please reach out, I'll do my best to respond ASAP. I'm interested in your work, as it will help us document this.

mttjhn commented 5 years ago

Ok, it's good to know that there's a way to do this. I was expecting that there might be a separate architectural layer where I could define my metadata in a single spot and then access it from getData(). For now, I'll play with the code example you provided and will be back with more questions (or an example of how I get it working)...

mttjhn commented 5 years ago

@rgwozdz: I got something working. Here's the general approach I followed:

  1. Created a constant in model.js to store my metadata:
    const layerMetadata = [
        {
            id: 0,
            name: 'First Layer',
            description: 'Points from my first layer',
            displayField: 'loc_name',
            geometryType: 'Point',
            idField: 'pointid'
        },
        {
            id: 1,
            name: 'Second Layer',
            description: 'Points from my second layer',
            displayField: 'loc_name',
            geometryType: 'Point',
            idField: 'pointid'
        }
    ];

I'm using the regex you provided to check the URL to see if I'm using the layers route, and then I'm calling agetMetadataGeoJson() method simply loops through all the items in my const array and creates a new FeatureCollection with a single feature in each:

function getMetadataGeoJson() {
    var metadata =  { layers: []};

    for (var i = 0; i < layerMetadata.length; i++) {
        var newFeature = {
            type: 'FeatureCollection',
            features: [
                {
                    type: 'Feature',
                    geometry: {
                        type: 'Point',
                        coordinates: [0, 0]
                    }
                }
            ],
            metadata: layerMetadata[i]
        }
        metadata.layers.push(newFeature);
    }
    return metadata;
}

Putting it all together, I updated getData to also look up metadata from my array by layerId, as shown below:

Model.prototype.getData = function (req, callback) {

    if (/\/FeatureServer\/layers$/i.test(req.originalUrl)) {
        // Build GeoJSON, sorta
        var layers = getMetadataGeoJson();
        callback(null, layers);
    }
    else {
        var layerId = req.params.layer;

        // Call database to get data
        getDataFromDb(layerId)
        .then((res) => {  
            // translate the response into geojson
            const geojson = translate(res)

            // Get metadata for the layer
            geojson.metadata = getLayerMetadata(layerId);

            // hand off the data to Koop
            callback(null, geojson)
        })
        .catch((err) => {
            callback(err)
        })
    }

The only downside to this right now is that I'm getting the following error whenever I hit the layers route:

WARNING: Source data for /provider/rest/services/FeatureServer/layers is invalid GeoJSON:
         1) "type" member required

If I provide a type (I was trying "FeatureCollection") it tries to parse it as actual GeoJSON... am I missing something here?

rgwozdz commented 5 years ago

Sorry for dropping the ball on this. Nice work.

The getData callback always expects geojson; you are seeing a warning caused by the GeoJSON validator. It's just a warning, and should be turned off on requests to this endpoint (I'll make it an issue). But you can safely ignore it - the code that processes the data can accept a FeatureCollection OR a regular JSON array.