import math from django.shortcuts import render from django.db.models import F, ExpressionWrapper, FloatField from django.db.models.functions import Sqrt, Power, Sin, Cos, Radians, ATan2 from rest_framework import generics, filters, permissions from django_filters.rest_framework import DjangoFilterBackend import logging from location import models, serializers logger = logging.getLogger(__name__) # PROVINCES class ProvinceList(generics.ListCreateAPIView): queryset = models.Province.objects.all() serializer_class = serializers.ProvinceSerializer filter_backends = [filters.SearchFilter, filters.OrderingFilter, DjangoFilterBackend] search_fields = ['name', 'code'] filterset_fields = ['name', 'code'] ordering_fields = '__all__' def get_permissions(self): if self.request.method == 'POST': return [permissions.IsAdminUser()] return [permissions.IsAuthenticated()] class ProvinceDetail(generics.RetrieveUpdateDestroyAPIView): queryset = models.Province.objects.all() serializer_class = serializers.ProvinceSerializer def get_permissions(self): if self.request.method == 'PUT' or self.request.method == 'DELETE': return [permissions.IsAdminUser()] return [permissions.IsAuthenticated()] # REGENCY/CITY class RegencyCityList(generics.ListCreateAPIView): queryset = models.RegencyCity.objects.all() serializer_class = serializers.RegencyCitySerializer filter_backends = [filters.SearchFilter, filters.OrderingFilter, DjangoFilterBackend] search_fields = ['name', 'code'] filterset_fields = ['name', 'code', 'province'] ordering_fields = '__all__' def get_serializer_class(self): serializer_class = self.serializer_class if self.request.method == 'GET': serializer_class = serializers.RegencyCityDetailSerializer return serializer_class def get_permissions(self): if self.request.method == 'POST': return [permissions.IsAdminUser()] return [permissions.IsAuthenticated()] class RegencyCityDetail(generics.RetrieveUpdateDestroyAPIView): queryset = models.RegencyCity.objects.all() serializer_class = serializers.RegencyCitySerializer def get_serializer_class(self): serializer_class = self.serializer_class if self.request.method == 'GET': serializer_class = serializers.RegencyCityDetailSerializer return serializer_class def get_permissions(self): if self.request.method == 'PUT' or self.request.method == 'DELETE': return [permissions.IsAdminUser()] return [permissions.IsAuthenticated()] # LOCATION class LocationList(generics.ListCreateAPIView): queryset = models.Location.objects.all() serializer_class = serializers.LocationSerializer filter_backends = [filters.SearchFilter, filters.OrderingFilter, DjangoFilterBackend] search_fields = ['name'] filterset_fields = ['name', 'totem_code', 'province', 'regency_city', 'active'] ordering_fields = '__all__' def get_queryset(self): queryset = models.Location.objects.all() lat = self.request.query_params.get('lat') lng = self.request.query_params.get('lng') radius = self.request.query_params.get('radius') if lat and lng: lat = float(lat) lng = float(lng) radius = float(radius) if radius else 50 # default 50m min_lat, max_lat, min_lng, max_lng = get_bounding_box(lat, lng, radius) logger.info(f"Filtering locations within radius {radius}m of point ({lat}, {lng})") # Step 1: bounding box filter queryset = queryset.filter( totem_latitude__range=(min_lat, max_lat), totem_longitude__range=(min_lng, max_lng) ) # Step 2: haversine formula distance_expr = ExpressionWrapper( 6371000 * 2 * ATan2( Sqrt( Power(Sin((Radians(F('totem_latitude')) - Radians(lat)) / 2), 2) + Cos(Radians(lat)) * Cos(Radians(F('totem_latitude'))) * Power(Sin((Radians(F('totem_longitude')) - Radians(lng)) / 2), 2) ), Sqrt(1 - ( Power(Sin((Radians(F('totem_latitude')) - Radians(lat)) / 2), 2) + Cos(Radians(lat)) * Cos(Radians(F('totem_latitude'))) * Power(Sin((Radians(F('totem_longitude')) - Radians(lng)) / 2), 2) )) ), output_field=FloatField() ) queryset = queryset.annotate( distance=distance_expr ).filter( distance__lte=radius ).order_by('distance') logger.info(f"Queryset after applying bounding box and distance filter: {queryset.query}") logger.info(f"Applied bounding box filter: lat between {min_lat} and {max_lat}, lng between {min_lng} and {max_lng}") logger.info(f"Found {queryset.count()} locations within radius {radius}m of point ({lat}, {lng})") return queryset def get_serializer_class(self): serializer_class = self.serializer_class if self.request.method == 'GET': serializer_class = serializers.LocationDetailSerializer return serializer_class def get_permissions(self): if self.request.method == 'POST': return [permissions.IsAdminUser()] return [permissions.IsAuthenticated()] class LocationDetail(generics.RetrieveUpdateDestroyAPIView): queryset = models.Location.objects.all() serializer_class = serializers.LocationSerializer def get_serializer_class(self): serializer_class = self.serializer_class if self.request.method == 'GET': serializer_class = serializers.LocationDetailSerializer return serializer_class def get_permissions(self): if self.request.method == 'PUT' or self.request.method == 'DELETE': return [permissions.IsAdminUser()] return [permissions.IsAuthenticated()] def get_bounding_box(lat, lng, radius_in_meters): earth_radius = 6371000 # meter lat_delta = (radius_in_meters / earth_radius) * (180 / math.pi) lng_delta = (radius_in_meters / earth_radius) * (180 / math.pi) / math.cos(lat * math.pi/180) return ( lat - lat_delta, lat + lat_delta, lng - lng_delta, lng + lng_delta )