You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

201 lines
8.6 KiB

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)
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.only('id', 'name', 'slug', 'type', 'latitude', 'longitude', 'city', 'country', 'avatar', 'cover_image', 'is_featured', 'verification_status', 'follower_count'):
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(),
"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)