Skip to content

For NGSI-v2, ensure GeoJSON is correctly encode in the request. #854

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Dec 18, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGES_NEXT_RELEASE
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ Add husky and lint-staged
Fix: Add support for lazy and internal_attributes in service notifications to Manager (#768)
Fix: combine multi-entity and expressions with duplicate attribute name, by enabling expression over object_id (which are not duplicated in a attribute mapping contrary to name) (#941)
Fix: bug in legacy and JEXL expression that was not converting "0" to 0
Fix: support for mapping different attributes to the same entity_name
Fix: support for mapping different attributes to the same entity_name
Fix: Ensure GeoJSON is correctly encoded in NGSI-v2 requests (#961)
58 changes: 58 additions & 0 deletions doc/advanced-topics.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,64 @@ curl http://${KEYSTONE_HOST}/v3/OS-TRUST/trusts \
Apart from the generation of the trust, the use of secured Context Brokers should be transparent to the user of the IoT
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this PR include a CHANGES_NEXT_RELEASE entry? If I understand correctly the discussion on #961, this PR would be fixing that issue, so it could be mentioned in the CHANGES_NEXT_RELEASE entry.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed 51b8f10 + f4ebb69 - CNR updated

Agent.

### GeoJSON support

The defined `type` of any GeoJSON attribute can be any set to any of the standard NGSI-v2 GeoJSON types - (e.g.
`geo:json`, `geo:point`). NGSI-LD formats such as `GeoProperty`, `Point` and `LineString` are also accepted `type`
values. If the latitude and longitude are received as separate measures, the
[expression language](expressionLanguage.md) can be used to concatenate them.

````json
{
"entity_type": "GPS",
"resource": "/iot/d",
"protocol": "PDI-IoTA-JSON",
..etc
"attributes": [
{
"name": "location",
"type": "geo:json",
"expression": "${@lng}, ${@lat}"
}
]
}
```json


For `attributes` and `static_attributes` which need to be formatted as GeoJSON values, three separate input
formats are accepted. Provided the `type` is provisioned correctly, the `value` may be defined using any of
the following formats:

- a comma delimited string

```json
{
"name": "location",
"value": "23, 12.5"
}
````

- an array of numbers

```json
{
"name": "location",
"value": [23, 12.5]
}
```

- an fully formatted GeoJSON object

```json
{
"name": "location",
"value": {
"type": "Point",
"coordinates": [23, 12.5]
}
}
```

### Metadata support

Both `attributes` and `static_attributes` may be supplied with metadata when provisioning an IoT Agent, so that the
Expand Down
21 changes: 21 additions & 0 deletions lib/services/devices/devices-NGSI-v2.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
* Modified by: Jason Fox - FIWARE Foundation
*/

/* eslint-disable consistent-return */

const request = require('request');
const async = require('async');
const apply = async.apply;
Expand All @@ -38,6 +40,7 @@ const config = require('../../commonConfig');
const registrationUtils = require('./registrationUtils');
const _ = require('underscore');
const utils = require('../northBound/restUtils');
const NGSIv2 = require('../ngsi/entities-NGSI-v2');
const moment = require('moment');
const context = {
op: 'IoTAgentNGSI.Devices-v2'
Expand Down Expand Up @@ -232,6 +235,15 @@ function createInitialEntityNgsi2(deviceData, newDevice, callback) {
jsonConcat(options.json, formatAttributesNgsi2(deviceData.staticAttributes, true));
jsonConcat(options.json, formatCommandsNgsi2(deviceData.commands));

for (const att in options.json) {
try {
// Format any GeoJSON attrs properly
options.json[att] = NGSIv2.formatGeoAttrs(options.json[att]);
} catch (error) {
return callback(new errors.BadGeocoordinates(JSON.stringify(options.json)));
}
}

logger.debug(context, 'deviceData: %j', deviceData);
if (
('timestamp' in deviceData && deviceData.timestamp !== undefined
Expand Down Expand Up @@ -281,6 +293,15 @@ function updateEntityNgsi2(deviceData, updatedDevice, callback) {
jsonConcat(options.json, formatAttributesNgsi2(deviceData.staticAttributes, true));
jsonConcat(options.json, formatCommandsNgsi2(deviceData.commands));

for (const att in options.json) {
try {
// Format any GeoJSON attrs properly
options.json[att] = NGSIv2.formatGeoAttrs(options.json[att]);
} catch (error) {
return callback(new errors.BadGeocoordinates(JSON.stringify(options.json)));
}
}

if (
('timestamp' in deviceData && deviceData.timestamp !== undefined
? deviceData.timestamp
Expand Down
52 changes: 7 additions & 45 deletions lib/services/ngsi/entities-NGSI-LD.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,45 +56,6 @@ function valueOfOrNull(value) {
return isNaN(value) ? NGSI_LD_NULL : value;
}

/**
* @param {String/Array} value Comma separated list or array of values
* @return {Array} Array of Lat/Lngs for use as GeoJSON
*/
function splitLngLat(value) {
const lngLats = typeof value === 'string' || value instanceof String ? value.split(',') : value;
lngLats.forEach((element, index, lngLats) => {
if (Array.isArray(element)) {
lngLats[index] = splitLngLat(element);
} else if ((typeof element === 'string' || element instanceof String) && element.includes(',')) {
lngLats[index] = splitLngLat(element);
} else {
lngLats[index] = Number.parseFloat(element);
}
});
return lngLats;
}

/**
* @param {String} value Value to be analyzed
* @return {Array} split pairs of GeoJSON coordinates
*/
function getLngLats(value) {
const lngLats = _.flatten(splitLngLat(value));
if (lngLats.length === 2) {
return lngLats;
}

if (lngLats.length % 2 !== 0) {
logger.error(context, 'Bad attribute value type. Expecting geo-coordinates. Received:%s', value);
throw Error();
}
const arr = [];
for (let i = 0, len = lngLats.length; i < len; i = i + 2) {
arr.push([lngLats[i], lngLats[i + 1]]);
}
return arr;
}

/**
* Amends an NGSIv2 attribute to NGSI-LD format
* All native JSON types are respected and cast as Property values
Expand Down Expand Up @@ -155,33 +116,34 @@ function convertNGSIv2ToLD(attr) {
case 'geoproperty':
case 'point':
case 'geo:point':
case 'geo:json':
obj.type = 'GeoProperty';
obj.value = { type: 'Point', coordinates: getLngLats(attr.value) };
obj.value = NGSIUtils.getLngLats('Point', attr.value);
break;
case 'linestring':
case 'geo:linestring':
obj.type = 'GeoProperty';
obj.value = { type: 'LineString', coordinates: getLngLats(attr.value) };
obj.value = NGSIUtils.getLngLats('LineString', attr.value);
break;
case 'polygon':
case 'geo:polygon':
obj.type = 'GeoProperty';
obj.value = { type: 'Polygon', coordinates: getLngLats(attr.value) };
obj.value = NGSIUtils.getLngLats('Polygon', attr.value);
break;
case 'multipoint':
case 'geo:multipoint':
obj.type = 'GeoProperty';
obj.value = { type: 'MultiPoint', coordinates: getLngLats(attr.value) };
obj.value = NGSIUtils.getLngLats('MultiPoint', attr.value);;
break;
case 'multilinestring':
case 'geo:multilinestring':
obj.type = 'GeoProperty';
obj.value = { type: 'MultiLineString', coordinates: attr.value };
obj.value = NGSIUtils.getLngLats( 'MultiLineString', attr.value);
break;
case 'multipolygon':
case 'geo:multipolygon':
obj.type = 'GeoProperty';
obj.value = { type: 'MultiPolygon', coordinates: attr.value };
obj.value = NGSIUtils.getLngLats( 'MultiPolygon', attr.value);
break;

// Relationships
Expand Down
67 changes: 66 additions & 1 deletion lib/services/ngsi/entities-NGSI-v2.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
* Modified by: Jason Fox - FIWARE Foundation
*/

/* eslint-disable consistent-return */

const request = require('request');
const statsService = require('./../stats/statsRegistry');
const async = require('async');
Expand All @@ -35,11 +37,59 @@ const utils = require('../northBound/restUtils');
const config = require('../../commonConfig');
const constants = require('../../constants');
const moment = require('moment-timezone');
const NGSIUtils = require('./ngsiUtils');
const logger = require('logops');
const context = {
op: 'IoTAgentNGSI.Entities-v2'
};
const NGSIUtils = require('./ngsiUtils');

/**
* Amends an NGSIv2 Geoattribute from String to GeoJSON format
*
* @param {Object} attr Attribute to be analyzed
* @return {Object} GeoJSON version of the attribute
*/
function formatGeoAttrs(attr) {
const obj = attr;
if (attr.type) {
switch (attr.type.toLowerCase()) {
// GeoProperties
case 'geo:json':
case 'geoproperty':
case 'point':
case 'geo:point':
obj.type = 'geo:json';
obj.value = NGSIUtils.getLngLats('Point', attr.value);
break;
case 'linestring':
case 'geo:linestring':
obj.type = 'geo:json';
obj.value = NGSIUtils.getLngLats('LineString', attr.value);
break;
case 'polygon':
case 'geo:polygon':
obj.type = 'geo:json';
obj.value = NGSIUtils.getLngLats('Polygon', attr.value);
break;
case 'multipoint':
case 'geo:multipoint':
obj.type = 'geo:json';
obj.value = NGSIUtils.getLngLats('MultiPoint', attr.value);
break;
case 'multilinestring':
case 'geo:multilinestring':
obj.type = 'geo:json';
obj.value = NGSIUtils.getLngLats('MultiLineString', attr.value);
break;
case 'multipolygon':
case 'geo:multipolygon':
obj.type = 'geo:json';
obj.value = NGSIUtils.getLngLats('MultiPolygon', attr.value);
break;
}
}
return obj;
}

function addTimestampNgsi2(payload, timezone) {
function addTimestampEntity(entity, timezone) {
Expand Down Expand Up @@ -387,6 +437,13 @@ function sendUpdateValueNgsi2(entityName, attributes, typeInformation, token, ca
if (options.json.entities[entity][att].name) {
delete options.json.entities[entity][att].name;
}

try {
// Format any GeoJSON attrs properly
options.json.entities[entity][att] = formatGeoAttrs(options.json.entities[entity][att]);
} catch (error) {
return callback(new errors.BadGeocoordinates(JSON.stringify(options.json)));
}
}
}
} else {
Expand All @@ -402,6 +459,13 @@ function sendUpdateValueNgsi2(entityName, attributes, typeInformation, token, ca
if (options.json[att].name) {
delete options[att].name;
}

try {
// Format any GeoJSON attrs properly
options.json[att] = formatGeoAttrs(options.json[att]);
} catch (error) {
return callback(new errors.BadGeocoordinates(JSON.stringify(options.json)));
}
}
}

Expand All @@ -424,3 +488,4 @@ function sendUpdateValueNgsi2(entityName, attributes, typeInformation, token, ca
exports.sendQueryValue = sendQueryValueNgsi2;
exports.sendUpdateValue = sendUpdateValueNgsi2;
exports.addTimestamp = addTimestampNgsi2;
exports.formatGeoAttrs = formatGeoAttrs;
46 changes: 46 additions & 0 deletions lib/services/ngsi/ngsiUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,51 @@ const _ = require('underscore');
const config = require('../../commonConfig');
const updateMiddleware = [];
const queryMiddleware = [];

/**
* @param {String/Array} value Comma separated list or array of values
* @return {Array} Array of Lat/Lngs for use as GeoJSON
*/
function splitLngLat(value) {
const lngLats = typeof value === 'string' || value instanceof String ? value.split(',') : value;
lngLats.forEach((element, index, lngLats) => {
if (Array.isArray(element)) {
lngLats[index] = splitLngLat(element);
} else if ((typeof element === 'string' || element instanceof String) && element.includes(',')) {
lngLats[index] = splitLngLat(element);
} else {
lngLats[index] = Number.parseFloat(element);
}
});
return lngLats;
}

/**
* @param {String} type GeoJSON
* @param {String} value Value to be analyzed
* @return {Array} split pairs of GeoJSON coordinates
*/
function getLngLats(type, value) {
if (typeof value !== 'string' && Array.isArray(value) === false) {
return value;
}

const lngLats = _.flatten(splitLngLat(value));
if (lngLats.length === 2) {
return { type, coordinates: lngLats };
}

if (lngLats.length % 2 !== 0) {
logger.error(context, 'Bad attribute value type. Expecting geo-coordinates. Received:%s', value);
throw Error();
}
const arr = [];
for (let i = 0, len = lngLats.length; i < len; i = i + 2) {
arr.push([lngLats[i], lngLats[i + 1]]);
}
return { type, coordinates: arr };
}

/**
* Determines if a value is of type float
*
Expand Down Expand Up @@ -212,6 +257,7 @@ exports.getErrorField = intoTrans(context, getErrorField);
exports.createRequestObject = createRequestObject;
exports.applyMiddlewares = applyMiddlewares;
exports.getMetaData = getMetaData;
exports.getLngLats = getLngLats;
exports.castJsonNativeAttributes = castJsonNativeAttributes;
exports.isFloat = isFloat;
exports.updateMiddleware = updateMiddleware;
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"prettier": "prettier --config .prettierrc.json --write '**/**/**/**/*.js' '**/**/**/*.js' '**/**/*.js' '**/*.js' '*.js'",
"prettier:text": "prettier 'README.md' 'doc/*.md' 'doc/**/*.md' --no-config --tab-width 4 --print-width 120 --write --prose-wrap always",
"test": "nyc --reporter=text mocha --recursive 'test/**/*.js' --reporter spec --timeout 5000 --ui bdd --exit --color true",
"test:debug": "mocha --recursive 'test/**/*.js' --reporter spec --inspect-brk --timeout 30000 --ui bdd --exit" ,
"test:debug": "mocha --recursive 'test/**/*.js' --reporter spec --inspect-brk --timeout 30000 --ui bdd --exit",
"test:coverage": "nyc --reporter=lcov mocha -- --recursive 'test/**/*.js' --reporter spec --timeout 5000 --exit",
"test:coveralls": "npm run test:coverage && cat ./coverage/lcov.info | coveralls && rm -rf ./coverage",
"test:watch": "npm run test -- -w ./lib",
Expand Down
Loading