lobo-irl
Version:
Queries live data from the Land/Ocean Biogeochemical Observatory sensors operating in the Indian River Lagoon.
206 lines (186 loc) • 6.77 kB
JavaScript
/**************************************************
* Lobo - Indian River Lagoon Client
* Written by Steven Hunt
* MIT License
**************************************************/
var request = require('superagent'),
xml2js = require('xml2js'),
Promise = require('bluebird'),
_ = require('lodash'),
measures = require('./measurements'),
utils = require('./utils'),
config = utils.loadConfig();
// hash table for getting the data name.
var measurementNames = {};
// build the hash table.
_.keys(config.measurements).forEach(key => {
measurementNames[key] = config.measurements[key].name;
});
/**
* Returns a list of all sensors.
* @returns {Array} The collection of sensor names.
*/
exports.getSensors = function () {
return _.keys(config.sensors);
};
/**
* @private
* Gets the sensors within a specified area.
* @param minLat {float} The minimum latitude.
* @param maxLat {float} The maximum latitude.
* @param minLng {float} The minimum longitude.
* @param maxLng {float} The maximum longitude.
* @returns {Array} The collection of sensor names within the given area.
*/
exports.getSensorsByArea = function (minLat, maxLat, minLng, maxLng) {
var result = [];
for (var key in config.sensors) {
if (config.sensors.hasOwnProperty(key)) {
var sensor = config.sensors[key];
if (sensor.location.lat >= minLat &&
sensor.location.lat <= maxLat &&
sensor.location.lng >= minLng &&
sensor.location.lng <= maxLng) {
result.push(key);
}
}
}
return result;
};
/**
* @private
* Gets detailed information about a specific sensor.
* @param key {string} The sensor name.
* @returns {Object} Information about the sensor.
*/
exports.getSensor = function (key) {
return config.sensors[key] || null;
};
// caches requests to avoid re-querying data.
var requestCache = {};
/**
*
* @param url {string} The url to use when querying the sensor.
* @param [noCache] {boolean} Indicates whether or not to allow caching of responses.
* @returns {Promise} A promise for when the request has been completed.
*/
function sendSensorRequest(url, noCache) {
return new Promise((resolve, reject) => {
// check the cache first if no-cache is off.
if (!noCache && requestCache[url]) {
resolve(requestCache[url]);
}
else {
// perform the request.
request.get(url)
.send()
.end((err, res) => {
if (err || !res || !res.text) { reject(err); }
else {
resolve(res.text);
// cache the result if allowed.
if (!noCache) {
requestCache[url] = res.text;
}
}
});
}
});
}
/**
* @private
* Processes the data for the given sensor.
* @param sensor {string} The sensor key.
* @param text {string} The raw sensor data.
* @param resolve {function(result)} A resolve callback.
* @param reject {function(err)} A reject callback.
*/
function processSensorData(sensor, text, resolve, reject) {
try {
var resultLines = [],
resultMeasurements = [],
result = {},
time = null,
measurements = measures.getMeasurements();
// a collection of parsing strategies to use on the file.
var parsers = [
{
// example:
// <p>2016-07-22 09:00:00 EST</p>
condition: line => line.indexOf('<p>') === 0 && line.indexOf(':') > 0,
run: line => {
time = line
.replace('/p', 'p')
.replace(/<p>/g, '');
}
},
{
// everything else.
condition: line => true,
run: line => {
// for each line look for the measurement name at the beginning of the line.
// note: every() will exit if the function returns false, whereas forEach will not exit the loop.
measurements.every(function (key) {
if (line.indexOf(`${measurementNames[key]}: `) === 0) {
resultLines.push(line);
resultMeasurements.push(key);
return false; // stop looping if we found the match.
}
return true;
});
}
}
];
// split the result by line
text.split("\n").forEach(function (line) {
parsers.every(function (parser) {
if (parser.condition(line)) {
parser.run(line);
return false;
}
return true;
});
});
// Example line:
// pH: <b>7.947</b> <br/>
for (var i = 0; i < resultLines.length; i += 1) {
// extract the measurement value.
result[resultMeasurements[i]] = resultLines[i]
.replace('<b>', '') // remove the beginning <b> tag.
.split('</b>')[0] // take everything to the left of the ending <b> tag.
.split(':')[1] // split by the colon and take the right value.
.trim();
// if the measurement is a float, parse it.
if (measures.getMeasurement(resultMeasurements[i]).type === 'float') {
result[resultMeasurements[i]] = parseFloat(result[resultMeasurements[i]]);
}
}
// report back on the data.
resolve({
sensor: sensor,
dateTime: time,
data: result
});
}
catch (err) {
reject(err);
}
}
/**
* @private
* Given a sensor key, queries the sensor and parses the result to return an object containing the latest measurements.
* @param sensor {string} The sensor key.
* @param [noCache] {boolean} Indicates whether or not to allow caching of responses.
* @returns {Promise} A promise object for when the data has been queried and processed.
*/
exports.getSensorData = function (sensor, noCache) {
return new Promise((resolve, reject) => {
// get the sensor details.
var sensorInfo = exports.getSensor(sensor);
// use the url to query the sensor.
sendSensorRequest(sensorInfo.url, noCache)
// then process the sensor data.
.then(text => processSensorData(sensor, text, resolve, reject))
.catch(reject);
});
};