agmission/Development/server/helpers/poly_util.js

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: 7, 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: 7, 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
}