203 lines
6.9 KiB
JavaScript
203 lines
6.9 KiB
JavaScript
|
|
/**
|
|
* 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 latRaw = location.latitude || location.Latitude || location.lat;
|
|
const lngRaw = location.longitude || location.Longitude || location.lng || location.lon;
|
|
|
|
if (!latRaw || !lngRaw || !cutGeoJson) {
|
|
return false;
|
|
}
|
|
|
|
// Parse coordinates as floats since they might be strings
|
|
const lat = parseFloat(latRaw);
|
|
const lng = parseFloat(lngRaw);
|
|
|
|
if (isNaN(lat) || isNaN(lng)) {
|
|
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) {
|
|
console.log('No geojson data found for cut:', cut.name || cut.Name || 'Unknown');
|
|
return [];
|
|
}
|
|
|
|
// Debug: Log total locations being tested
|
|
console.log(`Testing ${locations.length} locations against cut boundaries`);
|
|
|
|
// First filter by geographic boundaries
|
|
let filteredLocations = locations.filter(location =>
|
|
isLocationInCut(location, geojsonData)
|
|
);
|
|
|
|
console.log(`Filtered ${locations.length} total locations down to ${filteredLocations.length} locations within cut bounds`);
|
|
|
|
// 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
|
|
};
|