413 lines
11 KiB
JavaScript
413 lines
11 KiB
JavaScript
'use strict';
|
|
|
|
const
|
|
debug = require('debug')('agm:poly_util'),
|
|
turf = require('@turf/turf'),
|
|
utils = require('./utils');
|
|
|
|
function geoPolygon(lnlats) {
|
|
try {
|
|
const _lnlats = lnlats[0].length > 1 ? lnlats[0].slice() : lnlats.slice();
|
|
if (_lnlats.length > 2 && _lnlats[0] !== _lnlats[_lnlats.length - 1]) {
|
|
_lnlats.push(_lnlats[0]);
|
|
}
|
|
return turf.polygon([_lnlats]);
|
|
} catch (error) {
|
|
debug(error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function createPoly(coors) {
|
|
if (!coors || coors.length < 3)
|
|
return null;
|
|
|
|
if (coors.length === 3)
|
|
coors.push(coors[coors.length - 1]);
|
|
|
|
// Add the the first to the end of the coors if it is not there
|
|
if (coors[0][0] !== coors[coors.length - 1][0] || coors[0][1] !== coors[coors.length - 1][1])
|
|
coors.push(coors[0]);
|
|
|
|
try {
|
|
return turf.polygon([coors]);
|
|
} catch (error) {
|
|
debug(error, "Could not create a geojson polygon for coors:", coors);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Chech line in polygon, if either first or middle or last point in the polygon
|
|
function lineInPolygon(lineLnLats, polygonLnlats) {
|
|
if ((utils.isEmptyArray(lineLnLats) || lineLnLats.length <= 1) || utils.isEmptyArray(polygonLnlats))
|
|
return false;
|
|
|
|
const polygon = geoPolygon(polygonLnlats);
|
|
if (!polygon)
|
|
return false;
|
|
|
|
if (lineLnLats.length === 2) {
|
|
try {
|
|
return turf.booleanPointInPolygon(lineLnLats[0], polygon) || turf.booleanPointInPolygon(lineLnLats[1], polygon);
|
|
} catch (error) {
|
|
debug(error);
|
|
return false;
|
|
}
|
|
}
|
|
else {
|
|
try {
|
|
return turf.booleanPointInPolygon(lineLnLats[0], polygon)
|
|
|| turf.booleanPointInPolygon(lineLnLats[Math.trunc((lineLnLats.length - 1) / 2)], polygon)
|
|
|| turf.booleanPointInPolygon(lineLnLats[lineLnLats.length - 1], polygon);
|
|
} catch (error) {
|
|
debug(error);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Determine whether a point within any of the polygon
|
|
* @param {*} polys GeoJSON polygon with lat/lon coors
|
|
* @returns {boolean} true or false
|
|
*/
|
|
function isPointinPolys(lat, lon, polys, ignoreBoundary = true) {
|
|
if (!utils.isNumber(lat) || !utils.isNumber(lon) || utils.isEmptyArray(polys)) return false;
|
|
|
|
for (let poly of polys) {
|
|
if (turf.booleanPointInPolygon([lon, lat], poly, { ignoreBoundary: ignoreBoundary })) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function polyIntersectOrIn(poly1Coors, poly2Coors, orIn = false) {
|
|
const poly1 = geoPolygon(poly1Coors);
|
|
const poly2 = geoPolygon(poly2Coors);
|
|
if (!poly1 || !poly2)
|
|
return false;
|
|
else
|
|
try {
|
|
let t = turf.intersect(poly1, poly2);
|
|
if (!t && orIn)
|
|
t = turf.booleanContains(poly1, poly2);
|
|
return t;
|
|
} catch (error) {
|
|
debug(error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function polygonWithin(poly1Coors, poly2Coors) {
|
|
const poly1 = geoPolygon(poly1Coors);
|
|
const poly2 = geoPolygon(poly2Coors);
|
|
if (!poly1 || !poly2)
|
|
return false;
|
|
else
|
|
return turf.booleanWithin(poly1, poly2);
|
|
}
|
|
|
|
function removeSameLatLns(coors) {
|
|
for (let i = 0; coors.length > 2 && i < coors.length - 1; i++) {
|
|
for (let j = i + 1; j < coors.length;) {
|
|
if (isSameLatLn(coors[i], coors[j]) && j != coors.length - 1) {
|
|
coors.splice(j, 1);
|
|
}
|
|
else j++;
|
|
}
|
|
}
|
|
return coors;
|
|
}
|
|
|
|
function isSameLatLn(p1, p2, offset = 1e-6) {
|
|
return (Math.abs(p1[0] - p2[0]) < offset && Math.abs(p1[1] - p2[1]) < offset);
|
|
}
|
|
|
|
function truncatePoly(coors) {
|
|
const processPoly = createPoly(coors);
|
|
if (!processPoly) return null;
|
|
|
|
try {
|
|
turf.truncate(processPoly, { precision: 6, mutate: true });
|
|
} catch (error) {
|
|
debug(error);
|
|
}
|
|
|
|
try {
|
|
removeSameLatLns(processPoly.geometry.coordinates[0]);
|
|
} catch (error) {
|
|
debug(error);
|
|
}
|
|
|
|
if (processPoly.geometry.coordinates[0].length <= 3)
|
|
return null;
|
|
|
|
return processPoly.geometry.coordinates[0];
|
|
}
|
|
|
|
function truncatePoint(coors) {
|
|
const point = turf.point(coors);
|
|
try {
|
|
turf.truncate(point, { precision: 6, mutate: true });
|
|
} catch (error) {
|
|
debug(error);
|
|
return null;
|
|
}
|
|
return point.geometry.coordinates;
|
|
}
|
|
|
|
/**
|
|
* Create a new GeoJson Area feature
|
|
* @param { number } type 0 : Spray area, 1: XCL area
|
|
* @param { [number][number] } coors array of corners [lat, lon]
|
|
* @param { string } name area name (be normalized)
|
|
* @param { string } color color name for the boundary
|
|
* @param { number } appRate application rate value
|
|
* @param { object } crop the crop object
|
|
*/
|
|
function createArea(type, coors, name, color, appRate = 0, crop = null) {
|
|
const isCW = isClockwise(coors);
|
|
if ((type === 0 && isCW === false) || (type === 1 && isCW === true))
|
|
coors = coors.reverse();
|
|
|
|
const area = {
|
|
properties: {
|
|
name: utils.normalizeName(name),
|
|
type: type,
|
|
appRate: (type === 0 && appRate) ? appRate : 0,
|
|
color: color ? color : type === 0 ? 'blue' : 'red'
|
|
},
|
|
geometry: {
|
|
type: 'Polygon',
|
|
coordinates: [coors]
|
|
}
|
|
};
|
|
if (crop) {
|
|
if (crop.color)
|
|
area.properties.color = crop.color;
|
|
if (crop._id)
|
|
area.properties.crop = crop._id;
|
|
}
|
|
return area;
|
|
}
|
|
|
|
/**
|
|
* Create normalized areas. Input as one area might be splitted into multiple ones if it is kinked.
|
|
* @param {*} type area type. 0: Spray, 1: Xcl
|
|
* @param {*} coors coordinate list
|
|
* @param {*} name area name
|
|
* @param {*} color prefered color string
|
|
* @param {*} appRate application rate
|
|
* @param {*} crop crop entity instance
|
|
* @returns list of areas
|
|
*/
|
|
function createAreas(type, coors, name, color = null, appRate = 0, crop = null) {
|
|
const areas = [];
|
|
const _coors = truncatePoly(coors);
|
|
const poly = createPoly(_coors);
|
|
|
|
if (!poly) return areas;
|
|
|
|
try {
|
|
if (!utils.isEmptyArray(turf.kinks(poly).features)) {
|
|
let unKinks = turf.unkinkPolygon(poly);
|
|
for (let j = 0; j < unKinks.features.length; j++) {
|
|
if (turf.area(unKinks.features[j]) >= 200) { // skip too small area, less than 200 square meters
|
|
const newItem = createArea(type, truncatePoly(unKinks.features[j].geometry.coordinates[0]), name, color, appRate, crop);
|
|
areas.push(newItem);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
areas.push(createArea(type, _coors, name, color, appRate, crop));
|
|
} catch (error) {
|
|
debug(error);
|
|
return areas;
|
|
}
|
|
|
|
return areas;
|
|
}
|
|
|
|
function createGeoPoint(type, coors, name) {
|
|
coors = truncatePoint(coors);
|
|
if (!coors) return null;
|
|
const wpt = {
|
|
properties: {
|
|
name: utils.normalizeName(name),
|
|
type: type, // 2; Waypoint, 4: PlaceMark
|
|
},
|
|
geometry: {
|
|
type: 'Point',
|
|
coordinates: coors
|
|
}
|
|
};
|
|
return wpt;
|
|
}
|
|
|
|
function createGeoBuffer(coors, name, width) {
|
|
const line = turf.lineString(coors);
|
|
try {
|
|
const truncLine = turf.truncate(line);
|
|
turf.cleanCoords(line, { mutate: true });
|
|
coors = truncLine.geometry.coordinates;
|
|
} catch (error) {
|
|
debug(error);
|
|
return null;
|
|
}
|
|
const buf = {
|
|
properties: {
|
|
name: utils.normalizeName(name),
|
|
type: 3, // 3: Buffer LineString
|
|
color: 'yellow',
|
|
width: width
|
|
},
|
|
geometry: {
|
|
type: "LineString",
|
|
coordinates: coors
|
|
}
|
|
};
|
|
return buf;
|
|
}
|
|
|
|
/**
|
|
* Return a processed exlusion items (if any)
|
|
* Rules: xcl item will have the same name with its first intersected spray one.
|
|
* @param {*} sprayAreas SprayArea object list
|
|
* @param {*} xclAreas List of xcl poly items
|
|
* @returns List of XCL area objects (GeoJSON)
|
|
*/
|
|
function processXclsName(sprayAreas, xclAreas) {
|
|
if (utils.isEmptyArray(xclAreas)) return [];
|
|
|
|
const processedXCls = [];
|
|
const _xclItems = xclAreas.slice(0);
|
|
let sprArea, xclArea, removedItems;
|
|
|
|
// Process spray items with its related (first intersected) xcl items has the same name
|
|
for (let i = 0; i < sprayAreas.length; i++) {
|
|
sprArea = sprayAreas[i];
|
|
for (let j = _xclItems.length - 1; j >= 0; j--) {
|
|
xclArea = _xclItems[j];
|
|
if (polyIntersectOrIn(sprArea.geometry.coordinates, xclArea.geometry.coordinates, true)) {
|
|
removedItems = _xclItems.splice(j, 1);
|
|
if (removedItems.length) {
|
|
xclArea.properties.name = sprArea.properties.name;
|
|
processedXCls.push(xclArea);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Process the rest of non-intersected xcls
|
|
if (!utils.isEmptyArray(_xclItems)) {
|
|
let nameIdx = 1;
|
|
for (const xcl of _xclItems) {
|
|
xcl.properties.name = `XCL_${nameIdx}`;
|
|
processedXCls.push(xcl);
|
|
nameIdx++;
|
|
}
|
|
}
|
|
return processedXCls;
|
|
}
|
|
|
|
// const unKinkGeoPolys = function (geoItems) {
|
|
// if (utils.isEmptyArray(geoItems)) return [];
|
|
// let item, items = [], unKinks, poly;
|
|
// for (let i = 0; i < geoItems.length; i++) {
|
|
// item = geoItems[i];
|
|
// poly = createPoly(item.geometry.coordinates[0]);
|
|
// if (!poly) continue;
|
|
|
|
// if (!utils.isEmptyArray(turf.kinks(poly).features)) {
|
|
// unKinks = turf.unkinkPolygon(poly);
|
|
// for (let j = 0; j < unKinks.features.length; j++) {
|
|
// if (turf.area(unKinks.features[j]) >= 200) { // skip too small area, less than 200 square meters
|
|
// const newItem = createArea(item.properties.type, unKinks.features[j].geometry.coordinates[0], item.properties.name);
|
|
// items.push(newItem);
|
|
// }
|
|
// }
|
|
// }
|
|
// else
|
|
// items.push(item);
|
|
// }
|
|
// return items;
|
|
// }
|
|
|
|
/**
|
|
* @brief Check whether a set of poly coordinates is clockwise or counterclockwise
|
|
* @param {*} coors coordinate arrays i.e.: [[0,0],[1,1],[1,0],[0,0]] => true, [[0,0],[1,0],[1,1],[0,0]] => false
|
|
*/
|
|
function isClockwise(coors) {
|
|
if (!Array.isArray(coors)) //throw new Error('Coordinates not valid');
|
|
return false;
|
|
|
|
let sum = 0, i = 1, prev, cur;
|
|
while (i < coors.length) {
|
|
prev = cur || coors[0];
|
|
cur = coors[i];
|
|
sum += ((cur[0] - prev[0]) * (cur[1] + prev[1]));
|
|
i++;
|
|
}
|
|
return sum > 0;
|
|
}
|
|
|
|
function toGeoItems(features) {
|
|
let feature, items = [];
|
|
|
|
if (!utils.isEmptyArray(features)) {
|
|
for (let i = 0; i < features.length; i++) {
|
|
feature = features[i];
|
|
|
|
const item = {
|
|
_id: feature._id,
|
|
properties: feature.properties ? {
|
|
name: feature.properties.name,
|
|
type: feature.properties.type,
|
|
color: feature.properties.color,
|
|
crop: feature.properties.crop
|
|
} : null,
|
|
geometry: feature.geometry
|
|
};
|
|
// if (withType) item['type'] = 'Feature';
|
|
if (feature.client) item['client'] = feature.client;
|
|
|
|
items.push(item);
|
|
}
|
|
}
|
|
return items;
|
|
}
|
|
|
|
/**
|
|
* Returns true if two rectangles (l1, r1) and (l2, r2) overlap
|
|
* @param {*} l1 retangle 1's top left point
|
|
* @param {*} r1 retangle 1's bottom right point
|
|
* @param {*} l2 retangle 2's top left point
|
|
* @param {*} r2 retangle 2's bottom right point
|
|
*/
|
|
function doOverlap(l1, r1, l2, r2) {
|
|
|
|
// To check if either rectangle is actually a line
|
|
// For example : l1 ={-1,0} r1={1,1} l2={0,-1} r2={0,1}
|
|
if (l1.x == r1.x || l1.y == r1.y ||
|
|
l2.x == r2.x || l2.y == r2.y) {
|
|
// the line cannot have positive overlap
|
|
return false;
|
|
}
|
|
|
|
// If one rectangle is on left side of other
|
|
if (l1.x >= r2.x || l2.x >= r1.x) {
|
|
return false;
|
|
}
|
|
|
|
// If one rectangle is above other
|
|
if (l1.y <= r2.y || l2.y <= r1.y) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
module.exports = {
|
|
createPoly, geoPolygon, createArea, createAreas, createGeoPoint, createGeoBuffer, lineInPolygon, polygonIntersect: polyIntersectOrIn, polygonWithin, removeSameLatLns, truncatePoly, processXclsName, truncatePoint, isClockwise, toGeoItems, doOverlap, isPointinPolys
|
|
}
|