From 9951eba14663ca3cfaa8e08f50a0628142c204c4 Mon Sep 17 00:00:00 2001 From: JerryVincent Date: Thu, 11 Sep 2025 14:55:42 +0200 Subject: [PATCH 1/3] added time constraints --- .../layers/mobile/mobile-overview-layer.tsx | 155 +++++++++++++++--- 1 file changed, 136 insertions(+), 19 deletions(-) diff --git a/app/components/map/layers/mobile/mobile-overview-layer.tsx b/app/components/map/layers/mobile/mobile-overview-layer.tsx index 97c5fd63..ccd2d512 100644 --- a/app/components/map/layers/mobile/mobile-overview-layer.tsx +++ b/app/components/map/layers/mobile/mobile-overview-layer.tsx @@ -15,6 +15,7 @@ const FIT_PADDING = 100 // Clustering configuration const CLUSTER_DISTANCE_METERS = 8 // Distance threshold for clustering +const CLUSTER_TIME_THRESHOLD_MINUTES = 30 // Time threshold for clustering (30 minutes) const MIN_CLUSTER_SIZE = 15 // Minimum points to form a cluster // Function to calculate distance between two points in meters @@ -33,24 +34,36 @@ function calculateDistance(point1: LocationPoint, point2: LocationPoint): number return R * c } -// Cluster points within a single trip -function clusterTripPoints(points: LocationPoint[], distanceThreshold: number, minClusterSize: number) { +// Function to calculate time difference in minutes +function calculateTimeDifference(point1: LocationPoint, point2: LocationPoint): number { + const time1 = new Date(point1.time).getTime() + const time2 = new Date(point2.time).getTime() + return Math.abs(time2 - time1) / (1000 * 60) // Convert to minutes +} + +function clusterTripPoints( + points: LocationPoint[], + distanceThreshold: number, + timeThresholdMinutes: number, + minClusterSize: number +) { const clusters: LocationPoint[][] = [] const visited = new Set() + const sortedPoints = [...points].sort((a, b) => new Date(a.time).getTime() - new Date(b.time).getTime()) - for (let i = 0; i < points.length; i++) { + for (let i = 0; i < sortedPoints.length; i++) { if (visited.has(i)) continue - const cluster: LocationPoint[] = [points[i]] + const cluster: LocationPoint[] = [sortedPoints[i]] visited.add(i) - - // Find all points within distance threshold - for (let j = i + 1; j < points.length; j++) { + for (let j = i + 1; j < sortedPoints.length; j++) { if (visited.has(j)) continue - const distance = calculateDistance(points[i], points[j]) - if (distance <= distanceThreshold) { - cluster.push(points[j]) + const spatialDistance = calculateDistance(sortedPoints[i], sortedPoints[j]) + const temporalDistance = calculateTimeDifference(sortedPoints[i], sortedPoints[j]) + + if (spatialDistance <= distanceThreshold && temporalDistance <= timeThresholdMinutes) { + cluster.push(sortedPoints[j]) visited.add(j) } } @@ -67,19 +80,99 @@ function clusterTripPoints(points: LocationPoint[], distanceThreshold: number, m return clusters } -// Calculate cluster center and metadata +function dbscanClusterTripPoints( + points: LocationPoint[], + distanceThreshold: number, + timeThresholdMinutes: number, + minClusterSize: number +) { + const clusters: LocationPoint[][] = [] + const visited = new Set() + const noise: LocationPoint[] = [] + + function findNeighbors(pointIndex: number): number[] { + const neighbors: number[] = [] + const currentPoint = points[pointIndex] + + for (let i = 0; i < points.length; i++) { + if (i === pointIndex) continue + + const spatialDistance = calculateDistance(currentPoint, points[i]) + const temporalDistance = calculateTimeDifference(currentPoint, points[i]) + + if (spatialDistance <= distanceThreshold && temporalDistance <= timeThresholdMinutes) { + neighbors.push(i) + } + } + + return neighbors + } + // Helper function to expand cluster + function expandCluster(pointIndex: number, neighbors: number[], cluster: LocationPoint[]) { + cluster.push(points[pointIndex]) + visited.add(pointIndex) + + for (let i = 0; i < neighbors.length; i++) { + const neighborIndex = neighbors[i] + + if (!visited.has(neighborIndex)) { + visited.add(neighborIndex) + const neighborNeighbors = findNeighbors(neighborIndex) + + if (neighborNeighbors.length >= minClusterSize - 1) { + neighbors.push(...neighborNeighbors.filter(n => !neighbors.includes(n))) + } + } + + // Add to cluster if not already in any cluster + if (!clusters.some(c => c.includes(points[neighborIndex])) && !cluster.includes(points[neighborIndex])) { + cluster.push(points[neighborIndex]) + } + } + } + + for (let i = 0; i < points.length; i++) { + if (visited.has(i)) continue + + const neighbors = findNeighbors(i) + + if (neighbors.length < minClusterSize - 1) { + // Mark as noise (will be individual points) + noise.push(points[i]) + visited.add(i) + } else { + // Start new cluster + const cluster: LocationPoint[] = [] + expandCluster(i, neighbors, cluster) + clusters.push(cluster) + } + } + + // Add noise points as individual clusters + noise.forEach(point => clusters.push([point])) + + return clusters +} + + function calculateClusterCenter(cluster: LocationPoint[], clusterId: string) { const centerX = cluster.reduce((sum, point) => sum + point.geometry.x, 0) / cluster.length const centerY = cluster.reduce((sum, point) => sum + point.geometry.y, 0) / cluster.length - // Sort by timestamp to get earliest and latest + // Sorting by timestamp to get earliest and latest const sortedByTime = cluster.sort((a, b) => new Date(a.time).getTime() - new Date(b.time).getTime()) + // Calculate duration for clusters + const startTime = sortedByTime[0].time + const endTime = sortedByTime[sortedByTime.length - 1].time + const duration = new Date(endTime).getTime() - new Date(startTime).getTime() + return { coordinates: [centerX, centerY], pointCount: cluster.length, - startTime: sortedByTime[0].time, - endTime: sortedByTime[sortedByTime.length - 1].time, + startTime, + endTime, + duration, isCluster: cluster.length > 1, clusterId: clusterId, originalPoints: cluster // Keep reference to original points @@ -115,8 +208,12 @@ export default function MobileOverviewLayer({ if (!trips || trips.length === 0) return [] return trips.map((trip, tripIndex) => { - const clusters = clusterTripPoints(trip.points, CLUSTER_DISTANCE_METERS, MIN_CLUSTER_SIZE) - + const clusters = clusterTripPoints( + trip.points, + CLUSTER_DISTANCE_METERS, + CLUSTER_TIME_THRESHOLD_MINUTES, + MIN_CLUSTER_SIZE + ) return { ...trip, clusters: clusters.map((cluster, clusterIndex) => @@ -136,6 +233,7 @@ export default function MobileOverviewLayer({ isCluster?: boolean startTime?: string endTime?: string + duration?: number clusterId?: string } > | null>(null) @@ -170,6 +268,7 @@ export default function MobileOverviewLayer({ startTime: string endTime: string pointCount?: number + duration?: number isCluster?: boolean } | null>(null) @@ -191,6 +290,7 @@ export default function MobileOverviewLayer({ isCluster: cluster.isCluster, startTime: cluster.startTime, endTime: cluster.endTime, + duration: cluster.duration, clusterId: cluster.clusterId, }), ), @@ -253,7 +353,7 @@ export default function MobileOverviewLayer({ if (event.features && event.features.length > 0) { const feature = event.features[0] - const { tripNumber, startTime, endTime, pointCount, isCluster, clusterId } = feature.properties + const { tripNumber, startTime, endTime, pointCount, duration, isCluster, clusterId } = feature.properties setHighlightedTrip(tripNumber) // Set hovered cluster if it's a cluster @@ -270,6 +370,7 @@ export default function MobileOverviewLayer({ startTime, endTime, pointCount, + duration, isCluster }) } else { @@ -311,6 +412,18 @@ export default function MobileOverviewLayer({ } }, [mapRef, handleHover, showOriginalColors]) + // Helper function to format duration + const formatDuration = (durationMs: number): string => { + const minutes = Math.floor(durationMs / (1000 * 60)) + const hours = Math.floor(minutes / 60) + + if (hours > 0) { + const remainingMinutes = minutes % 60 + return `${hours}h ${remainingMinutes}m` + } + return `${minutes}m` + } + if (!sourceData) return null return ( @@ -425,6 +538,11 @@ export default function MobileOverviewLayer({

Cluster of {popupInfo.pointCount} points

+ {popupInfo.duration && ( +

+ Duration: {formatDuration(popupInfo.duration)} +

+ )} )}
@@ -461,5 +579,4 @@ export default function MobileOverviewLayer({ /> ) -} - +} \ No newline at end of file From 48bc614524990421ab117ca6a122ef4b0ab485f6 Mon Sep 17 00:00:00 2001 From: JerryVincent Date: Tue, 23 Sep 2025 14:40:17 +0200 Subject: [PATCH 2/3] updated cluster centering --- .../layers/mobile/mobile-overview-layer.tsx | 513 +++++++++++------- 1 file changed, 303 insertions(+), 210 deletions(-) diff --git a/app/components/map/layers/mobile/mobile-overview-layer.tsx b/app/components/map/layers/mobile/mobile-overview-layer.tsx index ccd2d512..264be838 100644 --- a/app/components/map/layers/mobile/mobile-overview-layer.tsx +++ b/app/components/map/layers/mobile/mobile-overview-layer.tsx @@ -15,8 +15,8 @@ const FIT_PADDING = 100 // Clustering configuration const CLUSTER_DISTANCE_METERS = 8 // Distance threshold for clustering -const CLUSTER_TIME_THRESHOLD_MINUTES = 30 // Time threshold for clustering (30 minutes) const MIN_CLUSTER_SIZE = 15 // Minimum points to form a cluster +const DENSITY_THRESHOLD = 0.5 // Only cluster the most dense 50% of candidate points // Function to calculate distance between two points in meters function calculateDistance(point1: LocationPoint, point2: LocationPoint): number { @@ -34,148 +34,225 @@ function calculateDistance(point1: LocationPoint, point2: LocationPoint): number return R * c } -// Function to calculate time difference in minutes -function calculateTimeDifference(point1: LocationPoint, point2: LocationPoint): number { - const time1 = new Date(point1.time).getTime() - const time2 = new Date(point2.time).getTime() - return Math.abs(time2 - time1) / (1000 * 60) // Convert to minutes -} - -function clusterTripPoints( - points: LocationPoint[], - distanceThreshold: number, - timeThresholdMinutes: number, - minClusterSize: number -) { - const clusters: LocationPoint[][] = [] - const visited = new Set() - const sortedPoints = [...points].sort((a, b) => new Date(a.time).getTime() - new Date(b.time).getTime()) - - for (let i = 0; i < sortedPoints.length; i++) { - if (visited.has(i)) continue +// Function to calculate density score for each point based on nearby neighbors +function calculateDensityScore(points: LocationPoint[], pointIndex: number, distanceThreshold: number): number { + const targetPoint = points[pointIndex] + let nearbyCount = 0 + let totalDistance = 0 - const cluster: LocationPoint[] = [sortedPoints[i]] - visited.add(i) - for (let j = i + 1; j < sortedPoints.length; j++) { - if (visited.has(j)) continue + for (let i = 0; i < points.length; i++) { + if (i === pointIndex) continue + + const distance = calculateDistance(targetPoint, points[i]) + if (distance <= distanceThreshold) { + nearbyCount++ + totalDistance += distance + } + } - const spatialDistance = calculateDistance(sortedPoints[i], sortedPoints[j]) - const temporalDistance = calculateTimeDifference(sortedPoints[i], sortedPoints[j]) + // Higher score = more dense (more neighbors, closer together) + // Avoid division by zero + if (nearbyCount === 0) return 0 + + // Score combines neighbor count with average proximity + const averageDistance = totalDistance / nearbyCount + const maxDistance = distanceThreshold + const proximityScore = (maxDistance - averageDistance) / maxDistance + + return nearbyCount * (1 + proximityScore) +} - if (spatialDistance <= distanceThreshold && temporalDistance <= timeThresholdMinutes) { - cluster.push(sortedPoints[j]) - visited.add(j) - } - } +// Function to find all points within distance threshold from a center point +function findPointsInRadius(points: LocationPoint[], centerIndex: number, distanceThreshold: number): number[] { + const pointsInRadius: number[] = [centerIndex] // Include the center point itself + const centerPoint = points[centerIndex] - // Only create cluster if it meets minimum size requirement - if (cluster.length >= minClusterSize) { - clusters.push(cluster) - } else { - // Add individual points that don't form clusters - cluster.forEach(point => clusters.push([point])) + for (let i = 0; i < points.length; i++) { + if (i === centerIndex) continue + + const distance = calculateDistance(centerPoint, points[i]) + if (distance <= distanceThreshold) { + pointsInRadius.push(i) } } - return clusters + return pointsInRadius } -function dbscanClusterTripPoints( - points: LocationPoint[], - distanceThreshold: number, - timeThresholdMinutes: number, - minClusterSize: number -) { - const clusters: LocationPoint[][] = [] - const visited = new Set() - const noise: LocationPoint[] = [] +// Function to select only the most densely packed points from candidates +function selectDensestPoints(points: LocationPoint[], candidateIndices: number[], distanceThreshold: number, densityThreshold: number): number[] { + // Calculate density score for each candidate point + const pointsWithDensity = candidateIndices.map(index => ({ + index, + densityScore: calculateDensityScore(points, index, distanceThreshold) + })) - function findNeighbors(pointIndex: number): number[] { - const neighbors: number[] = [] - const currentPoint = points[pointIndex] + // Sort by density score (highest first) + pointsWithDensity.sort((a, b) => b.densityScore - a.densityScore) - for (let i = 0; i < points.length; i++) { - if (i === pointIndex) continue + // Take only the top percentage based on density threshold + const numToTake = Math.max(MIN_CLUSTER_SIZE, Math.ceil(pointsWithDensity.length * densityThreshold)) + const selectedPoints = pointsWithDensity.slice(0, numToTake) - const spatialDistance = calculateDistance(currentPoint, points[i]) - const temporalDistance = calculateTimeDifference(currentPoint, points[i]) - - if (spatialDistance <= distanceThreshold && temporalDistance <= timeThresholdMinutes) { - neighbors.push(i) - } - } + return selectedPoints.map(p => p.index) +} - return neighbors +// Function to calculate cluster quality focusing on spatial centrality +function calculateSpatialCentrality(points: LocationPoint[], centerIndex: number, clusterIndices: number[]): number { + const centerPoint = points[centerIndex] + let totalDistanceSquared = 0 + + // Calculate sum of squared distances (minimize this for better centrality) + for (const idx of clusterIndices) { + if (idx === centerIndex) continue + const distance = calculateDistance(centerPoint, points[idx]) + totalDistanceSquared += distance * distance } - // Helper function to expand cluster - function expandCluster(pointIndex: number, neighbors: number[], cluster: LocationPoint[]) { - cluster.push(points[pointIndex]) - visited.add(pointIndex) - - for (let i = 0; i < neighbors.length; i++) { - const neighborIndex = neighbors[i] - - if (!visited.has(neighborIndex)) { - visited.add(neighborIndex) - const neighborNeighbors = findNeighbors(neighborIndex) + + // Lower score means better centrality (center point minimizes total squared distances) + return totalDistanceSquared / clusterIndices.length +} - if (neighborNeighbors.length >= minClusterSize - 1) { - neighbors.push(...neighborNeighbors.filter(n => !neighbors.includes(n))) - } - } +// Function to find the most spatially central point within a potential cluster +function findOptimalClusterCenter(points: LocationPoint[], candidateIndices: number[], distanceThreshold: number): { + centerIndex: number + clusterIndices: number[] + quality: number +} { + // First, select only the most densely packed points from all candidates + const densePointIndices = selectDensestPoints(points, candidateIndices, distanceThreshold, DENSITY_THRESHOLD) + + if (densePointIndices.length < MIN_CLUSTER_SIZE) { + // If we don't have enough dense points, fall back to the original approach + const fallbackCenter = candidateIndices[0] + const fallbackCluster = findPointsInRadius(points, fallbackCenter, distanceThreshold) + return { + centerIndex: fallbackCenter, + clusterIndices: fallbackCluster.length >= MIN_CLUSTER_SIZE ? fallbackCluster : [], + quality: fallbackCluster.length >= MIN_CLUSTER_SIZE ? calculateSpatialCentrality(points, fallbackCenter, fallbackCluster) : Infinity + } + } - // Add to cluster if not already in any cluster - if (!clusters.some(c => c.includes(points[neighborIndex])) && !cluster.includes(points[neighborIndex])) { - cluster.push(points[neighborIndex]) + let bestCenter = densePointIndices[0] + let bestClusterIndices = densePointIndices + let bestQuality = calculateSpatialCentrality(points, bestCenter, bestClusterIndices) + + // Test each dense point as a potential cluster center + for (const candidateIndex of densePointIndices) { + // For this center, find which dense points are within radius + const clusterIndices = densePointIndices.filter(idx => { + if (idx === candidateIndex) return true + return calculateDistance(points[candidateIndex], points[idx]) <= distanceThreshold + }) + + // Only consider if cluster is large enough + if (clusterIndices.length >= MIN_CLUSTER_SIZE) { + const quality = calculateSpatialCentrality(points, candidateIndex, clusterIndices) + + // Better quality = lower sum of squared distances (more central) + if (quality < bestQuality) { + bestCenter = candidateIndex + bestClusterIndices = clusterIndices + bestQuality = quality } } } + + return { + centerIndex: bestCenter, + clusterIndices: bestClusterIndices, + quality: bestQuality + } +} +// Enhanced clustering algorithm that finds truly optimal spatial centers +function spatiallyOptimizedClustering(points: LocationPoint[], distanceThreshold: number, minClusterSize: number) { + const clusters: LocationPoint[][] = [] + const visited = new Set() + + // Find all dense regions (areas with enough points for clustering) + const denseRegions: Array<{ + centerIndex: number + clusterIndices: number[] + quality: number + }> = [] + + // First pass: identify all potential dense regions for (let i = 0; i < points.length; i++) { if (visited.has(i)) continue - - const neighbors = findNeighbors(i) - - if (neighbors.length < minClusterSize - 1) { - // Mark as noise (will be individual points) - noise.push(points[i]) - visited.add(i) - } else { - // Start new cluster - const cluster: LocationPoint[] = [] - expandCluster(i, neighbors, cluster) - clusters.push(cluster) + + const pointsInRadius = findPointsInRadius(points, i, distanceThreshold) + + if (pointsInRadius.length >= minClusterSize) { + // Find the optimal center within this dense region, focusing only on densest points + const optimalCenter = findOptimalClusterCenter(points, pointsInRadius, distanceThreshold) + + // Only add if we found a valid cluster + if (optimalCenter.clusterIndices.length >= minClusterSize) { + denseRegions.push(optimalCenter) + } } } - - // Add noise points as individual clusters - noise.forEach(point => clusters.push([point])) - + + // Sort dense regions by quality (better spatial centrality first) + denseRegions.sort((a, b) => a.quality - b.quality) + + // Second pass: greedily select non-overlapping clusters starting with best spatial centers + for (const region of denseRegions) { + // Check if any points in this cluster are already assigned + if (region.clusterIndices.some(idx => visited.has(idx))) { + continue + } + + // Mark all points in this cluster as visited + region.clusterIndices.forEach(idx => visited.add(idx)) + + // Create the cluster using the optimal center's point collection + const cluster = region.clusterIndices.map(idx => points[idx]) + clusters.push(cluster) + } + + // Add remaining unvisited points as individual clusters + for (let i = 0; i < points.length; i++) { + if (!visited.has(i)) { + clusters.push([points[i]]) + } + } + return clusters } - -function calculateClusterCenter(cluster: LocationPoint[], clusterId: string) { +// Calculate cluster center using the actual geometric centroid +function calculateOptimalClusterCenter(cluster: LocationPoint[], clusterId: string) { + // Calculate geometric centroid (true center of mass) const centerX = cluster.reduce((sum, point) => sum + point.geometry.x, 0) / cluster.length const centerY = cluster.reduce((sum, point) => sum + point.geometry.y, 0) / cluster.length - // Sorting by timestamp to get earliest and latest + // Sort by timestamp to get earliest and latest const sortedByTime = cluster.sort((a, b) => new Date(a.time).getTime() - new Date(b.time).getTime()) - // Calculate duration for clusters + // Calculate additional metrics const startTime = sortedByTime[0].time const endTime = sortedByTime[sortedByTime.length - 1].time const duration = new Date(endTime).getTime() - new Date(startTime).getTime() + // Calculate cluster spread (max distance from centroid) + const maxDistanceFromCenter = Math.max(...cluster.map(point => { + const dx = point.geometry.x - centerX + const dy = point.geometry.y - centerY + return Math.sqrt(dx * dx + dy * dy) * 111320 // Convert to approximate meters + })) + return { coordinates: [centerX, centerY], pointCount: cluster.length, startTime, endTime, - duration, + duration, + maxSpread: maxDistanceFromCenter, isCluster: cluster.length > 1, clusterId: clusterId, - originalPoints: cluster // Keep reference to original points + originalPoints: cluster } } @@ -200,24 +277,22 @@ export default function MobileOverviewLayer({ }: { locations: LocationPoint[] }) { - // Generate trips and assign colors once + const trips = useMemo(() => categorizeIntoTrips(locations, 50), [locations]) - - // Cluster points within each trip const clusteredTrips = useMemo(() => { if (!trips || trips.length === 0) return [] return trips.map((trip, tripIndex) => { - const clusters = clusterTripPoints( - trip.points, - CLUSTER_DISTANCE_METERS, - CLUSTER_TIME_THRESHOLD_MINUTES, + const clusters = spatiallyOptimizedClustering( + trip.points, + CLUSTER_DISTANCE_METERS, MIN_CLUSTER_SIZE - ) + ) + return { ...trip, clusters: clusters.map((cluster, clusterIndex) => - calculateClusterCenter(cluster, `trip-${tripIndex}-cluster-${clusterIndex}`) + calculateOptimalClusterCenter(cluster, `trip-${tripIndex}-cluster-${clusterIndex}`) ) } }) @@ -234,6 +309,7 @@ export default function MobileOverviewLayer({ startTime?: string endTime?: string duration?: number + maxSpread?: number clusterId?: string } > | null>(null) @@ -250,18 +326,14 @@ export default function MobileOverviewLayer({ const { osem: mapRef } = useMap() - // Legend items state const [legendItems, setLegendItems] = useState< { label: string; color: string }[] >([]) - // State to track the highlighted trip number const [highlightedTrip, setHighlightedTrip] = useState(null) - // State to track the hovered cluster const [hoveredCluster, setHoveredCluster] = useState(null) - // State to track the popup information const [popupInfo, setPopupInfo] = useState<{ longitude: number latitude: number @@ -269,6 +341,7 @@ export default function MobileOverviewLayer({ endTime: string pointCount?: number duration?: number + maxSpread?: number isCluster?: boolean } | null>(null) @@ -291,12 +364,12 @@ export default function MobileOverviewLayer({ startTime: cluster.startTime, endTime: cluster.endTime, duration: cluster.duration, + maxSpread: cluster.maxSpread, clusterId: cluster.clusterId, }), ), ) - // Create expanded points data for cluster hover const expandedPoints = clusteredTrips.flatMap((trip, tripIndex) => trip.clusters.flatMap((cluster) => { if (!cluster.isCluster || !cluster.originalPoints) return [] @@ -312,7 +385,6 @@ export default function MobileOverviewLayer({ }) ) - // Set legend items for the trips const legend = clusteredTrips.map((_, index) => ({ label: `Trip ${index + 1}`, color: colors[index], @@ -353,7 +425,7 @@ export default function MobileOverviewLayer({ if (event.features && event.features.length > 0) { const feature = event.features[0] - const { tripNumber, startTime, endTime, pointCount, duration, isCluster, clusterId } = feature.properties + const { tripNumber, startTime, endTime, pointCount, duration, maxSpread, isCluster, clusterId } = feature.properties setHighlightedTrip(tripNumber) // Set hovered cluster if it's a cluster @@ -371,6 +443,7 @@ export default function MobileOverviewLayer({ endTime, pointCount, duration, + maxSpread, isCluster }) } else { @@ -411,8 +484,7 @@ export default function MobileOverviewLayer({ mapRef.off('mouseleave', 'box-overview-layer', onMouseLeave) } }, [mapRef, handleHover, showOriginalColors]) - - // Helper function to format duration + const formatDuration = (durationMs: number): string => { const minutes = Math.floor(durationMs / (1000 * 60)) const hours = Math.floor(minutes / 60) @@ -429,98 +501,114 @@ export default function MobileOverviewLayer({ return ( <> - - - {/* Text layer for cluster point counts */} - - - - {/* Expanded cluster points - shown on hover */} - {hoveredCluster && expandedSourceData && ( - - - - )} - + + + {/* Text layer for cluster point counts */} + + + +{/* Expanded cluster points - shown on hover */} +{hoveredCluster && expandedSourceData && ( + + + +)} + {highlightedTrip && popupInfo && ( )} + {popupInfo.maxSpread && ( +

+ Spread: {Math.round(popupInfo.maxSpread)}m +

+ )}
)}
From 5ad86d7296c0e68c375de4c310cc48c94a77d4cf Mon Sep 17 00:00:00 2001 From: JerryVincent Date: Wed, 1 Oct 2025 10:41:15 +0200 Subject: [PATCH 3/3] modified clustering --- .../layers/mobile/mobile-overview-layer.tsx | 26 ++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/app/components/map/layers/mobile/mobile-overview-layer.tsx b/app/components/map/layers/mobile/mobile-overview-layer.tsx index 264be838..374d1746 100644 --- a/app/components/map/layers/mobile/mobile-overview-layer.tsx +++ b/app/components/map/layers/mobile/mobile-overview-layer.tsx @@ -51,7 +51,6 @@ function calculateDensityScore(points: LocationPoint[], pointIndex: number, dist } // Higher score = more dense (more neighbors, closer together) - // Avoid division by zero if (nearbyCount === 0) return 0 // Score combines neighbor count with average proximity @@ -165,22 +164,20 @@ function findOptimalClusterCenter(points: LocationPoint[], candidateIndices: num } } -// Enhanced clustering algorithm that finds truly optimal spatial centers +// clustering algorithm that finds truly optimal spatial centers function spatiallyOptimizedClustering(points: LocationPoint[], distanceThreshold: number, minClusterSize: number) { const clusters: LocationPoint[][] = [] const visited = new Set() - // Find all dense regions (areas with enough points for clustering) - const denseRegions: Array<{ + // Evaluate ALL points as potential cluster centers + const allPotentialClusters: Array<{ centerIndex: number clusterIndices: number[] quality: number }> = [] - // First pass: identify all potential dense regions + // First pass: evaluate EVERY point as a potential cluster center for (let i = 0; i < points.length; i++) { - if (visited.has(i)) continue - const pointsInRadius = findPointsInRadius(points, i, distanceThreshold) if (pointsInRadius.length >= minClusterSize) { @@ -189,26 +186,25 @@ function spatiallyOptimizedClustering(points: LocationPoint[], distanceThreshold // Only add if we found a valid cluster if (optimalCenter.clusterIndices.length >= minClusterSize) { - denseRegions.push(optimalCenter) + allPotentialClusters.push(optimalCenter) } } } - // Sort dense regions by quality (better spatial centrality first) - denseRegions.sort((a, b) => a.quality - b.quality) + //Sort ALL potential clusters by quality (better spatial centrality first) + allPotentialClusters.sort((a, b) => a.quality - b.quality) - // Second pass: greedily select non-overlapping clusters starting with best spatial centers - for (const region of denseRegions) { + for (const potentialCluster of allPotentialClusters) { // Check if any points in this cluster are already assigned - if (region.clusterIndices.some(idx => visited.has(idx))) { + if (potentialCluster.clusterIndices.some(idx => visited.has(idx))) { continue } // Mark all points in this cluster as visited - region.clusterIndices.forEach(idx => visited.add(idx)) + potentialCluster.clusterIndices.forEach(idx => visited.add(idx)) // Create the cluster using the optimal center's point collection - const cluster = region.clusterIndices.map(idx => points[idx]) + const cluster = potentialCluster.clusterIndices.map(idx => points[idx]) clusters.push(cluster) }