import math from typing import List, Dict, Any, Optional from django.db.models import QuerySet def apply_bounding_box( queryset: QuerySet, north: Optional[float], south: Optional[float], east: Optional[float], west: Optional[float] ) -> QuerySet: """ Filters a Django QuerySet of models having latitude and longitude within the viewport bounding box [north, south, east, west]. """ # Ensure items have non-null geo coordinates queryset = queryset.filter(latitude__isnull=False, longitude__isnull=False) if None in (north, south, east, west): return queryset # Standard bounding box if west <= east: return queryset.filter( latitude__gte=south, latitude__lte=north, longitude__gte=west, longitude__lte=east ) else: # Crosses the antimeridian (180th meridian) from django.db.models import Q return queryset.filter( latitude__gte=south, latitude__lte=north ).filter( Q(longitude__gte=west) | Q(longitude__lte=east) ) def lat_lng_to_pixel(lat: float, lng: float, zoom: int) -> tuple: """ Projects latitude/longitude into Web Mercator pixel coordinates at a given zoom level. """ sin_lat = math.sin(math.radians(lat)) # Clip sin_lat between -0.9999 and 0.9999 to prevent math domain error sin_lat = max(min(sin_lat, 0.9999), -0.9999) scale = 256 * (2 ** zoom) x = (lng + 180.0) / 360.0 * scale y = (0.5 - math.log((1.0 + sin_lat) / (1.0 - sin_lat)) / (4.0 * math.pi)) * scale return x, y def cluster_institutions( institutions_list: List[Dict[str, Any]], zoom: int = 10, cluster_radius_pixels: int = 60, max_zoom_cluster: int = 15 ) -> List[Dict[str, Any]]: """ Grid-distance spatial clustering algorithm. Groups nearby pins on the map at the given zoom level. """ if zoom >= max_zoom_cluster or not institutions_list: # Return individual items directly return [ { "is_cluster": False, "id": inst["id"], "name": inst["name"], "slug": inst["slug"], "type": inst["type"], "type_display": inst.get("type_display", inst["type"]), "lat": inst["lat"], "lng": inst["lng"], "city": inst["city"], "country": inst["country"], "avatar": inst.get("avatar"), "cover_image": inst.get("cover_image"), "is_featured": inst.get("is_featured", False), "verification_status": inst.get("verification_status", "pending"), "follower_count": inst.get("follower_count", 0), } for inst in institutions_list ] # Pre-calculate pixel positions points = [] for inst in institutions_list: lat = inst.get("lat") or inst.get("latitude") lng = inst.get("lng") or inst.get("longitude") if lat is None or lng is None: continue px, py = lat_lng_to_pixel(float(lat), float(lng), zoom) points.append({ "data": inst, "lat": float(lat), "lng": float(lng), "px": px, "py": py, "clustered": False }) clusters_result = [] for i, pt in enumerate(points): if pt["clustered"]: continue cluster_points = [pt] pt["clustered"] = True for j in range(i + 1, len(points)): other_pt = points[j] if other_pt["clustered"]: continue dx = pt["px"] - other_pt["px"] dy = pt["py"] - other_pt["py"] distance_sq = dx * dx + dy * dy if distance_sq <= (cluster_radius_pixels * cluster_radius_pixels): cluster_points.append(other_pt) other_pt["clustered"] = True if len(cluster_points) == 1: inst = cluster_points[0]["data"] clusters_result.append({ "is_cluster": False, "id": inst["id"], "name": inst["name"], "slug": inst["slug"], "type": inst["type"], "type_display": inst.get("type_display", inst["type"]), "lat": cluster_points[0]["lat"], "lng": cluster_points[0]["lng"], "city": inst["city"], "country": inst["country"], "avatar": inst.get("avatar"), "cover_image": inst.get("cover_image"), "is_featured": inst.get("is_featured", False), "verification_status": inst.get("verification_status", "pending"), "follower_count": inst.get("follower_count", 0), }) else: # Multi-point cluster total_lat = sum(p["lat"] for p in cluster_points) total_lng = sum(p["lng"] for p in cluster_points) center_lat = total_lat / len(cluster_points) center_lng = total_lng / len(cluster_points) type_breakdown = {} for p in cluster_points: t = p["data"]["type"] type_breakdown[t] = type_breakdown.get(t, 0) + 1 clusters_result.append({ "is_cluster": True, "cluster_id": f"c_{zoom}_{int(center_lat*1000)}_{int(center_lng*1000)}", "count": len(cluster_points), "lat": round(center_lat, 6), "lng": round(center_lng, 6), "type_breakdown": type_breakdown, "country": cluster_points[0]["data"]["country"], "preview_institutions": [ { "id": p["data"]["id"], "name": p["data"]["name"], "slug": p["data"]["slug"], "type": p["data"]["type"], "avatar": p["data"].get("avatar") } for p in cluster_points[:4] ] }) return clusters_result