Skip to content

Commit a7eb260

Browse files
authored
Merge pull request #854 from jason-fox/feature/ngsi-ld
For NGSI-v2, ensure GeoJSON is correctly encode in the request.
2 parents 925aa86 + f4ebb69 commit a7eb260

23 files changed

+833
-63
lines changed

CHANGES_NEXT_RELEASE

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,5 @@ Add husky and lint-staged
1919
Fix: Add support for lazy and internal_attributes in service notifications to Manager (#768)
2020
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)
2121
Fix: bug in legacy and JEXL expression that was not converting "0" to 0
22-
Fix: support for mapping different attributes to the same entity_name
22+
Fix: support for mapping different attributes to the same entity_name
23+
Fix: Ensure GeoJSON is correctly encoded in NGSI-v2 requests (#961)

doc/advanced-topics.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,64 @@ curl http://${KEYSTONE_HOST}/v3/OS-TRUST/trusts \
4646
Apart from the generation of the trust, the use of secured Context Brokers should be transparent to the user of the IoT
4747
Agent.
4848

49+
### GeoJSON support
50+
51+
The defined `type` of any GeoJSON attribute can be any set to any of the standard NGSI-v2 GeoJSON types - (e.g.
52+
`geo:json`, `geo:point`). NGSI-LD formats such as `GeoProperty`, `Point` and `LineString` are also accepted `type`
53+
values. If the latitude and longitude are received as separate measures, the
54+
[expression language](expressionLanguage.md) can be used to concatenate them.
55+
56+
````json
57+
{
58+
"entity_type": "GPS",
59+
"resource": "/iot/d",
60+
"protocol": "PDI-IoTA-JSON",
61+
..etc
62+
"attributes": [
63+
{
64+
"name": "location",
65+
"type": "geo:json",
66+
"expression": "${@lng}, ${@lat}"
67+
}
68+
]
69+
}
70+
```json
71+
72+
73+
For `attributes` and `static_attributes` which need to be formatted as GeoJSON values, three separate input
74+
formats are accepted. Provided the `type` is provisioned correctly, the `value` may be defined using any of
75+
the following formats:
76+
77+
- a comma delimited string
78+
79+
```json
80+
{
81+
"name": "location",
82+
"value": "23, 12.5"
83+
}
84+
````
85+
86+
- an array of numbers
87+
88+
```json
89+
{
90+
"name": "location",
91+
"value": [23, 12.5]
92+
}
93+
```
94+
95+
- an fully formatted GeoJSON object
96+
97+
```json
98+
{
99+
"name": "location",
100+
"value": {
101+
"type": "Point",
102+
"coordinates": [23, 12.5]
103+
}
104+
}
105+
```
106+
49107
### Metadata support
50108

51109
Both `attributes` and `static_attributes` may be supplied with metadata when provisioning an IoT Agent, so that the

lib/services/devices/devices-NGSI-v2.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
* Modified by: Jason Fox - FIWARE Foundation
2626
*/
2727

28+
/* eslint-disable consistent-return */
29+
2830
const request = require('request');
2931
const async = require('async');
3032
const apply = async.apply;
@@ -38,6 +40,7 @@ const config = require('../../commonConfig');
3840
const registrationUtils = require('./registrationUtils');
3941
const _ = require('underscore');
4042
const utils = require('../northBound/restUtils');
43+
const NGSIv2 = require('../ngsi/entities-NGSI-v2');
4144
const moment = require('moment');
4245
const context = {
4346
op: 'IoTAgentNGSI.Devices-v2'
@@ -232,6 +235,15 @@ function createInitialEntityNgsi2(deviceData, newDevice, callback) {
232235
jsonConcat(options.json, formatAttributesNgsi2(deviceData.staticAttributes, true));
233236
jsonConcat(options.json, formatCommandsNgsi2(deviceData.commands));
234237

238+
for (const att in options.json) {
239+
try {
240+
// Format any GeoJSON attrs properly
241+
options.json[att] = NGSIv2.formatGeoAttrs(options.json[att]);
242+
} catch (error) {
243+
return callback(new errors.BadGeocoordinates(JSON.stringify(options.json)));
244+
}
245+
}
246+
235247
logger.debug(context, 'deviceData: %j', deviceData);
236248
if (
237249
('timestamp' in deviceData && deviceData.timestamp !== undefined
@@ -281,6 +293,15 @@ function updateEntityNgsi2(deviceData, updatedDevice, callback) {
281293
jsonConcat(options.json, formatAttributesNgsi2(deviceData.staticAttributes, true));
282294
jsonConcat(options.json, formatCommandsNgsi2(deviceData.commands));
283295

296+
for (const att in options.json) {
297+
try {
298+
// Format any GeoJSON attrs properly
299+
options.json[att] = NGSIv2.formatGeoAttrs(options.json[att]);
300+
} catch (error) {
301+
return callback(new errors.BadGeocoordinates(JSON.stringify(options.json)));
302+
}
303+
}
304+
284305
if (
285306
('timestamp' in deviceData && deviceData.timestamp !== undefined
286307
? deviceData.timestamp

lib/services/ngsi/entities-NGSI-LD.js

Lines changed: 7 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -56,45 +56,6 @@ function valueOfOrNull(value) {
5656
return isNaN(value) ? NGSI_LD_NULL : value;
5757
}
5858

59-
/**
60-
* @param {String/Array} value Comma separated list or array of values
61-
* @return {Array} Array of Lat/Lngs for use as GeoJSON
62-
*/
63-
function splitLngLat(value) {
64-
const lngLats = typeof value === 'string' || value instanceof String ? value.split(',') : value;
65-
lngLats.forEach((element, index, lngLats) => {
66-
if (Array.isArray(element)) {
67-
lngLats[index] = splitLngLat(element);
68-
} else if ((typeof element === 'string' || element instanceof String) && element.includes(',')) {
69-
lngLats[index] = splitLngLat(element);
70-
} else {
71-
lngLats[index] = Number.parseFloat(element);
72-
}
73-
});
74-
return lngLats;
75-
}
76-
77-
/**
78-
* @param {String} value Value to be analyzed
79-
* @return {Array} split pairs of GeoJSON coordinates
80-
*/
81-
function getLngLats(value) {
82-
const lngLats = _.flatten(splitLngLat(value));
83-
if (lngLats.length === 2) {
84-
return lngLats;
85-
}
86-
87-
if (lngLats.length % 2 !== 0) {
88-
logger.error(context, 'Bad attribute value type. Expecting geo-coordinates. Received:%s', value);
89-
throw Error();
90-
}
91-
const arr = [];
92-
for (let i = 0, len = lngLats.length; i < len; i = i + 2) {
93-
arr.push([lngLats[i], lngLats[i + 1]]);
94-
}
95-
return arr;
96-
}
97-
9859
/**
9960
* Amends an NGSIv2 attribute to NGSI-LD format
10061
* All native JSON types are respected and cast as Property values
@@ -155,33 +116,34 @@ function convertNGSIv2ToLD(attr) {
155116
case 'geoproperty':
156117
case 'point':
157118
case 'geo:point':
119+
case 'geo:json':
158120
obj.type = 'GeoProperty';
159-
obj.value = { type: 'Point', coordinates: getLngLats(attr.value) };
121+
obj.value = NGSIUtils.getLngLats('Point', attr.value);
160122
break;
161123
case 'linestring':
162124
case 'geo:linestring':
163125
obj.type = 'GeoProperty';
164-
obj.value = { type: 'LineString', coordinates: getLngLats(attr.value) };
126+
obj.value = NGSIUtils.getLngLats('LineString', attr.value);
165127
break;
166128
case 'polygon':
167129
case 'geo:polygon':
168130
obj.type = 'GeoProperty';
169-
obj.value = { type: 'Polygon', coordinates: getLngLats(attr.value) };
131+
obj.value = NGSIUtils.getLngLats('Polygon', attr.value);
170132
break;
171133
case 'multipoint':
172134
case 'geo:multipoint':
173135
obj.type = 'GeoProperty';
174-
obj.value = { type: 'MultiPoint', coordinates: getLngLats(attr.value) };
136+
obj.value = NGSIUtils.getLngLats('MultiPoint', attr.value);;
175137
break;
176138
case 'multilinestring':
177139
case 'geo:multilinestring':
178140
obj.type = 'GeoProperty';
179-
obj.value = { type: 'MultiLineString', coordinates: attr.value };
141+
obj.value = NGSIUtils.getLngLats( 'MultiLineString', attr.value);
180142
break;
181143
case 'multipolygon':
182144
case 'geo:multipolygon':
183145
obj.type = 'GeoProperty';
184-
obj.value = { type: 'MultiPolygon', coordinates: attr.value };
146+
obj.value = NGSIUtils.getLngLats( 'MultiPolygon', attr.value);
185147
break;
186148

187149
// Relationships

lib/services/ngsi/entities-NGSI-v2.js

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
* Modified by: Jason Fox - FIWARE Foundation
2626
*/
2727

28+
/* eslint-disable consistent-return */
29+
2830
const request = require('request');
2931
const statsService = require('./../stats/statsRegistry');
3032
const async = require('async');
@@ -35,11 +37,59 @@ const utils = require('../northBound/restUtils');
3537
const config = require('../../commonConfig');
3638
const constants = require('../../constants');
3739
const moment = require('moment-timezone');
40+
const NGSIUtils = require('./ngsiUtils');
3841
const logger = require('logops');
3942
const context = {
4043
op: 'IoTAgentNGSI.Entities-v2'
4144
};
42-
const NGSIUtils = require('./ngsiUtils');
45+
46+
/**
47+
* Amends an NGSIv2 Geoattribute from String to GeoJSON format
48+
*
49+
* @param {Object} attr Attribute to be analyzed
50+
* @return {Object} GeoJSON version of the attribute
51+
*/
52+
function formatGeoAttrs(attr) {
53+
const obj = attr;
54+
if (attr.type) {
55+
switch (attr.type.toLowerCase()) {
56+
// GeoProperties
57+
case 'geo:json':
58+
case 'geoproperty':
59+
case 'point':
60+
case 'geo:point':
61+
obj.type = 'geo:json';
62+
obj.value = NGSIUtils.getLngLats('Point', attr.value);
63+
break;
64+
case 'linestring':
65+
case 'geo:linestring':
66+
obj.type = 'geo:json';
67+
obj.value = NGSIUtils.getLngLats('LineString', attr.value);
68+
break;
69+
case 'polygon':
70+
case 'geo:polygon':
71+
obj.type = 'geo:json';
72+
obj.value = NGSIUtils.getLngLats('Polygon', attr.value);
73+
break;
74+
case 'multipoint':
75+
case 'geo:multipoint':
76+
obj.type = 'geo:json';
77+
obj.value = NGSIUtils.getLngLats('MultiPoint', attr.value);
78+
break;
79+
case 'multilinestring':
80+
case 'geo:multilinestring':
81+
obj.type = 'geo:json';
82+
obj.value = NGSIUtils.getLngLats('MultiLineString', attr.value);
83+
break;
84+
case 'multipolygon':
85+
case 'geo:multipolygon':
86+
obj.type = 'geo:json';
87+
obj.value = NGSIUtils.getLngLats('MultiPolygon', attr.value);
88+
break;
89+
}
90+
}
91+
return obj;
92+
}
4393

4494
function addTimestampNgsi2(payload, timezone) {
4595
function addTimestampEntity(entity, timezone) {
@@ -387,6 +437,13 @@ function sendUpdateValueNgsi2(entityName, attributes, typeInformation, token, ca
387437
if (options.json.entities[entity][att].name) {
388438
delete options.json.entities[entity][att].name;
389439
}
440+
441+
try {
442+
// Format any GeoJSON attrs properly
443+
options.json.entities[entity][att] = formatGeoAttrs(options.json.entities[entity][att]);
444+
} catch (error) {
445+
return callback(new errors.BadGeocoordinates(JSON.stringify(options.json)));
446+
}
390447
}
391448
}
392449
} else {
@@ -402,6 +459,13 @@ function sendUpdateValueNgsi2(entityName, attributes, typeInformation, token, ca
402459
if (options.json[att].name) {
403460
delete options[att].name;
404461
}
462+
463+
try {
464+
// Format any GeoJSON attrs properly
465+
options.json[att] = formatGeoAttrs(options.json[att]);
466+
} catch (error) {
467+
return callback(new errors.BadGeocoordinates(JSON.stringify(options.json)));
468+
}
405469
}
406470
}
407471

@@ -424,3 +488,4 @@ function sendUpdateValueNgsi2(entityName, attributes, typeInformation, token, ca
424488
exports.sendQueryValue = sendQueryValueNgsi2;
425489
exports.sendUpdateValue = sendUpdateValueNgsi2;
426490
exports.addTimestamp = addTimestampNgsi2;
491+
exports.formatGeoAttrs = formatGeoAttrs;

lib/services/ngsi/ngsiUtils.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,51 @@ const _ = require('underscore');
3434
const config = require('../../commonConfig');
3535
const updateMiddleware = [];
3636
const queryMiddleware = [];
37+
38+
/**
39+
* @param {String/Array} value Comma separated list or array of values
40+
* @return {Array} Array of Lat/Lngs for use as GeoJSON
41+
*/
42+
function splitLngLat(value) {
43+
const lngLats = typeof value === 'string' || value instanceof String ? value.split(',') : value;
44+
lngLats.forEach((element, index, lngLats) => {
45+
if (Array.isArray(element)) {
46+
lngLats[index] = splitLngLat(element);
47+
} else if ((typeof element === 'string' || element instanceof String) && element.includes(',')) {
48+
lngLats[index] = splitLngLat(element);
49+
} else {
50+
lngLats[index] = Number.parseFloat(element);
51+
}
52+
});
53+
return lngLats;
54+
}
55+
56+
/**
57+
* @param {String} type GeoJSON
58+
* @param {String} value Value to be analyzed
59+
* @return {Array} split pairs of GeoJSON coordinates
60+
*/
61+
function getLngLats(type, value) {
62+
if (typeof value !== 'string' && Array.isArray(value) === false) {
63+
return value;
64+
}
65+
66+
const lngLats = _.flatten(splitLngLat(value));
67+
if (lngLats.length === 2) {
68+
return { type, coordinates: lngLats };
69+
}
70+
71+
if (lngLats.length % 2 !== 0) {
72+
logger.error(context, 'Bad attribute value type. Expecting geo-coordinates. Received:%s', value);
73+
throw Error();
74+
}
75+
const arr = [];
76+
for (let i = 0, len = lngLats.length; i < len; i = i + 2) {
77+
arr.push([lngLats[i], lngLats[i + 1]]);
78+
}
79+
return { type, coordinates: arr };
80+
}
81+
3782
/**
3883
* Determines if a value is of type float
3984
*
@@ -212,6 +257,7 @@ exports.getErrorField = intoTrans(context, getErrorField);
212257
exports.createRequestObject = createRequestObject;
213258
exports.applyMiddlewares = applyMiddlewares;
214259
exports.getMetaData = getMetaData;
260+
exports.getLngLats = getLngLats;
215261
exports.castJsonNativeAttributes = castJsonNativeAttributes;
216262
exports.isFloat = isFloat;
217263
exports.updateMiddleware = updateMiddleware;

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
"prettier": "prettier --config .prettierrc.json --write '**/**/**/**/*.js' '**/**/**/*.js' '**/**/*.js' '**/*.js' '*.js'",
3838
"prettier:text": "prettier 'README.md' 'doc/*.md' 'doc/**/*.md' --no-config --tab-width 4 --print-width 120 --write --prose-wrap always",
3939
"test": "nyc --reporter=text mocha --recursive 'test/**/*.js' --reporter spec --timeout 5000 --ui bdd --exit --color true",
40-
"test:debug": "mocha --recursive 'test/**/*.js' --reporter spec --inspect-brk --timeout 30000 --ui bdd --exit" ,
40+
"test:debug": "mocha --recursive 'test/**/*.js' --reporter spec --inspect-brk --timeout 30000 --ui bdd --exit",
4141
"test:coverage": "nyc --reporter=lcov mocha -- --recursive 'test/**/*.js' --reporter spec --timeout 5000 --exit",
4242
"test:coveralls": "npm run test:coverage && cat ./coverage/lcov.info | coveralls && rm -rf ./coverage",
4343
"test:watch": "npm run test -- -w ./lib",

0 commit comments

Comments
 (0)