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
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)
|