agmission/Development/server/helpers/file_storage.js

487 lines
15 KiB
JavaScript

'use strict';
const path = require('path'),
fs = require('fs-extra'),
util = require('util'),
utils = require('./utils'),
geoUtil = require('./geo_util'),
polyUtil = require('./poly_util'),
FILE = require('./file_constants'),
turf = require('@turf/turf'),
GeojsonRbush = require('@mickeyjohn/geojson-rbush').default,
debug = require('debug')('agm:file_storage'),
{ AppInputError } = require('./app_error');
function getItem(val, pos) {
return val[pos] ? val[pos].trim() : '';
}
/**
* Read DSP or XCL file for polygons
* @param filePath dsp or xcl file path
* @param cb callback function (err, polygon array)
*/
function readXCL_DSP(filePath, cb) {
if (!/\.dsp/i.test(path.extname(filePath)) && !/\.xcl$/i.test(path.extname(filePath))) {
const err = "Invalid file, expect .dsp or .xcl";
if (cb)
return cb(err);
}
fs.readFile(filePath, function (err, data) {
if (err) {
if (cb)
return cb(err);
}
const lines = data.toString().split("\n");
const polys = [];
let fields, coor1, coor2, zoneNum, currZoneNum = 0;
for (const i in lines) {
fields = lines[i].split(new RegExp('\\s+'));
if (fields.length >= 3) {
zoneNum = Number(getItem(fields, 0));
if (zoneNum !== currZoneNum) {
polys.push([]);
currZoneNum = zoneNum;
}
coor1 = parseFloat(getItem(fields, 1)); // X in UTM
coor2 = parseFloat(getItem(fields, 2)); // Y in UTM
if (utils.isNumber(coor1) && utils.isNumber(coor2))
polys[polys.length - 1].push([coor2, coor1]);
}
}
if (cb)
cb(null, polys);
else return polys;
});
}
async function readVFR(filePath) {
let rates = [];
try {
const existed = await fs.pathExists(filePath);
if (!existed) return rates;
const content = await fs.readFile(filePath);
const lines = content.toString().split("\n");
let fields, rate;
for (let line of lines) {
if (!line) continue;
// 1 9.416577 -82.576460 30.00
fields = line.split(new RegExp('\\s+'));
if (fields.length && fields.length >= 4) {
if (utils.isNumber(fields[3])) {
rate = parseFloat(fields[3]);
if (!rates.length || rate != rates[rates.length - 1])
rates.push(rate);
}
}
}
} catch (err) {
debug(err);
}
return rates;
}
function readAGN_PRJ(filePath, areaTypeAndFiles, ops, cb) {
if (!ops || !ops.appRate) {
if (cb) return cb(AppInputError.create());
else AppInputError.throw();
}
const fileType = areaTypeAndFiles.type;
let coorSys, lineNum, fields, coor1, coor2, valueF, swath = 0, swaths = 0, measureUnit = 1; //, rateUnit;
let vertexesSets = [], wPoints = [], rates = [], zoneNames = [], xclItems, vfrRates;
let equo_crossing = 1; // 1: North: 2: South
let zoneCM = 0;
fs.readFile(path.join(filePath, areaTypeAndFiles.area))
.then((data) => {
const lines = data.toString().split("\n");
for (const i in lines) {
fields = lines[i].split(new RegExp('\\s+'));
lineNum = parseInt(getItem(fields, 0));
switch (lineNum) {
case FILE.NO1_CENTRAL_MERIDIAN:
coorSys = getItem(fields, 1);
zoneCM = parseInt(getItem(fields, 2));
break;
case FILE.NO1_EQUATORIAL_CROSSING:
equo_crossing = Number(getItem(fields, 1));
break;
case FILE.NO1_CORNER:
if (fileType === FILE.FILE_NO1) {
if (fields.length >= 3) {
coor1 = parseFloat(getItem(fields, 1));
if (!utils.isNumber(coor1))
break;
coor2 = parseFloat(getItem(fields, 2));
if (!utils.isNumber(coor2))
break;
if (coorSys === 'L')
vertexesSets.push([coor2, coor1]); // For using in GeoJson long,lat
else
vertexesSets.push([coor1, coor2]); // UTM: x, y
}
}
else
continue;
break;
case FILE.NO1_WAYPOINT:
if (ops.onlyAreas) continue;
if (fields.length >= 3) {
coor1 = parseFloat(getItem(fields, 1));
if (!utils.isNumber(coor1))
break;
coor2 = parseFloat(getItem(fields, 2));
if (!utils.isNumber(coor2))
break;
let wptName = "";
if (fields.length >= 4) {
wptName = getItem(fields, 3);
}
if (wptName && wptName.trim().toUpperCase() != "COR1")
wPoints.push({ name: wptName, coors: coorSys === 'L' ? [coor2, coor1] : [coor1, coor2] });
}
break;
case FILE.NO1_DISPLAY_UNIT:
if (fields.length >= 2)
measureUnit = Number(getItem(fields, 1));
break;
case FILE.AGN_APPLICATION_RATE:
if (fields.length >= 6) {
valueF = parseFloat(getItem(fields, 1));
// let unit = getItem(fields, 5).toUpperCase();
if (utils.isNumber(valueF)) {
if (valueF < 0)
valueF = -1.0;
// else if (/OZPA/i.test(unit))
// rateUnit = 0;
// else if (/GPA/i.test(unit))
// rateUnit = 1;
// else if (/LBPA/i.test(unit))
// rateUnit = 2;
// else if (/LPHA/i.test(unit))
// rateUnit = 3;
// else if (/KGPHA/i.test(unit))
// rateUnit = 4;
rates.push(valueF);
}
}
break;
case FILE.NO1_SWATHWIDTH_AC_INSIDE_AREA:
if (fields.length > 1) {
valueF = parseFloat(getItem(fields, 1));
if (utils.isNumber(valueF)) swath = valueF;
}
break;
case FILE.NO1_SWATHWIDTH_SEGMENTS:
for (let i = 1; i <= 4; i++) {
if (i < fields.length) {
valueF = parseFloat(getItem(fields, i));
if (utils.isNumber(valueF)) swaths += valueF;
}
}
break;
case FILE.PRJ_ZONE_NAME:
if (fields.length >= 2)
zoneNames.push(getItem(fields, 1));
break;
default: break;
}
}
if (fileType === FILE.FILE_PRJ && areaTypeAndFiles.dsp) {
return readXCL_DSPAsync(path.join(filePath, areaTypeAndFiles.dsp));
}
else {
vertexesSets = [vertexesSets];
}
})
.then(dspItems => {
// Process for the PRJ or NO1
if (dspItems || fileType === FILE.FILE_NO1) {
if (fileType !== FILE.FILE_NO1)
vertexesSets = dspItems;
if (areaTypeAndFiles.xcl)
return readXCL_DSPAsync(path.join(filePath, areaTypeAndFiles.xcl));
}
})
.then(xcls => {
xclItems = xcls;
if (fileType === FILE.FILE_PRJ)
return readVFR(path.join(filePath, utils.bareFilename(areaTypeAndFiles.area, true) + '.vfr'));
})
.then(vfr => {
vfrRates = vfr;
})
.then(() => {
let jobItem = {};
if (fileType !== FILE.FILE_AGN) {
let sprayAreas = [], wayPoints = [], appRate = ops.appRate;
const appRates = fileType === FILE.FILE_PRJ ? vfrRates : rates;
const bareFilename = utils.normalizeName(utils.bareFilename(areaTypeAndFiles.area));
const needConvert = (coorSys === 'U' || coorSys === 'Z');
let fromPrj, coors;
if (needConvert) fromPrj = geoUtil.getUTMProj(coorSys, equo_crossing, zoneCM);
// Start to convert to Job items
for (let i = 0; i < vertexesSets.length; i++) {
if (sprayAreas.length >= FILE.MAX_ITEM) break;
coors = vertexesSets[i];
if (needConvert) {
try {
coors = geoUtil.UTMtoLnLats(fromPrj, coors, (fileType === FILE.FILE_PRJ));
} catch (err) {
debug("UTMtoLnLats failed !", err);
continue;
}
}
if (utils.isEmptyArray(coors)) continue;
// AppRate for the area(s)
if (!utils.isEmptyArray(appRates))
appRate = i <= appRates.length - 1 ? appRates[i] : appRates[appRates.length - 1];
if (!appRate)
appRate = ops.appRate;
// Changes: Nov.08/21 => use same name from no1 or prj files
let areas = polyUtil.createAreas(0, coors, zoneNames[i] || bareFilename, ops.sprZoneColor, appRate, ops.crop);
// Ensure GeoJson with type = "Feature" for next Xcl names inferring process
if (!utils.isEmptyArray(xclItems)) {
if (!areas[0].type || areas[0].type != "Feature")
areas = areas.map(it => { it.type = "Feature"; return it; });
}
if (sprayAreas.length < FILE.MAX_ITEM && !utils.isEmptyArray(areas)) {
sprayAreas = sprayAreas.concat(areas);
}
}
jobItem["sprayAreas"] = sprayAreas;
if (!utils.isEmptyArray(xclItems)) {
let sprTree = GeojsonRbush();
sprTree.load(sprayAreas);
let xclAreas = [], nearAreas, nameNum = 0;
for (let k = 0; k < xclItems.length; k++) {
if (xclAreas.length >= FILE.MAX_ITEM) break;
coors = xclItems[k];
if (needConvert) {
try {
coors = geoUtil.UTMtoLnLats(fromPrj, coors, true);
} catch (err) {
debug("UTMtoLnLats failed !", err);
continue;
}
}
if (utils.isEmptyArray(coors)) continue;
const xcls = polyUtil.createAreas(1, coors, '');
if (xclAreas.length < FILE.MAX_ITEM && !utils.isEmptyArray(xcls)) {
// Infer the name of each xcl: same name with the outer ring area or simply XCL++;
for (let xcl of xcls) {
if (xcl.type || xcl.type != "Feature")
xcl.type = "Feature";
nearAreas = sprTree.search(xcl);
if (nearAreas && nearAreas.features) {
for (let sprArea of nearAreas.features) {
if (turf.booleanContains(sprArea, xcl)) {
xcl.properties.name = sprArea.properties.name;
break;
}
}
}
if (!xcl.properties.name)
xcl.properties.name = `XCL ${nameNum++}`;
}
xclAreas = xclAreas.concat(xcls);
}
}
jobItem["xclAreas"] = xclAreas;
jobItem.xclAreas = polyUtil.processXclsName(jobItem.sprayAreas, jobItem.xclAreas);
sprTree = null;
}
if (!ops.onlyAreas) {
for (let j = 0; j < wPoints.length; j++) {
if (wayPoints.length >= FILE.MAX_ITEM) break;
try {
coors = wPoints[j].coors;
if (needConvert) {
try {
coors = geoUtil.UTMtoLnLats(fromPrj, coors)[0];
} catch (err) {
debug(err, coors);
continue;
}
}
const wpt = polyUtil.createGeoPoint(2, coors, wPoints[j].name);
if (wpt)
wayPoints.push(wpt);
}
catch (err) {
debug(err)
}
}
jobItem["waypoints"] = wayPoints;
}
}
jobItem.meta = { coorSys: coorSys, hemisphere: equo_crossing, zoneCM: zoneCM, measureUnit: measureUnit, swath: swaths > swath ? swaths : swath };
if (cb) return cb(null, jobItem)
else return jobItem;
})
.catch(err => {
debug(err);
cb && cb(err);
return;
});
}
/**
* Read AgNav q (data info) file
* @param {*} qfileName the absolute path of the file
* @param {*} cb callback function
*/
function readQFile(qfileName, cb) {
fs.readFile(qfileName, function (err, text) {
if (err)
return cb(err);
const textlines = text.toString().split("\n");
let data = {}, line, _lineU, temp;
for (let i = 0; i < textlines.length; i++) {
try {
line = textlines[i].trim(); _lineU = line.toUpperCase();
if (!line.length) continue;
if (_lineU.startsWith('CLIENT'))
data["client"] = utils.line2ndFields(line)[0];
if (_lineU.startsWith('OPERATOR'))
data["operator"] = utils.line2ndFields(line)[0];
if (_lineU.startsWith('NAV/REMARK'))
data["remark"] = utils.line2ndFields(line)[0];
if (_lineU.startsWith('AIRCRAFT'))
data["aircraft"] = utils.line2ndFields(line)[0];
if (_lineU.startsWith('FLIGHT'))
data["flight"] = utils.line2ndFields(line)[0];
if (_lineU.startsWith('JOB'))
data["crop"] = utils.line2ndFields(line)[0];
if (_lineU.startsWith('PRODUCT'))
data["product"] = utils.line2ndFields(line)[0];
if (_lineU.startsWith('GUIA INFO')) {
if (_lineU.includes('GOLD') || _lineU.includes('SILVER'))
data["guiaInfo"] = utils.split2nd(line, ':', ' ');
else
data["guiaInfo"] = utils.line2ndFields(line, true);
}
if (_lineU.startsWith('FC TYPE'))
data["fcType"] = utils.line2ndFields(line)[0];
if (_lineU.startsWith('APP. RATE')) {
temp = utils.line2ndFields(line, true);
data["appRate"] = Number(temp[0]);
data["appRateUnitStr"] = temp[1];
}
if (_lineU.startsWith('DATE'))
data["date"] = utils.line2ndFields(line)[0];
if (_lineU.startsWith('GPS TIME'))
data["gpsTime"] = utils.line2ndFields(line)[0];
if (_lineU.startsWith('AREA/ZONE'))
data["areaOrZone"] = utils.line2ndFields(line)[0];
// <Sprayed Total Swath>
if (_lineU.startsWith('SPRAY COVERAGE')) {
data["sprCoverage"] = utils.line2ndFields(line, true);
data["sprCoverage"] = data["sprCoverage"].map(i => Number(i));
}
// [FLOWCONTROLLER]
if (_lineU.startsWith('PULSESPERLITER'))
data["pulsesPerLit"] = Number(utils.line2ndFields(line, false, '=')[0]);
if (_lineU.startsWith('SPRAYONLAG'))
data["sprOnLag"] = Number(utils.line2ndFields(line)[0]);
if (_lineU.startsWith('SPRAYOFFLAG'))
data["sprOffLag"] = Number(utils.line2ndFields(line)[0]);
} catch (err) {
debug(err);
data = null;
break;
}
}
cb(null, data);
});
}
/*
function writePBMFile(fileName, metadata, cb) {
try {
const lineEnd = '\r\n';
// Line 1 Origin in Lat Lon
let content = utils.truncR(metadata.origin.lat, 6) + ' ' + utils.truncR(metadata.origin.lng, 6) + lineEnd;
// Line 2 Rotation angle
content = content.concat('0' + lineEnd);
// Line 3 Scale X Y
content = content.concat(utils.truncR(metadata.scaleX, 6) + ' ' + metadata.scaleY, 6);
fs.writeFile(fileName + '.pbm', content, { encoding: 'utf8', flag: 'w' }, (err) => {
if (err)
return cb(err, false);
return cb(null, true);
});
} catch (err) {
debug('Error writing file pmb:' + fileName + '.pbm', err);
return cb(err, false);
}
}
*/
const readAGN_PRJAsync = util.promisify(readAGN_PRJ),
readXCL_DSPAsync = util.promisify(readXCL_DSP),
readQFile_Async = util.promisify(readQFile);
module.exports = {
readVFR, readAGN_PRJ, readAGN_PRJAsync, readXCL_DSP, readXCL_DSPAsync, readQFile, readQFile_Async
};