'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]; // 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 };