boundary entries updates
This commit is contained in:
@@ -1124,3 +1124,72 @@ router.put('/settings', async (req, res, next) => {
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
// ----- Properties management -----
|
||||
// List properties (with user info)
|
||||
router.get('/properties', async (req, res, next) => {
|
||||
try {
|
||||
const { search, user_id } = req.query;
|
||||
let where = [];
|
||||
let params = [];
|
||||
let n = 0;
|
||||
if (user_id) { n++; where.push(`p.user_id=$${n}`); params.push(user_id); }
|
||||
if (search) { n++; where.push(`(p.name ILIKE $${n} OR p.address ILIKE $${n} OR u.email ILIKE $${n})`); params.push(`%${search}%`); }
|
||||
const whereClause = where.length ? `WHERE ${where.join(' AND ')}` : '';
|
||||
const rs = await pool.query(`
|
||||
SELECT p.*, u.email, u.first_name, u.last_name,
|
||||
COUNT(ls.id) as section_count, COALESCE(SUM(ls.area),0) as calculated_area
|
||||
FROM properties p
|
||||
JOIN users u ON p.user_id=u.id
|
||||
LEFT JOIN lawn_sections ls ON ls.property_id=p.id
|
||||
${whereClause}
|
||||
GROUP BY p.id, u.email, u.first_name, u.last_name
|
||||
ORDER BY p.created_at DESC
|
||||
`, params);
|
||||
res.json({ success: true, data: { properties: rs.rows.map(r=>({
|
||||
id: r.id, userId: r.user_id, userEmail: r.email, userName: `${r.first_name} ${r.last_name}`,
|
||||
name: r.name, address: r.address, latitude: parseFloat(r.latitude), longitude: parseFloat(r.longitude),
|
||||
totalArea: parseFloat(r.total_area), calculatedArea: parseFloat(r.calculated_area), sectionCount: parseInt(r.section_count),
|
||||
createdAt: r.created_at, updatedAt: r.updated_at
|
||||
})) }});
|
||||
} catch (e) { next(e); }
|
||||
});
|
||||
|
||||
// Get property with sections (admin override)
|
||||
router.get('/properties/:id', validateParams(idParamSchema), async (req, res, next) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const pr = await pool.query(`SELECT p.*, u.email FROM properties p JOIN users u ON p.user_id=u.id WHERE p.id=$1`, [id]);
|
||||
if (pr.rows.length===0) throw new AppError('Property not found', 404);
|
||||
const srs = await pool.query(`SELECT * FROM lawn_sections WHERE property_id=$1 ORDER BY name`, [id]);
|
||||
const p = pr.rows[0];
|
||||
res.json({ success:true, data:{ property: {
|
||||
id: p.id, userId: p.user_id, userEmail: p.email, name: p.name, address: p.address,
|
||||
latitude: parseFloat(p.latitude), longitude: parseFloat(p.longitude), totalArea: parseFloat(p.total_area),
|
||||
createdAt: p.created_at, updatedAt: p.updated_at,
|
||||
sections: srs.rows.map(s=>({ id:s.id, name:s.name, area: parseFloat(s.area), polygonData: s.polygon_data, grassType: s.grass_type, grassTypes: s.grass_types, captureMethod: s.capture_method, captureMeta: s.capture_meta, createdAt:s.created_at, updatedAt:s.updated_at }))
|
||||
}}});
|
||||
} catch (e) { next(e); }
|
||||
});
|
||||
|
||||
// Update a lawn section (admin override)
|
||||
router.put('/properties/:propertyId/sections/:sectionId', validateParams(idParamSchema), async (req, res, next) => {
|
||||
try {
|
||||
const { propertyId, sectionId } = req.params;
|
||||
const { name, area, polygonData, grassType, grassTypes, captureMethod, captureMeta } = req.body;
|
||||
const own = await pool.query('SELECT id FROM properties WHERE id=$1', [propertyId]);
|
||||
if (own.rows.length===0) throw new AppError('Property not found', 404);
|
||||
// optional sanitize lite
|
||||
let poly = polygonData;
|
||||
try {
|
||||
const coords = polygonData?.coordinates?.[0] || [];
|
||||
let filtered = coords.filter(Boolean);
|
||||
if (filtered.length>=3) poly = { ...polygonData, coordinates: [filtered] };
|
||||
} catch {}
|
||||
const rs = await pool.query(`UPDATE lawn_sections SET name=$1, area=$2, polygon_data=$3, grass_type=$4, grass_types=$5, capture_method=$6, capture_meta=$7, updated_at=CURRENT_TIMESTAMP WHERE id=$8 AND property_id=$9 RETURNING *`, [
|
||||
name, area, JSON.stringify(poly), grassType || (Array.isArray(grassTypes)? grassTypes.join(', '): null), grassTypes? JSON.stringify(grassTypes): null, captureMethod||null, captureMeta? JSON.stringify(captureMeta): null, sectionId, propertyId
|
||||
]);
|
||||
if (rs.rows.length===0) throw new AppError('Section not found', 404);
|
||||
const s=rs.rows[0];
|
||||
res.json({ success:true, data:{ section:{ id:s.id, name:s.name, area: parseFloat(s.area), polygonData: s.polygon_data, grassType: s.grass_type, grassTypes: s.grass_types, captureMethod: s.capture_method, createdAt:s.created_at, updatedAt:s.updated_at }}});
|
||||
} catch (e) { next(e); }
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ const pool = require('../config/database');
|
||||
const { validateRequest, validateParams } = require('../utils/validation');
|
||||
const { propertySchema, lawnSectionSchema, idParamSchema } = require('../utils/validation');
|
||||
const { AppError } = require('../middleware/errorHandler');
|
||||
// Geometry helpers for sanitization and simplification are defined below
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -50,6 +51,65 @@ const calculatePolygonArea = (coordinates) => {
|
||||
return areaInSquareFeet;
|
||||
};
|
||||
|
||||
// ---- Geometry sanitization & simplification helpers ----
|
||||
const toMeters = (lat1, lng1, lat2, lng2) => {
|
||||
const toRad = (d)=> (d*Math.PI)/180; const R=6371000;
|
||||
const dLat=toRad(lat2-lat1), dLng=toRad(lng2-lng1);
|
||||
const a=Math.sin(dLat/2)**2 + Math.cos(toRad(lat1))*Math.cos(toRad(lat2))*Math.sin(dLng/2)**2;
|
||||
return 2*R*Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
|
||||
};
|
||||
|
||||
const sanitizePolygon = (coords) => {
|
||||
const MIN_MOVE_M = 0.5;
|
||||
const COLINEAR_EPS_DEG = 3;
|
||||
if (!Array.isArray(coords) || coords.length < 3) return coords || [];
|
||||
let filtered = [coords[0]];
|
||||
for (let i=1;i<coords.length;i++){
|
||||
const [pl,pg]=filtered[filtered.length-1];
|
||||
const [cl,cg]=coords[i];
|
||||
if (toMeters(pl,pg,cl,cg) >= MIN_MOVE_M) filtered.push(coords[i]);
|
||||
}
|
||||
const angleDeg = (a,b,c) => {
|
||||
const v1=[b[0]-a[0], b[1]-a[1]]; const v2=[c[0]-b[0], c[1]-b[1]];
|
||||
const dot=v1[0]*v2[0]+v1[1]*v2[1]; const m1=Math.hypot(v1[0],v1[1]); const m2=Math.hypot(v2[0],v2[1]);
|
||||
if (m1===0||m2===0) return 180;
|
||||
const cos= Math.min(1, Math.max(-1, dot/(m1*m2)));
|
||||
return Math.acos(cos)*180/Math.PI;
|
||||
};
|
||||
if (filtered.length > 3) {
|
||||
const out=[filtered[0]];
|
||||
for (let i=1;i<filtered.length-1;i++){
|
||||
const a=filtered[i-1], b=filtered[i], c=filtered[i+1];
|
||||
if (angleDeg(a,b,c) >= COLINEAR_EPS_DEG) out.push(b);
|
||||
}
|
||||
out.push(filtered[filtered.length-1]);
|
||||
filtered = out;
|
||||
}
|
||||
// RDP simplification with small epsilon (~2m in lat/long degrees)
|
||||
const EPS_DEG = 0.00002;
|
||||
const perpDist = (p, a, b) => {
|
||||
const [x0,y0]=p, [x1,y1]=a, [x2,y2]=b;
|
||||
const num = Math.abs((y2-y1)*x0 - (x2-x1)*y0 + x2*y1 - y2*x1);
|
||||
const den = Math.hypot(y2-y1, x2-x1) || 1e-9;
|
||||
return num/den;
|
||||
};
|
||||
const rdp = (pts, eps) => {
|
||||
if (pts.length < 3) return pts;
|
||||
let dmax=0, idx=0;
|
||||
for (let i=1;i<pts.length-1;i++){
|
||||
const d = perpDist(pts[i], pts[0], pts[pts.length-1]);
|
||||
if (d>dmax){ dmax=d; idx=i; }
|
||||
}
|
||||
if (dmax>eps){
|
||||
const res1 = rdp(pts.slice(0, idx+1), eps);
|
||||
const res2 = rdp(pts.slice(idx), eps);
|
||||
return res1.slice(0, -1).concat(res2);
|
||||
} else { return [pts[0], pts[pts.length-1]]; }
|
||||
};
|
||||
try { filtered = rdp(filtered, EPS_DEG); } catch {}
|
||||
return filtered;
|
||||
};
|
||||
|
||||
// @route GET /api/properties
|
||||
// @desc Get all properties for current user
|
||||
// @access Private
|
||||
@@ -278,7 +338,7 @@ router.delete('/:id', validateParams(idParamSchema), async (req, res, next) => {
|
||||
router.post('/:id/sections', validateParams(idParamSchema), validateRequest(lawnSectionSchema), async (req, res, next) => {
|
||||
try {
|
||||
const propertyId = req.params.id;
|
||||
const { name, area, polygonData, grassType, grassTypes, soilType } = req.body;
|
||||
const { name, area, polygonData, grassType, grassTypes, soilType, captureMethod, captureMeta } = req.body;
|
||||
|
||||
// Check if property exists and belongs to user
|
||||
const propertyResult = await pool.query(
|
||||
@@ -299,18 +359,27 @@ router.post('/:id/sections', validateParams(idParamSchema), validateRequest(lawn
|
||||
console.log('Calculated area:', calculatedArea);
|
||||
}
|
||||
|
||||
// Sanitize polygon before storing
|
||||
let poly = polygonData;
|
||||
try {
|
||||
const cleaned = sanitizePolygon(polygonData?.coordinates?.[0] || []);
|
||||
poly = { ...polygonData, coordinates: [cleaned] };
|
||||
} catch {}
|
||||
|
||||
const result = await pool.query(
|
||||
`INSERT INTO lawn_sections (property_id, name, area, polygon_data, grass_type, grass_types, soil_type)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
`INSERT INTO lawn_sections (property_id, name, area, polygon_data, grass_type, grass_types, soil_type, capture_method, capture_meta)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||
RETURNING *`,
|
||||
[
|
||||
propertyId,
|
||||
name,
|
||||
calculatedArea,
|
||||
JSON.stringify(polygonData),
|
||||
JSON.stringify(poly),
|
||||
grassType || (Array.isArray(grassTypes) ? grassTypes.join(', ') : null),
|
||||
grassTypes ? JSON.stringify(grassTypes) : null,
|
||||
soilType
|
||||
soilType,
|
||||
captureMethod || null,
|
||||
captureMeta ? JSON.stringify(captureMeta) : null
|
||||
]
|
||||
);
|
||||
|
||||
@@ -327,6 +396,7 @@ router.post('/:id/sections', validateParams(idParamSchema), validateRequest(lawn
|
||||
polygonData: section.polygon_data,
|
||||
grassType: section.grass_type,
|
||||
grassTypes: section.grass_types,
|
||||
captureMethod: section.capture_method,
|
||||
soilType: section.soil_type,
|
||||
createdAt: section.created_at,
|
||||
updatedAt: section.updated_at
|
||||
@@ -344,7 +414,7 @@ router.post('/:id/sections', validateParams(idParamSchema), validateRequest(lawn
|
||||
router.put('/:propertyId/sections/:sectionId', async (req, res, next) => {
|
||||
try {
|
||||
const { propertyId, sectionId } = req.params;
|
||||
const { name, area, polygonData, grassType, grassTypes, soilType } = req.body;
|
||||
const { name, area, polygonData, grassType, grassTypes, soilType, captureMethod, captureMeta } = req.body;
|
||||
|
||||
// Check if section exists and user owns the property
|
||||
const checkResult = await pool.query(
|
||||
@@ -365,18 +435,26 @@ router.put('/:propertyId/sections/:sectionId', async (req, res, next) => {
|
||||
calculatedArea = calculatePolygonArea(polygonData.coordinates[0]);
|
||||
}
|
||||
|
||||
let upoly = polygonData;
|
||||
try {
|
||||
const cleaned = sanitizePolygon(polygonData?.coordinates?.[0] || []);
|
||||
upoly = { ...polygonData, coordinates: [cleaned] };
|
||||
} catch {}
|
||||
|
||||
const result = await pool.query(
|
||||
`UPDATE lawn_sections
|
||||
SET name = $1, area = $2, polygon_data = $3, grass_type = $4, grass_types=$5, soil_type = $6, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $7
|
||||
SET name = $1, area = $2, polygon_data = $3, grass_type = $4, grass_types=$5, soil_type = $6, capture_method=$7, capture_meta=$8, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $9
|
||||
RETURNING *`,
|
||||
[
|
||||
name,
|
||||
calculatedArea,
|
||||
JSON.stringify(polygonData),
|
||||
JSON.stringify(upoly),
|
||||
grassType || (Array.isArray(grassTypes) ? grassTypes.join(', ') : null),
|
||||
grassTypes ? JSON.stringify(grassTypes) : null,
|
||||
soilType,
|
||||
captureMethod || null,
|
||||
captureMeta ? JSON.stringify(captureMeta) : null,
|
||||
sectionId
|
||||
]
|
||||
);
|
||||
@@ -394,6 +472,7 @@ router.put('/:propertyId/sections/:sectionId', async (req, res, next) => {
|
||||
polygonData: section.polygon_data,
|
||||
grassType: section.grass_type,
|
||||
grassTypes: section.grass_types,
|
||||
captureMethod: section.capture_method,
|
||||
soilType: section.soil_type,
|
||||
createdAt: section.created_at,
|
||||
updatedAt: section.updated_at
|
||||
|
||||
Reference in New Issue
Block a user