const utils = require('./utils'), // debug = require('debug')('agm:gridline_util'), Zone = require('./geometry/zone').Zone, ZoneType = require('./geometry/zone').Type, Bound = require('./geometry/rectbound').Bound, geoUtil = require('./geo_util'), polyUtil = require('./poly_util'), jobUtil = require('./job_util'), mathUtil = require('./math_helper'), bufUtil = require('./line_buffer'), cloneDeep = require('clone-deep'), _ = require("lodash"), { Errors } = require('./constants'); // UTM and LatLon LatLonEllipsoidal for accuracy, for convert ll and utm let Utm, LatLonUTM; // LatLonSpherical for calculating distances, bearings, destinations, etc on great circle paths and rhumb lines. let LatLonSP; let { rotateDEG, translate, transform, inverse, applyToPoint, applyToPoints } = require('transformation-matrix'); const { AppInputError } = require('./app_error'); const deg2rad = 0.01745329251994328; const rad2deg = 57.29577951308238; // degrees const MIN_SWATH_WIDTH = 1; // meter const MIN_SEGMENT_LENGTH = 10; // meters const BEST_HEADING_INC = 10; // degrees const BST_HDG = -999; // best heading function latlngToUTMCoors(coors, refZone) { if (!coors || !coors.length) return null; if (!refZone) refZone = { zone: undefined, hemisphere: undefined }; let lnlat, utm, latlonObj = new LatLonUTM(0, 0), _coors = []; for (let k = 0; k < coors.length; k++) { lnlat = coors[k]; latlonObj.lat = lnlat[1]; latlonObj.lon = lnlat[0]; try { utm = latlonObj.toUtm(refZone.zone, refZone.hemisphere); } catch (error) { // Handle the invalid UTM zone after converted case _coors = []; break; } if (undefined == refZone.zone) { refZone.zone = utm.zone; refZone.hemisphere = utm.hemisphere; } _coors.push({ x: utm.easting, y: utm.northing }); } if (!_coors.length) return null; else return { utmCoors: _coors }; } /** * Generate Gridlines for area(s) in LatLon. * @param {*} job the given job * @param {*} sprayIds sprayArea Id * @param {*} halfSwathOffset should offset 1/2 swath * @param {*} x UTM coordinate of the master point x * @param {*} y UTM coordinate of the master point x * @param {*} heading heading of the guide lines * @param {*} abLine AB Line * @param {*} options generate option { regenerate: true/false }; * normally use useGroup for interactive with user with selected areas as one group to generate lines with the same heading */ async function getLinesLatLng(job, sprayIds, halfSwathOffset = true, x, y, heading = 0, abLine, options = { useGroup: false, regenerate: true }) { const swath = utils.toMeter(job.swathWidth, job.measureUnit); if (swath < 4.99) AppInputError.throw(Errors.INVALID_SWATH_WIDTH); // Dynamically load ES geodesy modules. No need when all in ESM codes later. const llsMod = await import('@mickeyjohn/geodesy/latlon-spherical.js'); LatLonSP = llsMod.default; const utmMod = await import('@mickeyjohn/geodesy/utm.js'); Utm = utmMod.default; LatLonUTM = utmMod.LatLon; sprayIds = sprayIds || []; let utmSprayZones = [], utmXclZones = [], latlng = new LatLonUTM(0, 0); // Get the copy of arrays to work on to avoid direct mutation to jobs's data (JavaScript) let sprayZones = job.sprayAreas.slice(0), xclZones = job.excludedAreas.slice(0), bufs = job.bufs.slice(0); let masterPoint, genHeading; let linesResult = [], genResult, sprZones; let bbox = geoUtil.updateAreasBBoxLL(sprayZones); let refZone = geoUtil.calcRefZonebyBbox(bbox) || { zone: undefined, hemisphere: undefined }; // Convert line buffers to xcl zones then also include them within the next process if (bufs && bufs.length) { for (let i = 0; i < bufs.length; i++) { const polyCoors = bufUtil.lineBuffer(bufs[i].geometry.coordinates, utils.toMeter(bufs[i].properties.width, job.measureUnit)); if (!utils.isEmptyArray(polyCoors)) { xclZones.push(jobUtil.createXclArea({ name: `XCL${i + 1}`, coors: polyCoors })); } } } utmXclZones = await convertAreasCoorstoUTM(xclZones, ZoneType.XCL, refZone); let regenIds = []; /* Generate lines for selected areas and ones with no lines yet. Handle areas or area with same name groups separatedly. * Use in Agmission web client's GUI */ if (options.useGroup) { /* Group areas by ones with the same name to generate lines by each of these groups */ let selByName = {}; // selected area names hash list let restByName = {}; // the rest area names hash list if (!utils.isEmptyArray(sprayIds)) { let selAreas = [], j = sprayZones.length - 1; while (j >= 0) { if (sprayZones[j]._id && sprayIds.includes(sprayZones[j]._id.toHexString())) selAreas.push(sprayZones.splice(j, 1)[0]); j--; } if (selAreas.length) selByName = _.groupBy(selAreas, it => it.properties.name.toLowerCase()); } restByName = _.groupBy(sprayZones, it => it.properties.name.toLowerCase()); let selItems = Object.values(selByName); if (selItems.length) { for (let i = 0; i < selItems.length; i++) { selItems[i] = await convertAreasCoorstoUTM(selItems[i], ZoneType.NORMAL, refZone); } const selSprayZones = [].concat.apply([], selItems); // Scan for intersected xcls only for the next step let selXcls = getOverlapXcls(utmXclZones, geoUtil.getAreasBBox(selSprayZones)); const hdgResult = await getHeading([].concat.apply([], selItems), selXcls, swath, halfSwathOffset, abLine, heading, refZone, (heading === BST_HDG && selSprayZones.length === 1)); genHeading = hdgResult.heading; masterPoint = hdgResult.masterPoint; if (heading === BST_HDG && selSprayZones.length === 1) { linesResult.push({ isNew: true, areaId: selItems[0][0].id, lines: hdgResult.lineList, heading: genHeading, masterPoint: masterPoint }); regenIds.push(selItems[0][0].id); } else { for (let i = 0; i < selItems.length; i++) { sprZones = selItems[i]; const cloneSprayZones = cloneDeep(sprZones.length > 1 ? [].concat.apply([], sprZones) : sprZones, true); genResult = await getLinesUTM(cloneSprayZones, selXcls, swath, halfSwathOffset, masterPoint.x, masterPoint.y, genHeading); const newLine = { isNew: true, areaId: sprZones[0].id, lines: genResult.lineList, heading: genHeading, masterPoint: masterPoint }; const ids = sprZones.map(s => s.id); if (ids.length > 1) newLine["mems"] = ids; linesResult.push(newLine); regenIds = regenIds.concat(ids); } } } const restItems = Object.values(restByName); let checkResult, areaWLines; // Generate lines with best heading for each of for the rest areas if not has lines yet for (let i = 0; i < restItems.length; i++) { sprZones = restItems[i]; if (!options.regenerate) { checkResult = needRegenLines(sprZones); // TODO: revise this function if (checkResult.value == false) { // no lines changed for the single area => reuse areaWLines = checkResult.areaWLines; linesResult.push({ isNew: false, areaId: areaWLines._id.toHexString(), heading: areaWLines.heading, lines: areaWLines.lines, mems: areaWLines.mems }); continue; } } sprZones = await convertAreasCoorstoUTM(restItems[i], ZoneType.NORMAL, refZone); // Scan for intersected xcls only for the next step selXcls = getOverlapXcls(utmXclZones, geoUtil.getAreasBBox(sprZones)); masterPoint = sprZones[0].points[0]; let newLine, rootId = (checkResult && checkResult.areaWLines) ? checkResult.areaWLines._id.toHexString() : sprZones[0].id; if (selItems.length || (!selItems.length && heading === BST_HDG)) { const hdgResult = await getHeading(sprZones, selXcls, swath, halfSwathOffset, null, BST_HDG, refZone, true); newLine = { isNew: true, areaId: rootId, lines: hdgResult.lineList, heading: hdgResult.heading, masterPoint: masterPoint }; } else { const hdgResult = await getHeading(sprZones, selXcls, swath, halfSwathOffset, abLine, heading, refZone); genHeading = hdgResult.heading; let cloneSprayZones = cloneDeep(sprZones, true); genResult = await getLinesUTM(cloneSprayZones, selXcls, swath, halfSwathOffset, masterPoint.x, masterPoint.y, genHeading); newLine = { isNew: true, areaId: rootId, lines: genResult.lineList, heading: genHeading, masterPoint: masterPoint }; } const ids = sprZones.map(s => s.id); if (ids.length > 1) newLine["mems"] = ids; linesResult.push(newLine); regenIds = regenIds.concat(ids); } } else { /* Generate lines for all areas. Use for export/download job only */ utmSprayZones = await convertAreasCoorstoUTM(sprayZones, ZoneType.NORMAL, refZone); // Get the heading for all areas const hdgResult = await getHeading(utmSprayZones, [], swath, halfSwathOffset, abLine, heading, refZone); genHeading = hdgResult.heading; masterPoint = hdgResult.masterPoint; // Generate lines for all areas genResult = await getLinesUTM(cloneDeep(utmSprayZones, true), utmXclZones, swath, halfSwathOffset, masterPoint.x, masterPoint.y, genHeading); linesResult.push({ isNew: true, areaId: utmSprayZones[0].id, lines: genResult.lineList, heading: genHeading, masterPoint: masterPoint }); } // Convert new lines from Utm to Latlong let backUtm = Utm.newInstance(refZone.zone, refZone.hemisphere, 0, 0); let latlngHeading = 0, areaLine; for (let i = 0; i < linesResult.length; i++) { areaLine = linesResult[i]; if (!areaLine.isNew) continue; let xy; for (let j = 0; j < areaLine.lines.length; j++) { for (let k = 0; k < areaLine.lines[j].breakPoints.length; k++) { xy = areaLine.lines[j].breakPoints[k]; backUtm.easting = xy.x; backUtm.northing = xy.y; latlng = backUtm.toLatLon(); areaLine.lines[j].breakPoints[k] = [latlng.lat, latlng.lon]; } areaLine.lines[j] = areaLine.lines[j].breakPoints; delete areaLine.lines[j].breakPoints; } if (areaLine.lines.length) { latlngHeading = utmToLatlonHeading(refZone.zone, refZone.hemisphere, areaLine.masterPoint.x, areaLine.masterPoint.y, areaLine.heading); latlngHeading = geoUtil.to360Range(latlngHeading); areaLine["latlngHeading"] = utils.fixedTo(latlngHeading, 1); } else { areaLine["latlngHeading"] = 0; } backUtm.easting = areaLine.masterPoint.x, backUtm.northing = areaLine.masterPoint.y; latlng = backUtm.toLatLon(); areaLine["masterPoint"] = [latlng.lat, latlng.lon]; // debug(`${areaLine.areaId}, LLHeading: ${areaLine.latlngHeading}°, UTM Heading: ${areaLine.heading}°, Lines: ${areaLine.lines.length}`); } return ({ lines: linesResult, regenIds: regenIds }); } function getOverlapXcls(utmXclZones, wBbox) { const selXcls = []; let zoneBbox; for (let i = 0; i < utmXclZones.length; i++) { zoneBbox = utmXclZones[i].bbox; if (polyUtil.doOverlap({ x: wBbox[0], y: wBbox[3] }, { x: wBbox[2], y: wBbox[1] }, { x: zoneBbox[0], y: zoneBbox[3] }, { x: zoneBbox[2], y: zoneBbox[1] })) selXcls.push(utmXclZones[i]); } return selXcls; } async function convertAreasCoorstoUTM(areas, zoneType, refZone) { let utmZones = []; if (utils.isEmptyArray(areas)) return utmZones; for (let i = 0; i < areas.length; i++) { const coors = areas[i].geometry.coordinates[0]; if (coors.length < 3) continue; const convertRes = latlngToUTMCoors(coors, refZone); if (!convertRes) AppInputError.throw(); if (zoneType == ZoneType.NORMAL) { const hasLines = !utils.isEmptyArray(areas[i].lines); utmZones.push(new Zone(areas[i]._id.toHexString(), areas[i].properties.name, ZoneType.NORMAL, convertRes.utmCoors, hasLines)); } else { utmZones.push(new Zone(areas[i].properties.name, areas[i].properties.name, ZoneType.XCL, convertRes.utmCoors, false)); } } return utmZones; } /** * Check whether the area or group has generated lines or not. * @param {*} sprZones LatLon areas */ function needRegenLines(sprZones) { if (utils.isEmptyArray(sprZones)) return { value: false }; const checkResult = { value: false, areaWLines: sprZones[0] }; if (sprZones.length === 1) { checkResult.value = utils.isEmptyArray(sprZones[0].lines); } else { const rootItems = []; const ids = []; let areaId; for (let i = 0; i < sprZones.length; i++) { areaId = sprZones[i]._id.toHexString(); if (sprZones[i].lines && !utils.isEmptyArray(sprZones[i].mems)) rootItems.push(sprZones[i]); ids.push(areaId); } if (rootItems.length !== 1) { checkResult.value = true; } else if (utils.arraysEqual(ids, rootItems[0].mems, true)) { checkResult.value = false; checkResult.areaWLines = rootItems[0]; } } return checkResult; } /** * Returns the heading for the given zones and parameters. Also return the lines in the case of best heading. * @param {*} sprayZones * @param {*} xclZones * @param {number} swath * @param {boolean} halfSwathOffset * @param {*} abLine * @param {number} heading * @param {*} refZone { zone: [zone number], hemisphere: ['N'|'S'] } * @param {boolean} returnLines */ async function getHeading(sprayZones, xclZones, swath, halfSwathOffset, abLine, heading, refZone = { zone: undefined, hemisphere: undefined }, returnLines = false) { let genHeading = 0, masterPoint, bestHeadingResult, _xclZones = []; if (heading === BST_HDG) { // Find Best Heading let longestLineZoneIndex = 0, longestEdgeIndex = 0; let longestEdgeLength, edgeLength, longestEdgeHeading; let a, b, zone; bestHeadingResult = { heading: 0, leftmostPoint: null, lineList: [] }; _xclZones = cloneDeep(xclZones, 1); // Select the zone that has the longest line longestEdgeLength = -1; for (let i = 0; i < sprayZones.length; i++) { zone = sprayZones[i]; for (let j = 0; j < zone.points.length; j++) { a = zone.points[j]; b = (j === (zone.points.length - 1)) ? zone.points[0] : zone.points[j + 1]; edgeLength = mathUtil.segmentLengthP(a, b); if (edgeLength > longestEdgeLength) { longestEdgeLength = edgeLength; longestLineZoneIndex = i; longestEdgeIndex = j; } } } // Find the heading on the selected zone that produces minimum number of lines zone = sprayZones[longestLineZoneIndex]; a = zone.points[longestEdgeIndex]; b = (longestEdgeIndex === (zone.points.length - 1)) ? zone.points[0] : zone.points[(longestEdgeIndex + 1)]; longestEdgeHeading = geoUtil.utmBearing(a.x, a.y, b.x, b.y); let minNumLines = Number.MAX_SAFE_INTEGER + 1, scanHeading; for (let alpha = 0; alpha <= 180; alpha += BEST_HEADING_INC) { scanHeading = longestEdgeHeading + alpha; if (scanHeading > 360) scanHeading = scanHeading - 360; let cloneUtmSprayZones = cloneDeep(sprayZones, true); let cloneXclZones = !utils.isEmptyArray(_xclZones) ? cloneDeep(_xclZones, true) : []; let result = await getLinesUTM(cloneUtmSprayZones, cloneXclZones, swath, halfSwathOffset, cloneUtmSprayZones[0].points[0].x, cloneUtmSprayZones[0].points[0].y, scanHeading); // console.log("BHeading: ", Math.trunc(scanHeading), "Lines: ", result.lineList.length); if (result.lineList.length > 0 && result.lineList.length < minNumLines) { bestHeadingResult.heading = scanHeading; bestHeadingResult.leftmostPoint = result.leftmostPoint; if (returnLines) bestHeadingResult.lineList = result.lineList; minNumLines = result.lineList.length; } } masterPoint = bestHeadingResult.leftmostPoint; } else { if (abLine) { let utmA, utmB; const aSP = new LatLonSP(abLine.a.lat, abLine.a.lng); const bSP = new LatLonSP(abLine.b.lat, abLine.b.lng); const midSP = aSP.midpointTo(bSP); const llHeading = aSP.finalBearingTo(bSP); // Use the same method in Guia, Guia Pt to have the same Heading (not the direct heading caculation from AB) const a = midSP.destinationPoint(-500, llHeading); const b = midSP.destinationPoint(500, llHeading); utmA = new LatLonUTM(a.lat, a.lng).toUtm(refZone.zone, refZone.hemisphere); utmB = new LatLonUTM(b.lat, b.lng).toUtm(refZone.zone, refZone.hemisphere); heading = geoUtil.utmBearing(utmA.easting, utmA.northing, utmB.easting, utmB.northing); // Requirement: If using AB Line method, the Masterpoint must be the midpoint const midPUtm = new LatLonUTM(midSP.lat, midSP.lon).toUtm(refZone.zone, refZone.hemisphere); masterPoint = { x: midPUtm.easting, y: midPUtm.northing }; } else { masterPoint = sprayZones[0].points[0]; } } genHeading = utils.fixedTo((bestHeadingResult ? bestHeadingResult.heading : heading), 1); return { heading: genHeading, masterPoint: masterPoint, lineList: bestHeadingResult ? bestHeadingResult.lineList : [] }; } /** * Generate grid lines given zones in UTM coordinates. * @param {*} sprayZones * @param {*} xclZones * @param {*} halfSwathOffset whether to offset guide lines half swath inside area * @param {*} x UTM coordinate of the master point x * @param {*} y UTM coordinate of the master point y * @param {*} heading heading of the guide lines */ async function getLinesUTM(sprayZones, xclZones, swath, halfSwathOffset, x, y, heading) { let result = { leftmostPoint: null, lineList: [] }; let _xclZones = cloneDeep(xclZones, 1); /* Double check to ensure the MasterPoint is valid * if it is invalid, correct it as the 1st corner of the first sprayzone */ let lineList = []; let zoneList = utils.appendArray(sprayZones, _xclZones); let bounds = new Bound(); for (let i = 0; i < zoneList.length; i++) bounds.updateR(zoneList[i].bounds); if (!bounds.contains(x, y) && mathUtil.segmentLength(bounds.center().x, bounds.center().y, x, y) > (1000 * halfSwathOffset)) { x = sprayZones[0].points[0].x; y = sprayZones[0].points[0].y; } // console.log(bounds.center()); // Check minimum swath width swath = Math.max(MIN_SWATH_WIDTH, swath); const transform1 = halfSwathOffset ? transform( translate(-swath / 2, 0), rotateDEG(heading), translate(-x, -y), ) : transform( rotateDEG(heading), translate(-x, -y), ); const transform2 = inverse(transform1); // Transform all zones to make use in next steps for (let i = 0; i < zoneList.length; i++) { zoneList[i].points = applyToPoints(transform1, zoneList[i].points); zoneList[i].updateBounds(); } // *** All steps below assume the polygons are already transformed *** // find the leftmost point let leftmostPoint; for (let i = 0; i < sprayZones.length; i++) { let zone = sprayZones[i]; for (let j = 0; j < zone.points.length; j++) { if (!leftmostPoint) leftmostPoint = zone.points[j]; else { if (zone.points[j].x < leftmostPoint.x) leftmostPoint = zone.points[j]; } } } // compute jump points let linesWithJumpPoints = findJumpPoints(halfSwathOffset, swath, zoneList); if (linesWithJumpPoints.length > 0) { // compute break points let line, ok = false, m; for (let i = 0; i < linesWithJumpPoints.length; i++) { line = findBreakPoints(linesWithJumpPoints[i]); ok = (line.breakPoints.length && line.breakPoints.length % 2 === 0); // if (line.breakPoints.length === 2) // Single segment: only take one with length >= MIN_SEGMENT_LENGTH // ok = (mathUtil.segmentLengthP(line.breakPoints[0], line.breakPoints[1]) >= MIN_SEGMENT_LENGTH); if (ok) { m = line.breakPoints.length - 1; while (m >= 1) { // remove any too short segment if (mathUtil.segmentLengthP(line.breakPoints[m - 1], line.breakPoints[m]) < MIN_SEGMENT_LENGTH) { line.breakPoints.splice(m - 1, 2); } m -= 2; } // Transform break points back to original coordinate system if (line.breakPoints.length) { line.breakPoints = applyToPoints(transform2, line.breakPoints); lineList.push(line); } } } } if (leftmostPoint) result.leftmostPoint = applyToPoint(transform2, leftmostPoint); result.lineList = lineList; return result; } /** * Compute the jump points used to find break points forming grid lines * @param {*} swath the swathwidth between scan lines * @param {*} zoneList list of zone polygons to find jump points * @note The polygons must be transformed North-up and translated to the origin. */ function findJumpPoints(halfSwathOffset, swath, zoneList) { let nlines, zone; let lineList = [], scanLines = []; let wholeRect = new Bound(); for (let i = 0; i < zoneList.length; i++) { // Scan within sprayzones bounding ensure correct first line position in the case with xcls overlapped from out to inside from the scanning side if (zoneList[i].type === ZoneType.NORMAL) wholeRect.updateR(zoneList[i].bounds); } let xMin = wholeRect.xmin, xMax = wholeRect.xmax; let curX = xMin; curX += halfSwathOffset ? swath / 2 : 0; scanLines.push({ x: curX, y: 0 }); do { curX += swath; scanLines.push({ x: curX, y: 0 }); } while (curX <= xMax - swath); // Now in scanLines are the x coordinates of the vertical scan lines, // calculate the intersection points of the above lines with zone polygons. nlines = scanLines.length; for (let i = 0; i < nlines; i++) { let p1 = { x: 0, y: 0 }, p2 = { x: 0, y: 0 }; let line = { intersectSprayZones: [], intersectXclZones: [], breakPoints: [] }; for (let j = 0; j < zoneList.length; j++) { zone = zoneList[j]; if (zone.points.length < 3) continue; let hasIntersectPoint = false; xMin = zone.bounds.xmin; xMax = zone.bounds.xmax; // TODO: May use Active Edge and Edge Table for further performance improvement with large and multiple areas let v = { x: scanLines[i].x, y: 0 }; if (xMin <= v.x && v.x <= xMax) { for (let m = 0, n = 1; m < zone.points.length; m++, n++) { if (n === zone.points.length) n = 0; p1 = zone.points[m]; p2 = zone.points[n]; if (Math.abs(p1.x - p2.x) < 1e-4) { // vertical edge => skip because no intersect with vertical scan lines } else { // if oblic edge if ((p1.x - v.x) * (p2.x - v.x) < 1e-4) { // if cross the edge v.y = p1.y + (p2.y - p1.y) * (v.x - p1.x) / (p2.x - p1.x); const newPoint = { x: v.x, y: v.y, type: zone.type === ZoneType.NORMAL ? 0 : 1 }; line.breakPoints.push(newPoint); hasIntersectPoint = true; } } } } if (hasIntersectPoint) { if (zone.type === ZoneType.NORMAL) line.intersectSprayZones.push(zone); else line.intersectXclZones.push(zone); } } if (line.breakPoints.length) lineList.push(line); } // For each intersect point, we generate 2 jump points 0.1m up and down from the intersect point. // The jump points will be used to determine break points later. for (let i = 0; i < lineList.length; i++) { let jumpPoints = []; for (let j = 0; j < lineList[i].breakPoints.length; j++) { let v = lineList[i].breakPoints[j]; let vDown = { x: v.x, y: v.y - 0.1, type: v.type }; jumpPoints.push(vDown); let vUp = { x: v.x, y: v.y + 0.1, type: v.type }; jumpPoints.push(vUp); } // Sort (use Bubble Sort) the jump points by y (ascending) let n = jumpPoints.length; mathUtil.bubbleSort(jumpPoints, n); if (jumpPoints.length > 1) { lineList[i].breakPoints = jumpPoints; } } return lineList; } /** * Find the break points of a line based on jump points * @param {*} line list if lines with jump points * @return a line containing calculated break points */ function findBreakPoints(line) { let breakPoints = []; let line2 = { breakPoints: [], }; let findInside = true; // start with finding the first entry point for (let i = 0; i < line.breakPoints.length; i++) { const jumpPoint = line.breakPoints[i]; let insideArea = checkPointInZone(jumpPoint.x, jumpPoint.y, ZoneType.NORMAL, line.intersectSprayZones); if (insideArea) { if (line.intersectXclZones.length > 0 && checkPointInZone(jumpPoint.x, jumpPoint.y, ZoneType.XCL, line.intersectXclZones)) insideArea = false; } if (insideArea === findInside) { // found a break point breakPoints.push(jumpPoint); findInside = !findInside; // if an entry point found, the next break point must be an exit point and vice versa } } // Number of break points must be even. // If it is odd, the last point should be removed. if ((breakPoints.length > 0) && (breakPoints.length % 2)) breakPoints.splice(-1, 1); //remove last item line2.breakPoints = breakPoints; // Single segment: only take one with length >= MIN_SEGMENT_LENGTH if (line.breakPoints.length === 2) { if (mathUtil.segmentLengthP(line.breakPoints[0], line.breakPoints[1]) < MIN_SEGMENT_LENGTH) line2.breakPoints = []; } return line2; } /** * Check if the given point is inside a zone of current area * @param {*} zoneType type of zone (normal or exclusion) * @param {*} zoneList list of zones to check * @return true if inside zone */ function checkPointInZone(x, y, zoneType, zoneList) { for (let i = 0; i < zoneList.length; i++) { let zone = zoneList[i]; if (zone && (zone.type === zoneType)) { if (geoUtil.pointInOrOnPolygon(zone.points, x, y)) return true; } } return false; } /* * Convert Lat/Lon heading to UTM heading * @param {*} lat * @param {*} lon * @param {*} ll_heading Lat/Lon heading to convert function latlonToUtmHeading (lat, lon, ll_heading) { let utm_heading = ll_heading; let dradius = 0.01; // degree ~= 1000 meters let utm_x1, utm_y1, utm_x2, utm_y2; const head = ll_heading * deg2rad; const lon1 = lon - Math.sin(head) * dradius; const lat1 = lat - Math.cos(head) * dradius; const lon2 = lon + Math.sin(head) * dradius; const lat2 = lat + Math.cos(head) * dradius; let latlng = new LatLon(lat1, lon1), utm; utm = latlng.toUtm(); utm_x1 = utm.easting, utm_y1 = utm.northing; latlng.lat = lat2, latlng.lon = lon2; utm = latlng.toUtm(); utm_x2 = utm.easting, utm_y2 = utm.northing; utm_heading = Math.atan2(utm_x2 - utm_x1, utm_y2 - utm_y1) * rad2deg; return utm_heading; } */ /** * Convert UTM heading to Lat/Lon heading * @param {*} zone UTM zone number * @param {*} hemisphere 1: North hemisphere, 2: South hemisphere * @param {*} utm_x * @param {*} utm_y * @param {*} utm_heading UTM heading to convert */ function utmToLatlonHeading(zone, hemisphere, utm_x, utm_y, utm_heading) { let ll_heading = utm_heading; let lon1, lat1, lon2, lat2, utm_x1, utm_y1, utm_x2, utm_y2, head; let dradius = 500; // 500 meters head = utm_heading * deg2rad; utm_x1 = utm_x - Math.sin(head) * dradius; utm_y1 = utm_y - Math.cos(head) * dradius; utm_x2 = utm_x + Math.sin(head) * dradius; utm_y2 = utm_y + Math.cos(head) * dradius; let utm = Utm.newInstance(zone, hemisphere, 0, 0); utm.easting = utm_x1, utm.northing = utm_y1; let latlng = utm.toLatLon(); lat1 = latlng.lat, lon1 = latlng.lon; utm.easting = utm_x2, utm.northing = utm_y2; latlng = utm.toLatLon(); lat2 = latlng.lat, lon2 = latlng.lon; ll_heading = Math.atan2(lon2 - lon1, lat2 - lat1) * rad2deg; return ll_heading; } module.exports = { getLinesLatLng, BST_HDG }