Skip to content

Commit 20fb934

Browse files
committed
Rebasing PR
- For NGSI-v2, ensure GeoJSON is correctly encode in the request. … - Move Lat/Lng processing to a common location - For NGIS-v2, iterate attributes and convert common GeoJSON types - Ensure that if a GeoJSON object is parsed, it remains as GeoJSON (v2 and LD) - Amend test expectations to ensure geo:json is properly formated. - Add new NGSI-v2 tests for GeoJSON - Basic GeoJSON documentation. - Ensure consistent GeoProvisioning … - NGSI-LD supports geo:json as alias for GeoProperty - Add expression tests - Amend expectations.
1 parent 53e46a2 commit 20fb934

24 files changed

+975
-63
lines changed

doc/advanced-topics.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,67 @@ 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
53+
`type` values. If the latitude and longitude are received as separate measures, the [expression language](expressionLanguage.md) can be used
54+
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": [
103+
23,
104+
12.5
105+
]
106+
}
107+
}
108+
```
109+
49110
### Metadata support
50111

51112
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: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ const config = require('../../commonConfig');
3838
const registrationUtils = require('./registrationUtils');
3939
const _ = require('underscore');
4040
const utils = require('../northBound/restUtils');
41+
const NGSIv2 = require('../ngsi/entities-NGSI-v2');
4142
const moment = require('moment');
4243
const context = {
4344
op: 'IoTAgentNGSI.Devices-v2'
@@ -235,6 +236,15 @@ function createInitialEntityNgsi2(deviceData, newDevice, callback) {
235236
jsonConcat(options.json, formatAttributesNgsi2(deviceData.staticAttributes, true));
236237
jsonConcat(options.json, formatCommandsNgsi2(deviceData.commands));
237238

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

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+
286305
if (
287306
('timestamp' in deviceData && deviceData.timestamp !== undefined ?
288307
deviceData.timestamp : config.getConfig().timestamp) &&

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

Lines changed: 8 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -54,51 +54,12 @@ function valueOfOrNull(value) {
5454
return isNaN(value) ? NGSI_LD_NULL : value;
5555
}
5656

57-
/**
58-
* @param {String/Array} value Comma separated list or array of values
59-
* @return {Array} Array of Lat/Lngs for use as GeoJSON
60-
*/
61-
function splitLngLat(value) {
62-
const lngLats = typeof value === 'string' || value instanceof String ? value.split(',') : value;
63-
lngLats.forEach((element, index, lngLats) => {
64-
if (Array.isArray(element)) {
65-
lngLats[index] = splitLngLat(element);
66-
} else if ((typeof element === 'string' || element instanceof String) && element.includes(',')) {
67-
lngLats[index] = splitLngLat(element);
68-
} else {
69-
lngLats[index] = Number.parseFloat(element);
70-
}
71-
});
72-
return lngLats;
73-
}
74-
75-
/**
76-
* @param {String} value Value to be analyzed
77-
* @return {Array} split pairs of GeoJSON coordinates
78-
*/
79-
function getLngLats(value) {
80-
const lngLats = _.flatten(splitLngLat(value));
81-
if (lngLats.length === 2) {
82-
return lngLats;
83-
}
84-
85-
if (lngLats.length % 2 !== 0) {
86-
logger.error(context, 'Bad attribute value type.' + 'Expecting geo-coordinates. Received:%s', value);
87-
throw Error();
88-
}
89-
const arr = [];
90-
for (let i = 0, len = lngLats.length; i < len; i = i + 2) {
91-
arr.push([lngLats[i], lngLats[i + 1]]);
92-
}
93-
return arr;
94-
}
95-
9657
/**
9758
* Amends an NGSIv2 attribute to NGSI-LD format
9859
* All native JSON types are respected and cast as Property values
9960
* Relationships must be give the type relationship
10061
*
101-
* @param {String} attr Attribute to be analyzed
62+
* @param {Object} attr Attribute to be analyzed
10263
* @return {Object} an object containing the attribute in NGSI-LD
10364
* format
10465
*/
@@ -153,33 +114,34 @@ function convertNGSIv2ToLD(attr) {
153114
case 'geoproperty':
154115
case 'point':
155116
case 'geo:point':
117+
case 'geo:json':
156118
obj.type = 'GeoProperty';
157-
obj.value = { type: 'Point', coordinates: getLngLats(attr.value) };
119+
obj.value = NGSIUtils.getLngLats ('Point', attr.value);
158120
break;
159121
case 'linestring':
160122
case 'geo:linestring':
161123
obj.type = 'GeoProperty';
162-
obj.value = { type: 'LineString', coordinates: getLngLats(attr.value) };
124+
obj.value = NGSIUtils.getLngLats('LineString', attr.value);
163125
break;
164126
case 'polygon':
165127
case 'geo:polygon':
166128
obj.type = 'GeoProperty';
167-
obj.value = { type: 'Polygon', coordinates: getLngLats(attr.value) };
129+
obj.value = NGSIUtils.getLngLats('Polygon', attr.value);
168130
break;
169131
case 'multipoint':
170132
case 'geo:multipoint':
171133
obj.type = 'GeoProperty';
172-
obj.value = { type: 'MultiPoint', coordinates: getLngLats(attr.value) };
134+
obj.value = NGSIUtils.getLngLats('MultiPoint', attr.value);
173135
break;
174136
case 'multilinestring':
175137
case 'geo:multilinestring':
176138
obj.type = 'GeoProperty';
177-
obj.value = { type: 'MultiLineString', coordinates: attr.value };
139+
obj.value = NGSIUtils.getLngLats('MultiLineString', attr.value);
178140
break;
179141
case 'multipolygon':
180142
case 'geo:multipolygon':
181143
obj.type = 'GeoProperty';
182-
obj.value = { type: 'MultiPolygon', coordinates: attr.value };
144+
obj.value = NGSIUtils.getLngLats('MultiPolygon', attr.value);
183145
break;
184146

185147
// Relationships

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

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,60 @@ const utils = require('../northBound/restUtils');
3535
const config = require('../../commonConfig');
3636
const constants = require('../../constants');
3737
const moment = require('moment-timezone');
38+
const NGSIUtils = require('./ngsiUtils');
3839
const logger = require('logops');
3940
const context = {
4041
op: 'IoTAgentNGSI.Entities-v2'
4142
};
42-
const NGSIUtils = require('./ngsiUtils');
43+
44+
/**
45+
* Amends an NGSIv2 Geoattribute from String to GeoJSON format
46+
*
47+
* @param {Object} attr Attribute to be analyzed
48+
* @return {Object} GeoJSON version of the attribute
49+
*/
50+
function formatGeoAttrs(attr) {
51+
const obj = attr;
52+
if (attr.type) {
53+
switch (attr.type.toLowerCase()) {
54+
// GeoProperties
55+
case 'geo:json':
56+
case 'geoproperty':
57+
case 'point':
58+
case 'geo:point':
59+
obj.type = 'geo:json';
60+
obj.value = NGSIUtils.getLngLats('Point', attr.value);
61+
break;
62+
case 'linestring':
63+
case 'geo:linestring':
64+
obj.type = 'geo:json';
65+
obj.value = NGSIUtils.getLngLats('LineString', attr.value);
66+
break;
67+
case 'polygon':
68+
case 'geo:polygon':
69+
obj.type = 'geo:json';
70+
obj.value = NGSIUtils.getLngLats('Polygon', attr.value);
71+
break;
72+
case 'multipoint':
73+
case 'geo:multipoint':
74+
obj.type = 'geo:json';
75+
obj.value = NGSIUtils.getLngLats('MultiPoint', attr.value);
76+
break;
77+
case 'multilinestring':
78+
case 'geo:multilinestring':
79+
obj.type = 'geo:json';
80+
obj.value = NGSIUtils.getLngLats('MultiLineString', attr.value);
81+
break;
82+
case 'multipolygon':
83+
case 'geo:multipolygon':
84+
obj.type = 'geo:json';
85+
obj.value = NGSIUtils.getLngLats('MultiPolygon', attr.value);
86+
break;
87+
}
88+
}
89+
return obj;
90+
}
91+
4392

4493
function addTimestampNgsi2(payload, timezone) {
4594
function addTimestampEntity(entity, timezone) {
@@ -379,6 +428,13 @@ function sendUpdateValueNgsi2(entityName, attributes, typeInformation, token, ca
379428
if (options.json.entities[entity][att].multi) {
380429
delete options.json.entities[entity][att].multi;
381430
}
431+
try {
432+
// Format any GeoJSON attrs properly
433+
options.json.entities[entity][att] = formatGeoAttrs(options.json.entities[entity][att]);
434+
} catch (error) {
435+
return callback(new errors.BadGeocoordinates(JSON.stringify(options.json)));
436+
}
437+
382438
}
383439
}
384440
} else {
@@ -391,6 +447,13 @@ function sendUpdateValueNgsi2(entityName, attributes, typeInformation, token, ca
391447
if (options.json[att].multi) {
392448
delete options.json[att].multi;
393449
}
450+
451+
try {
452+
// Format any GeoJSON attrs properly
453+
options.json[att] = formatGeoAttrs(options.json[att]);
454+
} catch (error) {
455+
return callback(new errors.BadGeocoordinates(JSON.stringify(options.json)));
456+
}
394457
}
395458
}
396459

@@ -413,3 +476,4 @@ function sendUpdateValueNgsi2(entityName, attributes, typeInformation, token, ca
413476
exports.sendQueryValue = sendQueryValueNgsi2;
414477
exports.sendUpdateValue = sendUpdateValueNgsi2;
415478
exports.addTimestamp = addTimestampNgsi2;
479+
exports.formatGeoAttrs = formatGeoAttrs;

lib/services/ngsi/ngsiUtils.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,53 @@ const _ = require('underscore');
3333
const config = require('../../commonConfig');
3434
const updateMiddleware = [];
3535
const queryMiddleware = [];
36+
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+
63+
if(typeof value !== 'string' && Array.isArray(value) === false ){
64+
return value;
65+
}
66+
67+
const lngLats = _.flatten(splitLngLat(value));
68+
if (lngLats.length === 2) {
69+
return { type, coordinates: lngLats};
70+
}
71+
72+
if (lngLats.length % 2 !== 0) {
73+
logger.error(context, 'Bad attribute value type.' + 'Expecting geo-coordinates. Received:%s', value);
74+
throw Error();
75+
}
76+
const arr = [];
77+
for (let i = 0, len = lngLats.length; i < len; i = i + 2) {
78+
arr.push([lngLats[i], lngLats[i + 1]]);
79+
}
80+
return { type, coordinates: arr };
81+
}
82+
3683
/**
3784
* Determines if a value is of type float
3885
*
@@ -211,6 +258,7 @@ exports.getErrorField = intoTrans(context, getErrorField);
211258
exports.createRequestObject = createRequestObject;
212259
exports.applyMiddlewares = applyMiddlewares;
213260
exports.getMetaData = getMetaData;
261+
exports.getLngLats = getLngLats;
214262
exports.castJsonNativeAttributes = castJsonNativeAttributes;
215263
exports.isFloat = isFloat;
216264
exports.updateMiddleware = updateMiddleware;
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
[
2+
{
3+
"@context": "http://context.json-ld",
4+
"longitude": {
5+
"type": "Property",
6+
"value": 0.44
7+
},
8+
"latitude": {
9+
"type": "Property",
10+
"value": 10
11+
},
12+
"location": {
13+
"type": "GeoProperty",
14+
"value": {
15+
"type": "Point",
16+
"coordinates": [
17+
0.44,
18+
10
19+
]
20+
}
21+
},
22+
"id": "urn:ngsi-ld:WeatherStation:ws1",
23+
"type": "WeatherStation"
24+
}
25+
]

0 commit comments

Comments
 (0)