import logging from django.db.models import Count, Avg, Sum, Q from rest_framework import status from rest_framework.generics import GenericAPIView from rest_framework.permissions import AllowAny from rest_framework.response import Response from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiResponse from apps.profiles.models.institution import Institution from apps.geo_map.clustering import apply_bounding_box, cluster_institutions from apps.geo_map.serializers import ( MapClusterItemSerializer, RegionalDensitySerializer, MapStatsSerializer, ) logger = logging.getLogger(__name__) class MapInstitutionsView(GenericAPIView): permission_classes = [AllowAny] serializer_class = MapClusterItemSerializer queryset = Institution.objects.filter(is_active=True, latitude__isnull=False, longitude__isnull=False) @extend_schema( summary="Query map markers with bounding box & clustering", description="Returns geographic markers or aggregated clusters for institutions within the current map viewport.", parameters=[ OpenApiParameter('north', float, description='Northernmost latitude of viewport'), OpenApiParameter('south', float, description='Southernmost latitude of viewport'), OpenApiParameter('east', float, description='Easternmost longitude of viewport'), OpenApiParameter('west', float, description='Westernmost longitude of viewport'), OpenApiParameter('zoom', int, description='Current map zoom level (1 to 20, default 10)'), OpenApiParameter('cluster', bool, description='Whether to cluster nearby pins (default true)'), OpenApiParameter('type', str, description='Filter by institution type'), OpenApiParameter('country', str, description='Filter by country'), OpenApiParameter('city', str, description='Filter by city'), OpenApiParameter('tag', str, description='Filter by tag string'), OpenApiParameter('is_featured', bool, description='Filter by featured status'), OpenApiParameter('search', str, description='Search text query'), ], responses={200: MapClusterItemSerializer(many=True)}, tags=["Smart Geo-Atlas"], ) def get(self, request, *args, **kwargs): qs = Institution.objects.filter(is_active=True, latitude__isnull=False, longitude__isnull=False) # Filters search = request.query_params.get('search') if search: qs = qs.filter( Q(name__icontains=search) | Q(city__icontains=search) | Q(country__icontains=search) | Q(description__icontains=search) ) country = request.query_params.get('country') if country: qs = qs.filter(country__iexact=country) city = request.query_params.get('city') if city: qs = qs.filter(city__iexact=city) region = request.query_params.get('region') if region: if region.isdigit(): qs = qs.filter(region_id=int(region)) else: qs = qs.filter(Q(region__slug=region) | Q(region__name__iexact=region)) inst_type = request.query_params.get('type') if inst_type: qs = qs.filter(type=inst_type) tag = request.query_params.get('tag') if tag: qs = qs.filter(tags__icontains=tag) is_featured = request.query_params.get('is_featured') if is_featured is not None: qs = qs.filter(is_featured=is_featured.lower() in ['true', '1']) # Bounding Box north = request.query_params.get('north') south = request.query_params.get('south') east = request.query_params.get('east') west = request.query_params.get('west') if all(v is not None for v in (north, south, east, west)): try: qs = apply_bounding_box( qs, north=float(north), south=float(south), east=float(east), west=float(west) ) except ValueError: pass # Zoom & Cluster Settings try: zoom = int(request.query_params.get('zoom', 10)) except ValueError: zoom = 10 enable_cluster = request.query_params.get('cluster', 'true').lower() in ['true', '1'] # Format items institutions_list = [] for inst in qs.select_related('region').only('id', 'name', 'slug', 'type', 'latitude', 'longitude', 'city', 'country', 'avatar', 'cover_image', 'is_featured', 'verification_status', 'follower_count', 'region'): avatar_url = inst.avatar.url if inst.avatar else None cover_url = inst.cover_image.url if inst.cover_image else None institutions_list.append({ "id": inst.id, "name": inst.name, "slug": inst.slug, "type": inst.type, "type_display": inst.get_type_display(), "region": inst.region_id, "region_name": inst.region.name if inst.region else None, "lat": inst.latitude, "lng": inst.longitude, "city": inst.city, "country": inst.country, "avatar": avatar_url, "cover_image": cover_url, "is_featured": inst.is_featured, "verification_status": inst.verification_status, "follower_count": inst.follower_count, }) if enable_cluster: clustered_results = cluster_institutions(institutions_list, zoom=zoom) else: clustered_results = cluster_institutions(institutions_list, zoom=20) return Response(clustered_results, status=status.HTTP_200_OK) class MapClustersView(GenericAPIView): permission_classes = [AllowAny] serializer_class = RegionalDensitySerializer queryset = Institution.objects.filter(is_active=True, latitude__isnull=False, longitude__isnull=False) @extend_schema( summary="Regional density & geographic cluster overview", description="Returns aggregated institution counts and density breakdown per country and region.", responses={200: RegionalDensitySerializer(many=True)}, tags=["Smart Geo-Atlas"], ) def get(self, request, *args, **kwargs): qs = Institution.objects.filter(is_active=True, latitude__isnull=False, longitude__isnull=False) countries_agg = qs.values('country').annotate( total=Count('id'), avg_lat=Avg('latitude'), avg_lng=Avg('longitude'), total_followers=Sum('follower_count') ).order_by('-total') results = [] for c in countries_agg: country_name = c['country'] country_qs = qs.filter(country=country_name) results.append({ 'country': country_name, 'total_institutions': c['total'], 'mosques_count': country_qs.filter(type=Institution.InstitutionType.MOSQUE).count(), 'hussainiyas_count': country_qs.filter(type=Institution.InstitutionType.HUSSAINIYA).count(), 'cultural_centers_count': country_qs.filter(type=Institution.InstitutionType.CULTURAL_CENTER).count(), 'libraries_count': country_qs.filter(type=Institution.InstitutionType.LIBRARY).count(), 'institutes_count': country_qs.filter(type=Institution.InstitutionType.INSTITUTE).count(), 'charities_count': country_qs.filter(type=Institution.InstitutionType.CHARITY).count(), 'total_followers': c['total_followers'] or 0, 'center_lat': round(c['avg_lat'], 6) if c['avg_lat'] is not None else None, 'center_lng': round(c['avg_lng'], 6) if c['avg_lng'] is not None else None, }) return Response(results, status=status.HTTP_200_OK) class MapStatsView(GenericAPIView): permission_classes = [AllowAny] serializer_class = MapStatsSerializer queryset = Institution.objects.filter(is_active=True) @extend_schema( summary="Get global atlas statistics", description="Returns total institutions, countries covered, cities count, and classification distributions.", responses={200: MapStatsSerializer}, tags=["Smart Geo-Atlas"], ) def get(self, request, *args, **kwargs): qs = Institution.objects.filter(is_active=True) type_counts = dict(qs.values_list('type').annotate(count=Count('id'))) data = { 'total_institutions': qs.count(), 'total_countries': qs.values('country').distinct().count(), 'total_cities': qs.values('city').distinct().count(), 'type_distribution': type_counts, 'verified_count': qs.filter(verification_status=Institution.VerificationStatus.APPROVED).count(), 'featured_count': qs.filter(is_featured=True).count(), } return Response(data, status=status.HTTP_200_OK)