Repository: ResonantGeoData/ResonantGeoData

Issue:
Time of day filter field
Add new acquisition date time of day filter.

Use case: find all rasters acquired between 8am and 4pm for all days in the acquired date range (i.e. only daytime imagery)


Discussion:
This would essentially be implemented by adding a new field on `SpatialEntryFilter` in `django-rgd/rgd/filters.py`

Hopefully we can use: https://django-filter.readthedocs.io/en/stable/ref/filters.html#timerangefilter
```py
    time_of_day = filters. TimeRangeFilter(
        help_text='The minimum/maximum times during the day the records were aqcuired.',
        label='Time of Day',
        method='filter_time_of_day',
    )
```

then implement the `filter_time_of_day` method. This would require annotating the times on the queryset then filter on the annotation. Something along the lines of 

```py
    def filter_time_of_day(self, queryset, name, value):
        if value is not None:
            queryset = queryset.annotate(
                time_of_day=F('acquisition_date__time')
            )
            if value.start is not None:
                queryset = queryset.filter(time_of_day__gte=value.start)
            if value.stop is not None:
                queryset = queryset.filter(time_of_day__lte=value.stop)
```

@venkatabhishek, could you help on this?

Resolving patch:
diff --git a/django-rgd/rgd/filters.py b/django-rgd/rgd/filters.py
--- a/django-rgd/rgd/filters.py
+++ b/django-rgd/rgd/filters.py
@@ -2,6 +2,7 @@
 from django.contrib.gis.db.models.functions import GeometryDistance
 from django.contrib.gis.measure import D
 from django.core.validators import RegexValidator
+from django.db.models import F
 from django_filters import rest_framework as filters
 from rgd.models import SpatialEntry
 
@@ -67,6 +68,12 @@ class SpatialEntryFilter(filters.FilterSet):
         lookup_expr='icontains',
     )
 
+    time_of_day = filters.TimeRangeFilter(
+        help_text='The minimum/maximum times during the day the records were aqcuired.',
+        label='Time of Day',
+        method='filter_time_of_day',
+    )
+
     @property
     def _geometry(self):
         return self.form.cleaned_data['q']
@@ -116,6 +123,20 @@ def filter_distance(self, queryset, name, value):
                 queryset = queryset.filter(footprint__distance_lte=(geom, D(m=value.stop)))
         return queryset
 
+    def filter_time_of_day(self, queryset, name, value):
+        """Filter the queryset by time of day acquired.
+
+        Use case: find all rasters acquired between 8am and 4pm
+        for all days in the acquired date range (i.e. only daytime imagery)
+        """
+        if value is not None:
+            queryset = queryset.annotate(time_of_day=F('acquisition_date__time'))
+            if value.start is not None:
+                queryset = queryset.filter(time_of_day__gte=value.start)
+            if value.stop is not None:
+                queryset = queryset.filter(time_of_day__lte=value.stop)
+        return queryset
+
     class Meta:
         model = SpatialEntry
         fields = [
@@ -125,4 +146,5 @@ class Meta:
             'distance',
             'acquired',
             'instrumentation',
+            'time_of_day',
         ]
diff --git a/rgd-client/rgd_client/rgdc.py b/rgd-client/rgd_client/rgdc.py
--- a/rgd-client/rgd_client/rgdc.py
+++ b/rgd-client/rgd_client/rgdc.py
@@ -206,6 +206,7 @@ def search(
         instrumentation: Optional[str] = None,
         limit: Optional[int] = None,
         offset: Optional[int] = None,
+        time_of_day: Optional[DATETIME_OR_STR_TUPLE] = None,
     ) -> List[Dict]:
         """
         Search for geospatial entries based on various criteria.
@@ -237,6 +238,7 @@ def search(
             instrumentation=instrumentation,
             limit=limit,
             offset=offset,
+            time_of_day=time_of_day,
         )
         return list(limit_offset_pager(self.session, 'rgd/search', params=params))
 
diff --git a/rgd-client/rgd_client/utils.py b/rgd-client/rgd_client/utils.py
--- a/rgd-client/rgd_client/utils.py
+++ b/rgd-client/rgd_client/utils.py
@@ -84,6 +84,23 @@ def datetime_to_str(value: object):
     return value
 
 
+def datetime_to_time(value: object):
+    """Convert datetime objects to HH:MM strings."""
+    if value is not None:
+        if isinstance(value, datetime):
+            return value.strftime('%H:%M')
+
+    return value
+
+
+def order_datetimes(value1: object, value2: object):
+    """Sort 2 objects if they are datetimes."""
+    if isinstance(value1, datetime) and isinstance(value2, datetime):
+        return value1, value2 if value1 < value2 else value2, value1
+
+    return value1, value2
+
+
 def download_checksum_file_to_path(file: Dict, path: Path, keep_existing: bool):
     """
     Download a RGD ChecksumFile to a given path.
@@ -138,6 +155,7 @@ def spatial_search_params(
     instrumentation: Optional[str] = None,
     limit: Optional[int] = None,
     offset: Optional[int] = None,
+    time_of_day: Optional[DATETIME_OR_STR_TUPLE] = None,
 ) -> Dict:
     # The dict that will be used to store params.
     # Initialize with queries that won't be additionally processed.
@@ -171,8 +189,13 @@ def spatial_search_params(
 
     # TODO: Determine if the before/after param order needs to be swapped?
     if acquired and len(acquired) == 2:
-        amin, amax = acquired
+        amin, amax = order_datetimes(*acquired)
         params['acquired_before'] = datetime_to_str(amax)
         params['acquired_after'] = datetime_to_str(amin)
 
+    if time_of_day and len(time_of_day) == 2:
+        after, before = order_datetimes(*time_of_day)
+        params['time_of_day_after'] = datetime_to_time(after)
+        params['time_of_day_before'] = datetime_to_time(before)
+
     return params


Test patch:
diff --git a/django-rgd/tests/test_filters.py b/django-rgd/tests/test_filters.py
--- a/django-rgd/tests/test_filters.py
+++ b/django-rgd/tests/test_filters.py
@@ -1,3 +1,5 @@
+from datetime import datetime
+
 import pytest
 from rgd import models
 from rgd.filters import SpatialEntryFilter
@@ -54,3 +56,17 @@ def test_geojson_intersects(spatial_asset_a, spatial_asset_b):
     qs = filterset.filter_queryset(models.SpatialEntry.objects.all())
     assert qs.count() == 1
     assert qs.first().spatial_id == spatial_asset_a.spatial_id
+
+
+@pytest.mark.django_db(transaction=True)
+def test_q_time(spatial_asset_a):
+    spatial_asset_a.acquisition_date = datetime(2021, 1, 1, hour=12, second=0)
+    spatial_asset_a.save()
+    filterset = SpatialEntryFilter(data={'time_of_day_before': '13:00'})
+    assert filterset.is_valid()
+    qs = filterset.filter_queryset(models.SpatialEntry.objects.all())
+    assert qs.count() == 1
+    filterset = SpatialEntryFilter(data={'time_of_day_after': '13:00'})
+    assert filterset.is_valid()
+    qs = filterset.filter_queryset(models.SpatialEntry.objects.all())
+    assert qs.count() == 0