'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 }