agmission/Development/server/helpers/gridline_util.js

740 lines
27 KiB
JavaScript

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
}