Hexagon Geospatial
MENU

Developers Knowledge Base

M.App Portfolio provides a modern, cloud-based platform for creating and delivering diverse geospatial web applications.
Through the M.App Studio, our partners can design, build, and deploy their own Hexagon Smart M.Apps.
Showing results for 
Search instead for 
Do you mean 

How to create application with Region of Interest.

by Technical Evangelist ‎08-10-2016 05:48 AM - edited ‎08-11-2016 03:14 AM (764 Views)

Introduction

 

Snippets used in this guide: BitBucket repository

 

It is often desired that a geoprocessing needs to be run on a subset of the input data – so called Region of Interest (ROI). A very common case is to only process data that fall within a given rectangle, for example the rectangle defined by the current view of the data. Another common case is process on that data falls within a given polygon (or polygons), for example the boundary of a county or one or more parcels of land.

You can customize your Smart M.App to handle ROI and use it for geoprocessing.

This document provides a customization example that shows how to create two-panel Smart M.App (Spatial Recipe Panel and Map /BI Map) to support ROI cases:

 

  • Actual boundary of the map displayed
  • Geometry drawn on the map: bounding box (BBOX)
  • Features selected on BI Map (geoJSON objects).

Two steps to success

The steps we describe here in to create the geoprocessing Smart M.App with ROI are following:

 

  • Create a Smart Mapp with spatial recipe
  • Customize the Smart M.App to handle ROI and pass it on the spatial recipe input for geoprocessing.

Prerequisites

A successfully published spatial recipe with ROI input is the prerequisite. This spatial recipe must expose ROI as an input. Please read ROI Recipe With A Region Of Interest article to learn more on how to develop such spatial recipe using Spatial Workshop.

The Guide explains in details Java Script customization to enable ROI, thus a basic knowledge of JavaScript is encouraged.

 

Configure Smart M.App with Spatial Recipe ROI enabled

 

Configure Spatial Recipe Panel

  1. While creating Smart M.App in M.App Editor you need to add Spatial Recipe Panel and select spatial recipe you want to configure, then click next.

1.png

2. Select “Bound” checkbox for input you are going to use as ROI. 

2.png

If “Bound” checkbox is checked for input, then the input is not displayed on Spatial Recipe Panel when Smart M.App is run. You can leave it unchecked if you want it to be displayed for end user.  

 

3. Click OK to close the Spatial Recipe Panel configuration wizard.

 

Configure Map Panel

Configure BI Map if you want use selected features from BI Map for ROI. Otherwise use standard Map Panel.

As it is standard step of Smart M.App configuration, we do not provide the details in this article. Please, read M.App Studio Documentation or see the examples available on Community.

 

 

Customize Smart M.App

  1. Click “Customize” in M.App Editor.

3.png

  2. Open JavaScript code editor on Spatial Recipe Panel.

       NOTE: This step is the same for all ROI use cases.

4.png  3. Open Code Examples accordion available in the Toolbox and go to 07.Process category section.

  4. Drag and drop “Register preprocessor” example into JS editor.

 

A few lines of JavaScript code will appear in JS editor window.

gsp.m_app.processes.registerGeoprocessingPreprocessor(function (inputs, resolve, reject) {
    var input = null;

    for (var i = inputs.length; i--; ) {
        if (inputs[i].Name === "ROI In") {
            input = inputs[i];
        }
    }

    if (!input) {
        reject(new Error("Can't find input: 'ROI In'"));
        return;
    }

    input.Data.Type = "IMAGINE.FeatureSubset";

    input.Data.Value = {
        "type": "FeatureCollection",
        "features": [
            {
                "type": "Feature",
                "geometry": {
                    "type": "Polygon",
                    "coordinates": [
                        [
                            [13.348388671875, 48.29050321714062], [13.348388671875, 54.851315259686054],
                            [26.12548828125, 54.851315259686054], [26.12548828125, 48.29050321714062],
                            [13.348388671875, 48.29050321714062]
                        ]
                    ]
                },
                "properties": null
            }
        ],
        "attributeInfo": null,
        "crs": {
            "type": "EPRJ",
            "properties": {
                "code": "urn: ogc:def:crs:EPSG::4326"
            }
        }
    };

    resolve(inputs);
}); 

In brief, this code registers the function as a custom geoprocessing payload preprocessor. Your method will get payload generated by standard Recipe Panel.

In our example we look for ROI input named “ROI In” - see 5th code line above. If you use some other input name please change “ROI In” (bolded text fragment) to your input name.

The sample code above passes ROI as a polygon with specified coordinates. However, the lines 15 to 43 can be replaced with the code samples below to support ROI by BBOX, map boundary, selected features or any other your custom code.

CRS must be provided as URN code (eg. urn: ogc:def:crs:EPSG::4326)

 

Use actual map boundary as ROI

By design, each application panel is independent in the Smart M.App. In particular, Spatial Recipe Panel does not have access to Map Panel. To communicate between two panels, a messaging mechanism has to be used. Each panel can send or receive messages. See API Documentation, 08.Messages category section for more details and code examples.

To pass map boundary as ROI it is necessary to register a preprocessor which will implement following simple algorithm:

map_boundaries.png

 

JS Customization of Spatial Recipe Panel

 

gsp.m_app.processes.registerGeoprocessingPreprocessor(function(inputs, resolve, reject) {
    var input = null;
    for (let i = inputs.length; i--;) {
        if (inputs[i].Name === "ROI In") {
            input = inputs[i];
        }
    }
    if (!input) {
        reject(new Error("Can't find input: 'ROI In'"));
        return;
    }

    // Algorithm Step 5: Receive message with map boundaries
    // Subscribe for message “getcurrentMapBoundary”. 
    // Because user can execute model many times, you should 
    // unsubscribe message listener right after you receive message.
    var subscriptionKey = gsp.m_app.messages.subscribe("currentMapBoundary", function(bbox) {
        gsp.m_app.messages.unsubscribe(subscriptionKey);
        // Algorithm step 6: Set ROI
        // Create valid payload for your ROI, it’s recommended to use polygon payload instead of bounding box. 
        input.Data = {
            "Type": "IMAGINE.FeatureSubset",
            "Value": {
                "type": "FeatureCollection",
                "features": [{
                    "type": "Feature",
                    "geometry": {
                        "type": "Polygon",
                        "coordinates": [
                            [
                                [bbox[0], bbox[1]],
                                [bbox[0], bbox[3]],
                                [bbox[2], bbox[3]],
                                [bbox[2], bbox[1]],
                                [bbox[0], bbox[1]]
                            ]

                        ]
                    },
                    "properties": null
                }],
                "attributeInfo": null,
                "crs": {
                    "type": "name",
                    "properties": {
                        "name": "urn:ogc:def:crs:EPSG::4326"
                    }
                }
            }
        };
        // Algorithm Step 7: End payload preprocessing
        // Call resolve method after setting valid input
        resolve(inputs);
    });
    // Algorithm, Step 1: Send get map boundaries message    
    gsp.m_app.messages.send("getCurrentMapBoundary");
});

 

JS Customization in Map Panel

 

// Algorithm, Step 2: “Receive get map boundaries message”
// Algorithm , Step 3: Get current bounding box of map. 
gsp.m_app.messages.subscribe("getCurrentMapBoundary", function() {
    gsp.map.info(function(infoResponse) {
        // Algorithm step 4: Send message with map boundaries
        // Send message with current bbox. 
        // It’s recommended (but not required) to use WGS:84 CRS as it may be helpful while creating geoprocessing payload.
        gsp.m_app.messages.send("currentMapBoundary", infoResponse.info.wgs84.bbox);
    });
});

 

 

Use drawn geometry as ROI

Previous chapter was devoted to using map boundary as ROI, very often M.App developer wants to allow user to select specific area as ROI. Currently M.App Studio allows to enable selecting bounding box interaction, anyway you can add more interactions, but there is no exposed API to do that.

Preprocessor algorithm is very similar to that was described in previous chapter. Only difference is that Map Panel instead of returning current map boundaries should enable drawing mode, and returns drawn bounding box to Recipe Panel.

 

bbox.png

 

JS Customization of Spatial Recipe Panel

 

var button = document.createElement("button"),
    submitButton = document.getElementsByName("model-input")[0].getElementsByTagName("button")[0],
    bbox = null;

button.type = "button";
button.className = "btn btn-default";
button.innerHTML = "Select bbox";
submitButton.disabled = true;

submitButton.parentElement.insertBefore(button, submitButton);

button.addEventListener("click", function() {
    gsp.m_app.messages.send("getCurrentMapBoundary");
});

// Algorithm Step 5: Receive message with bboxgsp.m_app.messages.subscribe("currentMapBoundary", function (selectedBbox) {
bbox = selectedBbox;
submitButton.disabled = false;
});

gsp.m_app.processes.registerGeoprocessingPreprocessor(function(inputs, resolve, reject) {
    var input = null;
    for (let i = inputs.length; i--;) {
        if (inputs[i].Name === "ROI In") {
            input = inputs[i];
        }
    }

    if (!input) {
        reject(new Error("Can't find input: 'ROI In'"));
        return;
    }
    // Algorithm, Step 6: Set ROI
    input.Data = {
        "Type": "IMAGINE.FeatureSubset",
        "Value": {
            "type": "FeatureCollection",
            "features": [{
                "type": "Feature",
                "geometry": {
                    "type": "Polygon",
                    "coordinates": [
                        [
                            [bbox[0], bbox[1]],
                            [bbox[0], bbox[3]],
                            [bbox[2], bbox[3]],
                            [bbox[2], bbox[1]],
                            [bbox[0], bbox[1]]
                        ]

                    ]
                },
                "properties": null
            }],
            "attributeInfo": null,
            "crs": {
                "type": "name",
                "properties": {
                    "name": "urn:ogc:def:crs:EPSG::4326"
                }
            }
        }
    };
    //Algorithm Step 7: End payload preprocessing
    resolve(inputs);

    //Algorithm, Step 1: Send get map boundaries message
    gsp.m_app.messages.send("getCurrentMapBoundary");
});

 

JS customization of Map Panel

 

//Algorithm, Step 2: Receive get map boundaries message
gsp.m_app.messages.subscribe("getCurrentMapBoundary", function() {
    //Algorithm, Step 2: Enable drawing bounding box
    gsp.geometry.getBbox(
        true,
        function(bbox) {
            //the method returns Bbox in actually displayed CRS. It needs to be transformed to EPSG:4326.
            gsp.map.info(function(mapInfoRet) {
                gsp.crs.transform({
                        sourceCrsId: mapInfoRet.info.crs,
                        targetCrsId: "EPSG:4326",
                        points: [{
                            x: bbox[0],
                            y: bbox[1]
                        }, {
                            x: bbox[2],
                            y: bbox[3]
                        }]
                    },
                    //Algorithm , Step 4: Send message with selected bounding box
                    function(result) {
                        gsp.m_app.messages.send("currentMapBoundary", [result.points[0].x, result.points[0].y, result.points[1].x, result.points[1].y]);
                    },
                    function(err_result) {
                    });
            });
        },
        function(err) {
            console.error(err);
        }
    );
});

 

Use selected BI features

NOTICE: This example makes use of BI Map Panel, not a Map Panel.

 

JS customization of Spatial Recipe Panel

 

var itemToBeDeleted = null;
gsp.m_app.processes.registerGeoprocessingPreprocessor(function(inputs, resolve, reject) {
    var input = null;
    for (let i = inputs.length; i--;) {
        if (inputs[i].Name === "ROI In") {
            input = inputs[i];
        }
    }
    if (!input) {
        reject(new Error("Can't find input: 'ROI In'"));
        return;
    }
    // Algorithm Step 5: Receive message with polygons selected on BI Map
    var subscriptionKey = gsp.m_app.messages.subscribe("currentMapBoundary", function(geojson) {
        gsp.m_app.messages.unsubscribe(subscriptionKey);
        // Algorithm, Step 6: Set ROI
        // In most cases the file structure describing the polygons selected on BI Map I s pretty heavy (above 400kb). It is recommended to save the structure into a temporal file uploaded to  the user catalog and use that file as an input to spatial recipe.
        var fileToBeUploaded = {
            content: JSON.stringify({
                "Type": "IMAGINE.FeatureSubset",
                "Value": {
                    "type": "FeatureCollection",
                    "features": geojson.features,
                    "attributeInfo": null,
                    "crs": {
                        "type": "name",
                        "properties": {
                            "name": "urn:ogc:def:crs:EPSG::4326"
                        }
                    }
                }
            }),
            mimeType: "text/plain",
            fileName: "myfile.json"
        };
        gsp.m_app.platform.catalog.upload(fileToBeUploaded, null, function(response) {
            input.Data.Value = response.itemId;
            itemToBeDeleted = response.itemId;
            //Algorithm Step 7: End payload preprocessing
            resolve(inputs);
        });
    });
    //Algorithm, Step 1: Send get map boundaries message
    gsp.m_app.messages.send("getCurrentMapBoundary");
});
// Sending resolve method only would  result in uploading temporal file in user catalog which would be then displayed in M.App Chest. Best practice is to delete this temporal file.
gsp.m_app.events.processes.processExecutionFinish(function(ev, args) {
    gsp.m_app.platform.catalog.delete(itemToBeDeleted, function() {});
});

 

JS Customization of BI Map Panel

 

//Algorithm, Step 2: Receive get map boundaries message
gsp.m_app.messages.subscribe("getCurrentMapBoundary", function() {
    //Algorithm, Step 3: Get selected features
    gsp.bi.stage.findSelectedFeatures({}, function(ret) {
        //Algorithm, step 4: Send selected features (geojson structure)
        gsp.m_app.messages.send("currentMapBoundary", ret);
    }, function(err) {
        // display err
        console.error(err);
    });
});