/** * Spatial Operations Utility * Provides point-in-polygon calculations and location filtering within cut boundaries */ /** * Check if a point is inside a polygon using ray casting algorithm * @param {number} lat - Point latitude * @param {number} lng - Point longitude * @param {Array} polygon - Array of [lng, lat] coordinates * @returns {boolean} True if point is inside polygon */ function isPointInPolygon(lat, lng, polygon) { let inside = false; for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { const xi = polygon[i][0], yi = polygon[i][1]; const xj = polygon[j][0], yj = polygon[j][1]; if (((yi > lat) !== (yj > lat)) && (lng < (xj - xi) * (lat - yi) / (yj - yi) + xi)) { inside = !inside; } } return inside; } /** * Check if a location is within a cut's GeoJSON polygon * @param {Object} location - Location object with latitude/longitude * @param {Object} cutGeoJson - GeoJSON polygon object * @returns {boolean} True if location is within cut */ function isLocationInCut(location, cutGeoJson) { // Handle different possible field names for coordinates const lat = location.latitude || location.Latitude || location.lat; const lng = location.longitude || location.Longitude || location.lng || location.lon; if (!lat || !lng || !cutGeoJson) { return false; } try { const geojson = typeof cutGeoJson === 'string' ? JSON.parse(cutGeoJson) : cutGeoJson; if (geojson.type === 'Polygon') { return isPointInPolygon(lat, lng, geojson.coordinates[0]); } else if (geojson.type === 'MultiPolygon') { return geojson.coordinates.some(polygon => isPointInPolygon(lat, lng, polygon[0]) ); } return false; } catch (error) { console.error('Error checking point in polygon:', error); return false; } } /** * Filter locations based on cut boundaries and additional criteria * @param {Array} locations - Array of location objects * @param {Object} cut - Cut object with geojson * @param {Object} filters - Additional filter criteria * @returns {Array} Filtered locations within cut */ function filterLocationsInCut(locations, cut, filters = {}) { // Try multiple possible field names for the geojson data const geojsonData = cut.geojson || cut.Geojson || cut.GeoJSON || cut['GeoJSON Data'] || cut.geojson_data; if (!geojsonData) { return []; } // First filter by geographic boundaries let filteredLocations = locations.filter(location => isLocationInCut(location, geojsonData) ); // Apply additional filters if (filters.support_level) { filteredLocations = filteredLocations.filter(location => { const supportLevel = location.support_level || location['Support Level']; return supportLevel === filters.support_level; }); } if (filters.has_sign !== undefined) { filteredLocations = filteredLocations.filter(location => { const hasSign = location.sign || location.Sign; return Boolean(hasSign) === filters.has_sign; }); } if (filters.sign_size) { filteredLocations = filteredLocations.filter(location => { const signSize = location.sign_size || location['Sign Size']; return signSize === filters.sign_size; }); } if (filters.has_email !== undefined) { filteredLocations = filteredLocations.filter(location => { const email = location.email || location.Email; return Boolean(email) === filters.has_email; }); } if (filters.has_phone !== undefined) { filteredLocations = filteredLocations.filter(location => { const phone = location.phone || location.Phone; return Boolean(phone) === filters.has_phone; }); } return filteredLocations; } /** * Calculate statistics for locations within a cut * @param {Array} locations - Array of location objects within cut * @returns {Object} Statistics object */ function calculateCutStatistics(locations) { const stats = { total_locations: locations.length, support_levels: { '1': 0, '2': 0, '3': 0, '4': 0, 'unknown': 0 }, lawn_signs: { has_sign: 0, no_sign: 0, unknown: 0 }, sign_sizes: { Regular: 0, Large: 0, Unsure: 0, unknown: 0 }, contact_info: { has_email: 0, has_phone: 0, has_both: 0, has_neither: 0 } }; locations.forEach(location => { // Support level stats - handle different field names const supportLevel = location.support_level || location['Support Level'] || 'unknown'; if (stats.support_levels.hasOwnProperty(supportLevel)) { stats.support_levels[supportLevel]++; } else { stats.support_levels.unknown++; } // Lawn sign stats - handle different field names const sign = location.sign || location.Sign; if (sign === true || sign === 1 || sign === 'true') { stats.lawn_signs.has_sign++; } else if (sign === false || sign === 0 || sign === 'false') { stats.lawn_signs.no_sign++; } else { stats.lawn_signs.unknown++; } // Sign size stats - handle different field names const signSize = location.sign_size || location['Sign Size'] || 'unknown'; if (stats.sign_sizes.hasOwnProperty(signSize)) { stats.sign_sizes[signSize]++; } else { stats.sign_sizes.unknown++; } // Contact info stats - handle different field names const email = location.email || location.Email; const phone = location.phone || location.Phone; const hasEmail = Boolean(email); const hasPhone = Boolean(phone); if (hasEmail && hasPhone) { stats.contact_info.has_both++; } else if (hasEmail) { stats.contact_info.has_email++; } else if (hasPhone) { stats.contact_info.has_phone++; } else { stats.contact_info.has_neither++; } }); return stats; } module.exports = { isPointInPolygon, isLocationInCut, filterLocationsInCut, calculateCutStatistics };