freealberta/map/app/utils/spatial.js

189 lines
6.3 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 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
};