487 lines
15 KiB
JavaScript
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
|
|
};
|