TC9.8 - Developing new MicroService/node of Node-RED IOT Application

Test Case Title

TC9.8 - Developing new MicroService/node of Node-RED IOT Application

Goal

Creating a new MicroService for Snap4City Applications and environment integrating external services

Creating a new MicroService for Snap4City Applications and environment providing Map Access and interaction.

Connection via HTTPS/HTTP

Prerequisites

The usage of Snap4City Application has to be known. The access to some external service to be integrated. The adoption of some library for showing graphics on web pages, as D3, etc.

The following functionalities are available only for specific Snap4city users with specific privileges.

Expected successful result

The creating of new blocks/MicroServices that can be installed into NodeRED editor to create applications, put them in execution, update, share them with other developers via email, portal, ProcessLoader, etc.

Steps

 

See the several packages of Snap4City MicroServices in the BASIC and ADVANCED libraries that you have into BASIC and ADVANCED IOT Applications on cloud or you can download and install from JS Foundation Library of Node-RED. It is possible to add from your self new nodes into your IOT Application on cloud. If you encouter problems to update, install/add more Node/Blocks to your IOT Applications on Snap4CIty please contact US via:  https://www.snap4city.org/drupal/contact communicating us the name of IOT App and your nickname on Snap4City. You can even create a  new Node from scratch and ingest it by using:

 

 


Example 1: Create a basic MicroService node, the SC4Info, that you can see working on your examples NR active

  1. Create a folder that will contain the new package i.e. “node-red-contrib-snap4city”
  2. Create a folder that will contain the logic of the new node
  3. Create a JSON file called “package.json” that describes the package, containing this text:

{

    "name": "node-red-contrib-snap4city",

    "version": "0.0.1",

    "description": "",

    "dependencies": {

        "xmlhttprequest": "1.8.0"

    },

    "node-red": {

        "nodes": {

            "service-info": "service-info/service-info.js"

        }

    }

}

 

  1. A js file and an html file called in the same way as the folder must be created inside the service-info folder.
  2. The logic that is executed when a message arrives to the node you are creating must be inserted inside the js file.

module.exports = function(RED) {

    function ServiceInfo(config) {

        RED.nodes.createNode(this, config);

        var node = this;

        node.on('input', function(msg) {

            var uri = "http://servicemap.km4city.org/WebAppGrafo/api/v1/";

            var serviceuri = (msg.payload.serviceuri ? msg.payload.serviceuri : config.serviceuri);

            var lang = (msg.payload.lang ? msg.payload.lang : config.lang);

            var fromtime = (msg.payload.fromtime ? msg.payload.fromtime : config.fromtime);

            var XMLHttpRequest = require("xmlhttprequest").XMLHttpRequest;

            var xmlHttp = new XMLHttpRequest();

            xmlHttp.open("GET", encodeURI(uri + "?serviceUri=" + serviceuri + "&realtime=true" + "&lang=" + lang + (fromtime ? "&fromTime=" + fromtime : "")), false); // false for synchronous request

            xmlHttp.send(null);

            if (xmlHttp.responseText != "") {

                msg.payload = JSON.parse(xmlHttp.responseText);

            } else {

                msg.payload = JSON.parse("{'failure': 'ERROR'}");

            }

            node.send(msg);

        });

    }

    RED.nodes.registerType("service-info", ServiceInfo);

}

 

  1. 3 parts must be inserted in the html file. The first shows how the node is displayed.

<script type="text/javascript">

    RED.nodes.registerType('service-info', {

        category: 'S4CInfo',

        color: '#E9967A',

        defaults: {

            name: {

                value: ""

            },

            serviceuri: {

                value: "",

                required: false

            },

            lang: {

                value: "",

                required: false

            },

            fromtime: {

                value: "",

                required: false

            }

        },

        outputs: 1,

        inputs: 1,

        icon: "white-globe.png",

        label: function() {

            return this.name || "service-info";

        }

    });

</script>

 

  1. The second part relates to the tab that displays the node configuration

<script type="text/x-red" data-template-name="service-info">

    <div class="form-row">

        <label for="node-input-name">Name</label>

        <input type="text" id="node-input-name" placeholder="Name">

    </div>

    <div class="form-row">

        <label for="node-input-serviceuri">ServiceUri</label>

        <input type="text" id="node-input-serviceuri" placeholder="http://">

    </div>

    <div class="form-row">

        <label for="node-input-fromtime">fromTime</label>

        <input type="text" id="node-input-fromtime" placeholder="n-day, n-hour, n-minute or yyyyy-mm-ddThhh:mm:ss">

    </div>

    <div class="form-row">

        <label for="node-input-lang">Language</label>

        <select id="node-input-lang">

                    <option value="en">English</option>

                    <option value="fr">French</option>

                    <option value="de">German</option>

                    <option value="it">Italian</option>

                    <option value="es">Spanish</option>

             </select>

    </div>

</script>

 

  1. The third part relates to the info and relative help of that node.

<script type="text/x-red" data-help-name="service-info">

    <p>It allows to retrieve information about a service using its serviceUri.</p>

    <h3>Inputs</h3>

    A JSON with these parameters:

    <dl class="message-properties">

        <dt>serviceuri

            <span class="property-type">string</span>

        </dt>

        <dd> serviceUri (http://...) of the service</dd>

        <dt>fromtime

            <span class="property-type">string</span>

        </dt>

        <dd> fromtime, date/time from when retrieve realtime data. The string format can be: n-day, n-hour, n-minute or yyyyy-mm-ddThhh: mm: ss </dd>

        <dt>lang

                    <span class="property-type">string</span>

             </dt>

        <dd> ISO 2 chars language code (e.g. “it”, “en”, “fr”, “de”, “es”) to be used for returned descriptions if available in multiple languages. Currently for languages other than “it” and “en” it returns “en” descriptions. (if parameter is missing “en” is assumed)</dd>

    </dl>

    <h3>Outputs</h3>

    <dl class="message-properties">

        <dd> The API provides a GeoJSON description of the service with the main properties (name, address, city, type, etc.) and possibly some time varying properties for some kinds of services (traffic sensors, car park sensors, etc.)</dd>

    </dl>

    <h3>Details</h3>

    <p>The node can receive a JSON with the parameters described in the Inputs section and with them generate the output JSON. If the values are not present in the input JSON, these are read by those in the configuration. If they are not present in either part, an error is generated for the necessary parameters.</p>

</script>

 

  1. To test it locally you can go to the package root and open the terminal, type "npm link". Once you have done this command you must go to the home of the user where there is the folder ". node-red" and open the terminal on that folder you must run the command "npm link <name-package>".

 


Example 2: Create an advanced node for Service Discovering, it is one of the most interesting MicroServices of the Advanced Smart City API for Snap4City

  1. Create a folder that will contain the new package i.e. “node-red-contrib-snap4city-user”
  2. Create a folder that will contain the logic of the new node
  3. Create a JSON file called “package.json” that describes the package, containing this text:

{

    "name": " node-red-contrib-snap4city-user",

    "version": "0.0.1",

    "description": "",

    "dependencies": {

        "xmlhttprequest": "1.8.0"

    },

    "node-red": {

        "nodes": {

            "service-search-near-marker": "service-search-near-marker/service-search-near-marker.js"

        }

    }

}

  1. A js file and an html file called in the same way as the folder must be created inside the service-search-near-marker folder. In addition, a folder called “lib” with everything is useful for the js library imported:

     
  2. The logic that is executed when a message arrives to the node you are creating must be inserted inside the js file.

module.exports = function(RED) {

 

    function ServiceSearchNearMarker(config) {

        RED.nodes.createNode(this, config);

        var node = this;

        var msgs = [{}, {}, {}];

        node.on('input', function(msg) {

            var uri = "http://servicemap.km4city.org/WebAppGrafo/api/v1/";

            var latitude = (msg.payload.latitude ? msg.payload.latitude : config.latitude);

            var longitude = (msg.payload.longitude ? msg.payload.longitude : config.longitude);

            var categories = (msg.payload.categories ? msg.payload.categories : config.categories);

            var maxDists = (msg.payload.maxdists ? msg.payload.maxdists : config.maxdists);

            var maxResults = (msg.payload.maxresults ? msg.payload.maxresults : config.maxresults);

            var language = (msg.payload.lang ? msg.payload.lang : config.lang);

            var XMLHttpRequest = require("xmlhttprequest").XMLHttpRequest;

            var xmlHttp = new XMLHttpRequest();

            xmlHttp.open("GET", encodeURI(uri + "?selection=" + latitude + ";" + longitude + "&categories=" + categories + "&maxResults=" + maxResults + "&maxDists=" + maxDists + "&format=json" + "&lang=" + language + "&geometry=true"), false); // false for synchronous request

            xmlHttp.send(null);

 

            if (xmlHttp.responseText != "") {

                var response = JSON.parse(xmlHttp.responseText);

                var serviceUriArray = [];

                var completeFeatures = {

                    "Results": {

                        "fullCount": 0,

                        "type": "FeatureCollection",

                        "features": []

                    }

                }

                for (var category in response) {

                    for (var i = 0; i < response[category].features.length; i++) {

                        serviceUriArray.push(response[category].features[i].properties.serviceUri);

                    }

                }

                if (response[category].features.length != 0) {

                    completeFeatures["Results"].features = completeFeatures["Results"].features.concat(response[category].features);

                    completeFeatures["Results"].fullCount = completeFeatures["Results"].fullCount + response[category].fullCount;

                }

                msgs[0].payload = serviceUriArray;

                msgs[1].payload = response;

                msgs[2].payload = completeFeatures;

 

            } else {

                msgs[0].payload = JSON.parse("{'failure': 'ERROR'}");

                msgs[1].payload = JSON.parse("{'failure': 'ERROR'}");

                msgs[2].payload = JSON.parse("{'failure': 'ERROR'}");

            }

            node.send(msgs);

        });

    }

    RED.nodes.registerType("service-search-near-marker", ServiceSearchNearMarker);

 

    RED.httpAdmin.get('/s4c/js/*', function(req, res) {

        var options = {

            root: __dirname + '/lib/js/',

            dotfiles: 'deny'

        };

 

        res.sendFile(req.params[0], options);

    });

 

    RED.httpAdmin.get('/s4c/css/*', function(req, res) {

        var options = {

            root: __dirname + '/lib/css/',

            dotfiles: 'deny'

        };

 

        res.sendFile(req.params[0], options);

    });

 

    RED.httpAdmin.get('/s4c/json/*', function(req, res) {

        var options = {

            root: __dirname + '/lib/json/',

            dotfiles: 'deny'

        };

 

        res.sendFile(req.params[0], options);

    });

 

    RED.httpAdmin.get('/s4c/img/*', function(req, res) {

        var options = {

            root: __dirname + '/lib/img/',

            dotfiles: 'deny'

        };

 

        res.sendFile(req.params[0], options);

    });

}

3 parts must be inserted in the html file. The first shows how the node is displayed.

<script type="text/javascript">

    function selectCategories() {

        selectedKeys = [];

        if ($("#node-input-fancytree").fancytree("getTree").isFilterActive()) {

            selectedKeys = $.map($("#node-input-fancytree").fancytree("getTree").getSelectedNodes(), function(node) {

                if (node.isMatched() && !node.hasChildren()) {

                    return node.key;

                }

            });

        } else {

            selectedKeys = $.map($("#node-input-fancytree").fancytree("getTree").getSelectedNodes(true), function(node) {

                return node.key;

            });

        }

        if (selectedKeys.length == 0) {

            $("#node-input-categories").val("Service");

        } else {

            $("#node-input-categories").val(selectedKeys.join(";"));

        }

    }

 

    function filterMenu() {

        var match = $("#filterCategories").val();

        var opts =

            $("#node-input-fancytree").fancytree("getTree").filterNodes(match, {

                autoExpand: true,

                leavesOnly: true

            });

 

    }

    RED.nodes.registerType('service-search-near-marker', {

        category: 'S4CSearchUsr',

        color: '#E9967A',

        defaults: {

            name: {

                value: ""

            },

            latitude: {

                value: 0.0,

                required: false,

                validate: RED.validators.number()

            },

            longitude: {

                value: 0.0,

                required: false,

                validate: RED.validators.number()

            },

            categories: {

                value: "",

                required: false

            },

            maxdists: {

                value: 1,

                required: false,

                validate: RED.validators.number()

            },

            maxresults: {

                value: 100,

                required: false,

                validate: RED.validators.number()

            },

            lang: {

                value: "",

                required: false

            }

        },

        outputs: 3,

        inputs: 1,

        icon: "white-globe.png",

        label: function() {

            return this.name || "service-search-near-marker";

        },

        oneditprepare: function() {

            node = this;

            map = L.map('node-input-map').setView([43.78, 11.23], 9);

            L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', {

                attribution: '&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'

            }).addTo(map);

            window.node_input_map = map;

 

            var mapLayers = {};

 

            drawnItems = new L.FeatureGroup();

            map.addLayer(drawnItems);

 

            var editControl = new L.Control.Draw({

                draw: false,

                edit: {

                    featureGroup: drawnItems

                }

            });

            map.addControl(editControl);

 

            drawControl = new L.Control.Draw({

                draw: {

                    position: 'topleft',

                    polyline: false,

                    marker: {

                        icon: new L.DivIcon({

                            iconSize: new L.Point(8, 8),

                            className: 'leaflet-div-icon leaflet-editing-icon test'

                        })

                    },

                    circlemarker: false,

                    circle: false,

                    polygon: false,

                    rectangle: false

                }

            });

            map.addControl(drawControl);

 

            L.control.layers(mapLayers, {

                'drawlayer': drawnItems

            }, {

                collapsed: true

            }).addTo(map);

 

            map.on(L.Draw.Event.CREATED, function(e) {

                var fence = e.layer;

                fence.nodeID = node.id;

                if (drawnItems.hasLayer(fence) == false) {

                    drawnItems.addLayer(fence);

                }

 

                drawControl.remove();

 

                markers = {};

 

                drawnItems.eachLayer(function(layer) {

                    markers[layer.nodeID] = layer.toGeoJSON();

                });

 

                $("#node-input-latitude").val(markers[node.id].geometry.coordinates[1]);

                $("#node-input-longitude").val(markers[node.id].geometry.coordinates[0]);

            });

 

            map.on('draw:edited', function(e) {

                var fences = e.layers;

                fences.eachLayer(function(fence) {

                    fence.shape = "geofence";

                    if (drawnItems.hasLayer(fence) == false) {

                        drawnItems.addLayer(fence);

                    }

                });

 

                markers = {};

 

                drawnItems.eachLayer(function(layer) {

                    markers[layer.nodeID] = layer.toGeoJSON();

                });

 

                $("#node-input-latitude").val(markers[node.id].geometry.coordinates[1]);

                $("#node-input-longitude").val(markers[node.id].geometry.coordinates[0]);

            });

 

            map.on('draw:deleted', function(e) {

                drawControl.addTo(map);

                $("#node-input-latitude").val(0);

                $("#node-input-longitude").val(0);

            });

 

            $.ajax({

                url: "s4c/json/categories.json",

                async: false,

                cache: false,

                timeout: 2000,

                dataType: "json",

                success: function(data) {

                    $("#node-input-fancytree").fancytree({

                        source: data,

                        extensions: ["glyph", "filter"],

                        checkbox: true,

                        imagePath: "s4c/img/",

                        selectMode: 3,

                        clickFolderMode: 2,

                        click: function(event, data) {

                            if (!data.node.folder) {

                                data.node.toggleSelected();

                                selectCategories();

                                return false;

                            }

                        },

                        glyph: {

                            map: {

                                checkbox: "fa fa-square",

                                checkboxSelected: "fa fa-check-square",

                                checkboxUnknown: "fa fa-share-square"

                            }

                        },

                        filter: {

                            autoApply: false, // Re-apply last filter if lazy data is loaded

                            counter: false, // Show a badge with number of matching child nodes near parent icons

                            fuzzy: false, // Match single characters in order, e.g. 'fb' will match 'FooBar'

                            leavesOnly: true,

                            highlight: false, // Highlight matches by wrapping inside <mark> tags

                            nodata: "No services",

                            mode: "hide" // Grayout unmatched nodes (pass "hide" to remove unmatched node instead)

                        }

                    });

 

                    $("span.fancytree-checkbox").css("font-size", "30px").css("margin-top", "5px").css("margin-right", "5px");

                    $("img.fancytree-icon").css("font-size", "35px");

 

                    $("#node-input-fancytree").on("click", function(event, data) {

                        selectCategories();

                    });

                },

                error: function(data) {

                    console.log(data);

                }

            });

 

        },

        oneditresize: function() {

            if (window.node_input_map) {

                window.node_input_map.invalidateSize(true);

            }

        }

    });

</script>

The second part relates to the tab that displays the node configuration

<script type="text/x-red" data-template-name="service-search-near-marker">

    <div class="form-row">

        <label for="node-input-name">Name</label>

        <input type="text" id="node-input-name" placeholder="Name">

    </div>

    <div class="form-row">

        <label for="node-input-maxdists">Max Distance</label>

        <select id="node-input-maxdists">

                    <option value="0.1">100 Meters</option>

                    <option value="0.2">200 Meters</option>

                    <option value="0.5">500 Meters</option>

                    <option value="1">1 KMeters</option>

                    <option value="2">2 KMeters</option>

                    <option value="5">5 KMeters</option>

                    <option value="10">10 KMeters</option>

                    <option value="20">20 KMeters</option>

                    <option value="50">50 KMeters</option>

                    <option value="0">All</option>

             </select>

    </div>

    <div class="form-row">

        <label for="node-input-maxresults">Max Results</label>

        <select id="node-input-maxresults">

                    <option value="100">100</option>

                    <option value="200">200</option>

                    <option value="500">500</option>

                    <option value="1000">1000</option>

                    <option value="2000">2000</option>

                    <option value="0">All</option>

             </select>

    </div>

    <div class="form-row">

        <label for="node-input-lang">Language</label>

        <select id="node-input-lang">

                    <option value="en">English</option>

                    <option value="fr">French</option>

                    <option value="de">German</option>

                    <option value="it">Italian</option>

                    <option value="es">Spanish</option>

             </select>

    </div>

    <div class="form-row">

        <label for="node-input-latitude">Latitude</label>

        <input type="text" id="node-input-latitude" placeholder="Latitude" disabled>

    </div>

    <div class="form-row">

        <label for="node-input-longitude">Longitude</label>

        <input type="text" id="node-input-longitude" placeholder="Longitude" disabled>

    </div>

    <div class="form-row">

        <link rel="stylesheet" href="s4c/css/leaflet.css" />

        <link rel="stylesheet" href="s4c/css/leaflet.draw.css" />

        <div id="node-input-map" style="width: 100%; height: 400px"></div>

    </div>

    <div class="form-row">

        <label for="node-input-categories">Categories</label>

        <input type="text" id="node-input-categories" placeholder="Categories" disabled>

    </div>

    <div class="form-row">

        <link rel="stylesheet" href="s4c/css/ui.fancytree.min.css" />

        <div>

            <button onclick="$('#filterCategories').val('');filterMenu();selectCategories();"><i class="fa fa-trash"></i></button>

            <input id="filterCategories" name="search" onkeyup="filterMenu();" style="width:90%;" placeholder="Filter Categories...">

 

        </div>

        <div id="node-input-fancytree" style="width: 100%; height: 400px"></div>

    </div>

</script>

<script type="text/javascript" src="s4c/js/leaflet.js"></script>

<script type="text/javascript" src="s4c/js/leaflet.draw.js"></script>

<script type="text/javascript" src="s4c/js/jquery.fancytree-all.min.js"></script>

 

  1. The third part relates to the info and relative help of that node.

<script type="text/x-red" data-help-name="service-search-near-marker">

    <p>It allows to retrieve the set of services that are near a given GPS position. The services can be filtered as belonging to specific categories (e.g. Accommodation, Hotel, Restaurant etc). It can also be used to find services that have a WKT spatial description that contains a specific GPS position.</p>

    <h3>Inputs</h3>

    A JSON with these parameters:

    <dl class="message-properties">

        <dt>latitude

            <span class="property-type">number</span>

        </dt>

        <dd> latitude of a GPS position</dd>

        <dt>longitude

            <span class="property-type">number</span>

        </dt>

        <dd> longitude of a GPS position</dd>

        <dt>categories

                    <span class="property-type">string</span>

             </dt>

        <dd> the list of categories of the services to be retrieved separated with semicolon, if omitted all kinds of services are returned. It can contain macro categories or categories, if a macro category is specified all categories in the macro category are used. The complete list of categories and macro categories can be retrieved on servicemap.disit.org</dd>

        <dt>maxdistance

                    <span class="property-type">number</span>

             </dt>

        <dd> maximum distance from the GPS position of the services to be retrieved, expressed in Km (0.1 is used if parameter is missing) if it is equal to “inside” it searches for services with a WKT geometry that contains the specified GPS position (e.g a park)</dd>

        <dt>maxresults

                    <span class="property-type">number</span>

             </dt>

        <dd> maximum number of results to be returned (if parameter is missing 100 is assumed), if it is 0 all results are returned</dd>

        <dt>lang

                    <span class="property-type">string</span>

             </dt>

        <dd> ISO 2 chars language code (e.g. “it”, “en”, “fr”, “de”, “es”) to be used for returned descriptions if available in multiple languages. Currently for languages other than “it” and “en” it returns “en” descriptions. (if parameter is missing “en” is assumed)</dd>

         </dl>

    <h3>Outputs</h3>

    <ol class="node-ports">

        <li>ServiceUri Array

            <dl class="message-properties">

                <dd> Returns an array containing the servicesUri of each service found</dd>

            </dl>

        </li>

        <li>Standard output

            <dl class="message-properties">

                <dd> It returns the services split in three sections (BusStops , SensorSites, Services). Each section is provided as GeoJSON “FeatureCollection”, the results are sorted by distance, additionally in each section the “fullCount” property reports the full number of results available matching the query</dd>

            </dl>

        </li>

        <li> All services together

            <dl class="message-properties">

                <dd> It returns the services merge in Results section</dd>

            </dl>

        </li>

    </ol>

 

    <h3>Details</h3>

    <p>The node can receive a JSON with the parameters described in the Inputs section and with them generate the output JSON. If the values are not present in the input JSON, these are read by those in the configuration. If they are not present in either part, an error is generated for the necessary parameters.</p>

</script>

 

  1. To test it locally you can go to the package root and open the terminal, type "npm link". Once you have done this command you must go to the home of the user where there is the folder ". node-red" and open the terminal on that folder you must run the command "npm link <name-package>".