import Supercluster from 'supercluster'; import { CLUSTER_CONFIG } from '../config/MapboxConfig'; /** * Initialize Supercluster with custom configuration * @param {Array} events - Array of news events with location data * @returns {Supercluster} Configured supercluster instance */ export const initializeCluster = (events) => { const cluster = new Supercluster({ radius: CLUSTER_CONFIG.radius, maxZoom: CLUSTER_CONFIG.maxZoom, minZoom: CLUSTER_CONFIG.minZoom, minPoints: CLUSTER_CONFIG.minPoints, map: (props) => ({ sentiment_sum: props.sentiment_score || 0, sentiment_count: props.sentiment_score !== null ? 1 : 0, negative_count: (props.sentiment_score || 0) < -0.3 ? 1 : 0 }), reduce: (accumulated, props) => { accumulated.sentiment_sum += props.sentiment_sum; accumulated.sentiment_count += props.sentiment_count; accumulated.negative_count += props.negative_count; } }); // Convert events to GeoJSON features const features = events.map(event => ({ type: 'Feature', properties: { id: event.id, title: event.title, summary: event.summary, link: event.link, description: event.description, priority: event.priority, sentiment_score: event.sentiment_score || 0, sentiment_label: event.sentiment_label || 'neutral', source_name: event.source_name, published_date: event.published_date, entity_name: event.location?.entity_name || 'Unknown' }, geometry: { type: 'Point', coordinates: [ event.location?.longitude || 0, event.location?.latitude || 0 ] } })); cluster.load(features); return cluster; }; /** * Get clusters and points for current map bounds and zoom * @param {Supercluster} cluster - Supercluster instance * @param {Object} bounds - Map bounds {west, south, east, north} * @param {number} zoom - Current zoom level * @returns {Array} Array of clusters and individual points */ export const getClustersInBounds = (cluster, bounds, zoom) => { if (!cluster || !bounds) return []; return cluster.getClusters( [bounds.west, bounds.south, bounds.east, bounds.north], Math.floor(zoom) ); }; /** * Get individual points from a cluster * @param {Supercluster} cluster - Supercluster instance * @param {number} clusterId - Cluster ID * @returns {Array} Array of individual point features */ export const getClusterExpansion = (cluster, clusterId) => { if (!cluster) return []; return cluster.getLeaves(clusterId, Infinity); }; /** * Calculate average sentiment for a cluster * @param {Object} clusterProperties - Cluster properties from supercluster * @returns {number} Average sentiment score */ export const getClusterSentiment = (clusterProperties) => { if (!clusterProperties.sentiment_count) return 0; return clusterProperties.sentiment_sum / clusterProperties.sentiment_count; }; /** * Calculate negative news ratio for a cluster * @param {Object} clusterProperties - Cluster properties * @returns {number} Ratio of negative news (0 to 1) */ export const getClusterNegativeRatio = (clusterProperties) => { const total = clusterProperties.point_count || 1; return (clusterProperties.negative_count || 0) / total; }; /** * Get zoom level to expand a cluster * @param {Supercluster} cluster - Supercluster instance * @param {number} clusterId - Cluster ID * @returns {number} Zoom level to expand cluster */ export const getClusterExpansionZoom = (cluster, clusterId) => { if (!cluster) return 10; return cluster.getClusterExpansionZoom(clusterId); };