From ca8500dd0bfcb8a66c131b86daca5ee2eb3e6979 Mon Sep 17 00:00:00 2001 From: Bryon Lewis Date: Fri, 23 Feb 2024 12:30:35 -0500 Subject: [PATCH 1/4] metadata uploading and GRTS cells --- bats_ai/api.py | 3 +- bats_ai/core/admin/__init__.py | 2 + bats_ai/core/admin/grts_cells.py | 8 ++ bats_ai/core/admin/recording.py | 1 + .../management/commands/importGRTSCells.py | 57 +++++++++++++ .../0008_grtscells_recording_recorded_time.py | 48 +++++++++++ bats_ai/core/models/__init__.py | 2 + bats_ai/core/models/grts_cells.py | 52 ++++++++++++ bats_ai/core/models/recording.py | 1 + bats_ai/core/views/__init__.py | 2 + bats_ai/core/views/grts_cells.py | 83 +++++++++++++++++++ bats_ai/core/views/recording.py | 6 ++ client/src/api/api.ts | 26 +++++- client/src/components/MapLocation.vue | 2 +- client/src/components/UploadRecording.vue | 79 ++++++++++++++++-- client/src/views/Recordings.vue | 20 ++++- docker-compose.override.yml | 2 + 17 files changed, 384 insertions(+), 10 deletions(-) create mode 100644 bats_ai/core/admin/grts_cells.py create mode 100644 bats_ai/core/management/commands/importGRTSCells.py create mode 100644 bats_ai/core/migrations/0008_grtscells_recording_recorded_time.py create mode 100644 bats_ai/core/models/grts_cells.py create mode 100644 bats_ai/core/views/grts_cells.py diff --git a/bats_ai/api.py b/bats_ai/api.py index f82b7b7..2ec2f83 100644 --- a/bats_ai/api.py +++ b/bats_ai/api.py @@ -3,7 +3,7 @@ from ninja import NinjaAPI from oauth2_provider.models import AccessToken -from bats_ai.core.views import RecordingRouter, SpeciesRouter +from bats_ai.core.views import GRTSCellsRouter, RecordingRouter, SpeciesRouter logger = logging.getLogger(__name__) @@ -26,3 +26,4 @@ def global_auth(request): api.add_router('/recording/', RecordingRouter) api.add_router('/species/', SpeciesRouter) +api.add_router('/grts/', GRTSCellsRouter) diff --git a/bats_ai/core/admin/__init__.py b/bats_ai/core/admin/__init__.py index 23510a1..48b05ad 100644 --- a/bats_ai/core/admin/__init__.py +++ b/bats_ai/core/admin/__init__.py @@ -1,4 +1,5 @@ from .annotations import AnnotationsAdmin +from .grts_cells import GRTSCellsAdmin from .image import ImageAdmin from .recording import RecordingAdmin from .species import SpeciesAdmin @@ -12,4 +13,5 @@ 'SpectrogramAdmin', 'TemporalAnnotationsAdmin', 'SpeciesAdmin', + 'GRTSCellsAdmin', ] diff --git a/bats_ai/core/admin/grts_cells.py b/bats_ai/core/admin/grts_cells.py new file mode 100644 index 0000000..8ca38ab --- /dev/null +++ b/bats_ai/core/admin/grts_cells.py @@ -0,0 +1,8 @@ +from django.contrib import admin +from bats_ai.core.models import GRTSCells + +@admin.register(GRTSCells) +class GRTSCellsAdmin(admin.ModelAdmin): + list_display = ('id', 'grts_cell_id', 'sample_frame_id', 'water_p', 'outside_p') # Add other fields you want to display in the list + search_fields = ('id', 'grts_cell_id', 'sample_frame_id') # Add fields for searching + list_filter = ('location_1_type', 'location_2_type') # Add fields for filtering diff --git a/bats_ai/core/admin/recording.py b/bats_ai/core/admin/recording.py index 2255a43..b968cf9 100644 --- a/bats_ai/core/admin/recording.py +++ b/bats_ai/core/admin/recording.py @@ -17,6 +17,7 @@ class RecordingAdmin(admin.ModelAdmin): 'spectrogram_status', 'owner', 'recorded_date', + 'recorded_time', 'public', 'equipment', 'comments', diff --git a/bats_ai/core/management/commands/importGRTSCells.py b/bats_ai/core/management/commands/importGRTSCells.py new file mode 100644 index 0000000..b451c20 --- /dev/null +++ b/bats_ai/core/management/commands/importGRTSCells.py @@ -0,0 +1,57 @@ +import csv +from django.core.management.base import BaseCommand +from bats_ai.core.models import GRTSCells +from django.core.exceptions import ValidationError + + +class Command(BaseCommand): + help = 'Import data from CSV file' + + def add_arguments(self, parser): + parser.add_argument('csv_file', type=str, help='Path to the CSV file') + + def handle(self, *args, **options): + csv_file = options['csv_file'] + + # Get all field names of the GRTSCells model + model_fields = [field.name for field in GRTSCells._meta.get_fields()] + + with open(csv_file, 'r') as file: + reader = csv.DictReader(file) + total_rows = sum(1 for _ in reader) # Get total number of rows in the CSV + file.seek(0) # Reset file pointer to start + next(reader) # Skip header row + counter = 0 # Initialize progress counter + + for row in reader: + # Filter row dictionary to include only keys that exist in the model fields + filtered_row = {key: row[key] for key in row if key in model_fields} + + for key, value in filtered_row.items(): + if value == '': + filtered_row[key] = None + + # Convert boolean fields from string to boolean values + for boolean_field in ['priority_frame', 'priority_state', 'clipped']: + if filtered_row.get(boolean_field): + if filtered_row[boolean_field].lower() == 'true': + filtered_row[boolean_field] = True + elif filtered_row[boolean_field].lower() == 'false': + filtered_row[boolean_field] = False + else: + raise ValidationError(f'Invalid boolean value for field {boolean_field}: {filtered_row[boolean_field]}') + + # Check if a record with all the data already exists + if GRTSCells.objects.filter(**filtered_row).exists(): + #self.stdout.write(f'Skipping row because it already exists: {filtered_row}') + counter += 1 + self.stdout.write(f'Processed {counter} of {total_rows} rows') + continue + + try: + GRTSCells.objects.create(**filtered_row) + counter += 1 + self.stdout.write(f'Processed {counter} of {total_rows} rows') + except ValidationError as e: + self.stderr.write(str(e)) + continue diff --git a/bats_ai/core/migrations/0008_grtscells_recording_recorded_time.py b/bats_ai/core/migrations/0008_grtscells_recording_recorded_time.py new file mode 100644 index 0000000..b8d1376 --- /dev/null +++ b/bats_ai/core/migrations/0008_grtscells_recording_recorded_time.py @@ -0,0 +1,48 @@ +# Generated by Django 4.1.13 on 2024-02-23 17:06 + +import django.contrib.gis.db.models.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0007_temporalannotations'), + ] + + operations = [ + migrations.CreateModel( + name='GRTSCells', + fields=[ + ('id', models.IntegerField(primary_key=True, serialize=False)), + ('grts_cell_id', models.IntegerField()), + ('sample_frame_id', models.IntegerField(blank=True, null=True)), + ('grts_geom', django.contrib.gis.db.models.fields.GeometryField(blank=True, null=True, srid=4326)), + ('water_p', models.FloatField(blank=True, null=True)), + ('outside_p', models.FloatField(blank=True, null=True)), + ('location_1_type', models.CharField(blank=True, max_length=255, null=True)), + ('location_1_name', models.CharField(blank=True, max_length=255, null=True)), + ('location_1_p', models.FloatField(blank=True, null=True)), + ('location_2_type', models.CharField(blank=True, max_length=255, null=True)), + ('location_2_name', models.CharField(blank=True, max_length=255, null=True)), + ('location_2_p', models.FloatField(blank=True, null=True)), + ('sub_location_1_type', models.CharField(blank=True, max_length=255, null=True)), + ('sub_location_1_name', models.CharField(blank=True, max_length=255, null=True)), + ('sub_location_1_p', models.FloatField(blank=True, null=True)), + ('sub_location_2_type', models.CharField(blank=True, max_length=255, null=True)), + ('sub_location_2_name', models.CharField(blank=True, max_length=255, null=True)), + ('sub_location_2_p', models.FloatField(blank=True, null=True)), + ('own_1_name', models.CharField(blank=True, max_length=255, null=True)), + ('own_1_p', models.FloatField(blank=True, null=True)), + ('priority_frame', models.BooleanField(blank=True, null=True)), + ('priority_state', models.BooleanField(blank=True, null=True)), + ('geom_4326', django.contrib.gis.db.models.fields.GeometryField(srid=4326)), + ('clipped', models.BooleanField(blank=True, null=True)), + ], + ), + migrations.AddField( + model_name='recording', + name='recorded_time', + field=models.TimeField(blank=True, null=True), + ), + ] diff --git a/bats_ai/core/models/__init__.py b/bats_ai/core/models/__init__.py index cc8bf05..553e7cb 100644 --- a/bats_ai/core/models/__init__.py +++ b/bats_ai/core/models/__init__.py @@ -1,4 +1,5 @@ from .annotations import Annotations +from .grts_cells import GRTSCells from .image import Image from .recording import Recording from .recording_annotation_status import RecordingAnnotationStatus @@ -14,4 +15,5 @@ 'Species', 'Spectrogram', 'TemporalAnnotations', + 'GRTSCells', ] diff --git a/bats_ai/core/models/grts_cells.py b/bats_ai/core/models/grts_cells.py new file mode 100644 index 0000000..d9abc80 --- /dev/null +++ b/bats_ai/core/models/grts_cells.py @@ -0,0 +1,52 @@ +from django.contrib.gis.db import models + +sample_frame_map = { + 12: 'Mexico', + 14: 'Contintental US', + 19: 'Canada', + 20: 'Alaska', + 21: 'Puerto Rico', + 15: 'Hawaii', + 22: 'Offshore Caribbean', + 23: 'Offshore AKCAN', + 24: 'Offshore CONUS', + 25: 'Offshore Hawaii', + 26: 'Offshore Mexico', +} + +class GRTSCells(models.Model): + id = models.IntegerField(primary_key=True) + grts_cell_id = models.IntegerField() + sample_frame_id = models.IntegerField(blank=True, null=True) + grts_geom = models.GeometryField(blank=True, null=True) + water_p = models.FloatField(blank=True, null=True) + outside_p = models.FloatField(blank=True, null=True) + location_1_type = models.CharField(max_length=255, blank=True, null=True) + location_1_name = models.CharField(max_length=255, blank=True, null=True) + location_1_p = models.FloatField(blank=True, null=True) + location_2_type = models.CharField(max_length=255, blank=True, null=True) + location_2_name = models.CharField(max_length=255, blank=True, null=True) + location_2_p = models.FloatField(blank=True, null=True) + sub_location_1_type = models.CharField(max_length=255, blank=True, null=True) + sub_location_1_name = models.CharField(max_length=255, blank=True, null=True) + sub_location_1_p = models.FloatField(blank=True, null=True) + sub_location_2_type = models.CharField(max_length=255, blank=True, null=True) + sub_location_2_name = models.CharField(max_length=255, blank=True, null=True) + sub_location_2_p = models.FloatField(blank=True, null=True) + own_1_name = models.CharField(max_length=255, blank=True, null=True) + own_1_p = models.FloatField(blank=True, null=True) + # continue defining all fields similarly + priority_frame = models.BooleanField(blank=True, null=True) + priority_state = models.BooleanField(blank=True, null=True) + geom_4326 = models.GeometryField() + clipped = models.BooleanField(blank=True, null=True) + + @property + def sampleFrameMapping(self): + return sample_frame_map[self.sample_frame_id] + + @staticmethod + def sort_order(): + return [ + 14, 20, 15, 24, 21, 19, 12, 22, 23, 25, 26 + ] diff --git a/bats_ai/core/models/recording.py b/bats_ai/core/models/recording.py index 52af03e..19cf559 100644 --- a/bats_ai/core/models/recording.py +++ b/bats_ai/core/models/recording.py @@ -9,6 +9,7 @@ class Recording(TimeStampedModel, models.Model): audio_file = models.FileField() owner = models.ForeignKey(User, on_delete=models.CASCADE) recorded_date = models.DateField(blank=True, null=True) + recorded_time = models.TimeField(blank=True, null=True) equipment = models.TextField(blank=True, null=True) comments = models.TextField(blank=True, null=True) recording_location = models.GeometryField(srid=4326, blank=True, null=True) diff --git a/bats_ai/core/views/__init__.py b/bats_ai/core/views/__init__.py index ddf5efb..f503fcc 100644 --- a/bats_ai/core/views/__init__.py +++ b/bats_ai/core/views/__init__.py @@ -1,4 +1,5 @@ from .annotations import router as AnnotationRouter +from .grts_cells import router as GRTSCellsRouter from .recording import router as RecordingRouter from .species import router as SpeciesRouter from .temporal_annotations import router as TemporalAnnotationRouter @@ -8,4 +9,5 @@ 'SpeciesRouter', 'AnnotationRouter', 'TemporalAnnotationRouter', + 'GRTSCellsRouter', ] diff --git a/bats_ai/core/views/grts_cells.py b/bats_ai/core/views/grts_cells.py new file mode 100644 index 0000000..6c6c34e --- /dev/null +++ b/bats_ai/core/views/grts_cells.py @@ -0,0 +1,83 @@ +from django.http import JsonResponse +from bats_ai.core.models import GRTSCells +from django.http import HttpRequest +from ninja.pagination import RouterPaginated +from django.contrib.gis.geos import Polygon + +from django.contrib.gis.geos import Point +from django.contrib.gis.measure import D +from ninja import Query + +router = RouterPaginated() + +@router.get('/grid_cell_id') +def get_grid_cell_id(request: HttpRequest, latitude: float = Query(...), longitude: float = Query(...)): + try: + # Create a point object from the provided latitude and longitude + point = Point(longitude, latitude, srid=4326) + + # Query the grid cell that contains the provided point + cell = GRTSCells.objects.filter(geom_4326__contains=point).first() + + if cell: + # Return the grid cell ID + return JsonResponse({'grid_cell_id': cell.grts_cell_id}) + else: + return JsonResponse({'error': 'No grid cell found for the provided latitude and longitude'}, status=200) + except Exception as e: + return JsonResponse({'error': str(e)}, status=200) + + +@router.get('/{id}') +def get_cell_center(request: HttpRequest, id: int, quadrant: str = None): + try: + cells = GRTSCells.objects.filter(grts_cell_id=id) + + # Define a custom order for sample_frame_id + custom_order = GRTSCells.sort_order() # Define your custom order here + + # Define a custom key function to sort cells based on the custom order + def custom_sort_key(cell): + return custom_order.index(cell.sample_frame_id) + + # Sort the cells queryset based on the custom order + sorted_cells = sorted(cells, key=custom_sort_key) + cell = sorted_cells[0] + geom_4326 = cell.geom_4326 + + + # Get the centroid of the entire cell polygon + center = geom_4326.centroid + + if quadrant: + # If quadrant is specified, divide the cell polygon into quadrants + min_x, min_y, max_x, max_y = geom_4326.extent + mid_x = (min_x + max_x) / 2 + mid_y = (min_y + max_y) / 2 + + # Determine the bounding box coordinates of the specified quadrant + if quadrant.upper() == 'NW': + bbox = (min_x, mid_y, mid_x, max_y) + elif quadrant.upper() == 'SE': + bbox = (mid_x, min_y, max_x, mid_y) + elif quadrant.upper() == 'SW': + bbox = (min_x, min_y, mid_x, mid_y) + elif quadrant.upper() == 'NE': + bbox = (mid_x, mid_y, max_x, max_y) + + quadrant_polygon = Polygon.from_bbox(bbox) + + # Intersect the cell polygon with the specified quadrant's polygon + quadrant_polygon = geom_4326.intersection(quadrant_polygon) + + # Get the centroid of the intersected polygon + center = quadrant_polygon.centroid + + # Get the latitude and longitude of the centroid + center_latitude = center.y + center_longitude = center.x + + return JsonResponse({'latitude': center_latitude, 'longitude': center_longitude}) + except GRTSCells.DoesNotExist: + return JsonResponse({'error': f'Cell with cellId={id} does not exist'}, status=200) + diff --git a/bats_ai/core/views/recording.py b/bats_ai/core/views/recording.py index 39202c2..751a813 100644 --- a/bats_ai/core/views/recording.py +++ b/bats_ai/core/views/recording.py @@ -40,6 +40,7 @@ class RecordingSchema(Schema): class RecordingUploadSchema(Schema): name: str recorded_date: str + recorded_time: str equipment: str | None comments: str | None latitude: float = None @@ -90,6 +91,7 @@ def create_recording( publicVal: bool = False, ): converted_date = datetime.strptime(payload.recorded_date, '%Y-%m-%d') + converted_time = datetime.strptime(payload.recorded_time, '%H%M%S') point = None if payload.latitude and payload.longitude: point = Point(payload.longitude, payload.latitude) @@ -98,6 +100,7 @@ def create_recording( owner_id=request.user.pk, audio_file=audio_file, recorded_date=converted_date, + recorded_time=converted_time, equipment=payload.equipment, grts_cell_id=payload.gridCellId, recording_location=point, @@ -128,6 +131,9 @@ def update_recording(request: HttpRequest, id: int, recording_data: RecordingUpl if recording_data.recorded_date: converted_date = datetime.strptime(recording_data.recorded_date, '%Y-%m-%d') recording.recorded_date = converted_date + if recording_data.recorded_time: + converted_time = datetime.strptime(recording_data.recorded_time, '%H%M%S') + recording.recorded_time = converted_time if recording_data.publicVal is not None and recording_data.publicVal != recording.public: recording.public = recording_data.publicVal if recording_data.latitude and recording_data.longitude: diff --git a/client/src/api/api.ts b/client/src/api/api.ts index 177e8b9..f3628e1 100644 --- a/client/src/api/api.ts +++ b/client/src/api/api.ts @@ -18,6 +18,7 @@ export interface Recording { owner_id: number; owner_username: string; recorded_date: string; + recorded_time: string; equipment?: string, comments?: string; recording_location?: null | GeoJSON.Point, @@ -125,11 +126,12 @@ export const axiosInstance = axios.create({ }); -async function uploadRecordingFile(file: File, name: string, recorded_date: string, equipment: string, comments: string, publicVal = false, location: UploadLocation = null ) { +async function uploadRecordingFile(file: File, name: string, recorded_date: string, recorded_time: string, equipment: string, comments: string, publicVal = false, location: UploadLocation = null ) { const formData = new FormData(); formData.append('audio_file', file); formData.append('name', name); formData.append('recorded_date', recorded_date); + formData.append('recorded_time', recorded_time); formData.append('equipment', equipment); formData.append('comments', comments); if (location) { @@ -159,7 +161,7 @@ async function uploadRecordingFile(file: File, name: string, recorded_date: stri }); } - async function patchRecording(recordingId: number, name: string, recorded_date: string, equipment: string, comments: string, publicVal = false, location: UploadLocation = null ) { + async function patchRecording(recordingId: number, name: string, recorded_date: string, recorded_time: string, equipment: string, comments: string, publicVal = false, location: UploadLocation = null ) { const latitude = location ? location.latitude : undefined; const longitude = location ? location.longitude : undefined; const gridCellId = location ? location.gridCellId : undefined; @@ -168,6 +170,7 @@ async function uploadRecordingFile(file: File, name: string, recorded_date: stri { name, recorded_date, + recorded_time, equipment, comments, publicVal, @@ -187,6 +190,11 @@ interface DeletionResponse { message?: string; error?: string; } +interface GRTSCellCenter { + latitude?: number; + longitude?: number; + error?: string; +} async function getRecordings(getPublic=false) { return axiosInstance.get(`/recording/?public=${getPublic}`); @@ -246,6 +254,18 @@ async function getOtherUserAnnotations(recordingId: string) { return axiosInstance.get(`/recording/${recordingId}/annotations/other_users`); } +async function getCellLocation(cellId: number, quadrant?: 'SW' | 'NE' | 'NW' | 'SE') { + return axiosInstance.get(`/grts/${cellId}`, { params: { quadrant }}); +} + +interface CellIDReponse { + grid_cell_id?: number; + error?: string, +} +async function getCellfromLocation(latitude: number, longitude: number) { + return axiosInstance.get(`/grts/grid_cell_id`, {params: {latitude, longitude}}); +} + export { uploadRecordingFile, getRecordings, @@ -263,4 +283,6 @@ export { putTemporalAnnotation, deleteAnnotation, deleteTemporalAnnotation, + getCellLocation, + getCellfromLocation, }; \ No newline at end of file diff --git a/client/src/components/MapLocation.vue b/client/src/components/MapLocation.vue index d6e5408..dd08a96 100644 --- a/client/src/components/MapLocation.vue +++ b/client/src/components/MapLocation.vue @@ -83,7 +83,7 @@ export default defineComponent({ } }); watch(() => props.updateMap, () => { - if (props.location?.x && props.location?.y && markerLocation.value) { + if (props.location?.x && props.location?.y) { markerLocation.value = { x: props.location?.x, y: props.location.y }; markerFeature.value .data([markerLocation.value]) diff --git a/client/src/components/UploadRecording.vue b/client/src/components/UploadRecording.vue index 75bf020..81fe7ed 100644 --- a/client/src/components/UploadRecording.vue +++ b/client/src/components/UploadRecording.vue @@ -2,18 +2,27 @@ import { defineComponent, PropType, ref, Ref } from 'vue'; import { RecordingMimeTypes } from '../constants'; import useRequest from '../use/useRequest'; -import { UploadLocation, uploadRecordingFile, patchRecording } from '../api/api'; +import { UploadLocation, uploadRecordingFile, patchRecording, getCellLocation, getCellfromLocation } from '../api/api'; import { VDatePicker } from 'vuetify/labs/VDatePicker'; import MapLocation from './MapLocation.vue'; export interface EditingRecording { id: number, name: string, date: string, + time: string, equipment: string, comments: string, public: boolean; location?: { lat: number, lon: number }, } +function getCurrentTime() { + const now = new Date(); + const hours = String(now.getHours()).padStart(2, '0'); + const minutes = String(now.getMinutes()).padStart(2, '0'); + const seconds = String(now.getSeconds()).padStart(2, '0'); + return hours + minutes + seconds; +} + export default defineComponent({ components: { VDatePicker, @@ -33,6 +42,7 @@ export default defineComponent({ const errorText = ref(''); const progressState = ref(''); const recordedDate = ref(props.editing ? props.editing.date : new Date().toISOString().split('T')[0]); // YYYY-MM-DD Time + const recordedTime = ref(props.editing ? props.editing.time : getCurrentTime()); // HHMMSS const uploadProgress = ref(0); const name = ref(props.editing ? props.editing.name : ''); const equipment = ref(props.editing ? props.editing.equipment : ''); @@ -42,13 +52,44 @@ export default defineComponent({ const longitude: Ref = ref(props.editing?.location?.lon ? props.editing.location.lon : undefined); const gridCellId: Ref = ref(); const publicVal = ref(props.editing ? props.editing.public : false); - const readFile = (e: Event) => { + const autoFill = async (filename: string) => { + const parts = filename.split("_"); + + // Extracting individual components + const cellId = parts[0]; + const quadrant = parts[1]; + const date = parts[2]; + const timestamp = parts[3]; + if (cellId) { + gridCellId.value = parseInt(cellId, 10); + let updatedQuadrant; + if (['SW', 'NE', 'NW', 'SE'].includes(quadrant)) { + updatedQuadrant = quadrant as 'SW' | 'NE' | 'NW' | 'SE' | undefined; + } + const { latitude: lat , longitude: lon } = (await getCellLocation(gridCellId.value, updatedQuadrant)).data; + if (lat && lon) { + latitude.value = lat; + longitude.value = lon; + } + // Next we get the latitude longitude for this sell Id and quadarnt + } + if (date && date.length === 8) { + // We convert it to the YYYY-MM-DD time; + recordedDate.value = `${date.slice(0, 4)}-${date.slice(4, 6)}-${date.slice(6,8)}`; + } + if (timestamp) { + recordedTime.value = timestamp; + } + }; + const readFile = async (e: Event) => { const target = (e.target as HTMLInputElement); if (target?.files?.length) { const file = target.files.item(0); if (!file) { return; } + name.value = file.name.replace(/\.[^/.]+$/, ""); + await autoFill(name.value); if (!RecordingMimeTypes.includes(file.type)) { errorText.value = `Selected file is not one of the following types: ${RecordingMimeTypes.join(' ')}`; return; @@ -81,7 +122,7 @@ export default defineComponent({ } location['gridCellId'] = gridCellId.value; } - await uploadRecordingFile(file, name.value, recordedDate.value, equipment.value, comments.value, publicVal.value, location); + await uploadRecordingFile(file, name.value, recordedDate.value, recordedTime.value, equipment.value, comments.value, publicVal.value, location); emit('done'); }); @@ -100,7 +141,7 @@ export default defineComponent({ } location['gridCellId'] = gridCellId.value; } - await patchRecording(props.editing.id, name.value, recordedDate.value, equipment.value, comments.value, publicVal.value, location); + await patchRecording(props.editing.id, name.value, recordedDate.value, recordedTime.value, equipment.value, comments.value, publicVal.value, location); emit('done'); } else { submit(); @@ -112,9 +153,27 @@ export default defineComponent({ recordedDate.value = new Date(time as string).toISOString().split('T')[0]; }; - const setLocation = ({lat, lon}: {lat: number, lon: number}) => { + const setLocation = async ({lat, lon}: {lat: number, lon: number}) => { latitude.value = lat; longitude.value = lon; + const result = await getCellfromLocation(lat, lon); + if (result.data.grid_cell_id) { + gridCellId.value = result.data.grid_cell_id; + } else if (result.data.error) { + gridCellId.value = undefined; + } + }; + + const gridCellChanged = async () => { + if (gridCellId.value) { + const result = await getCellLocation(gridCellId.value); + if (result.data.latitude && result.data.longitude) { + latitude.value = result.data.latitude; + longitude.value = result.data.longitude; + triggerUpdateMap(); + + } + } }; const updateMap = ref(0); // updates the map when lat/lon change by editing directly; @@ -139,12 +198,14 @@ export default defineComponent({ gridCellId, publicVal, updateMap, + recordedTime, selectFile, readFile, handleSubmit, updateTime, setLocation, triggerUpdateMap, + gridCellChanged, }; }, }); @@ -258,6 +319,13 @@ export default defineComponent({ @update:model-value="updateTime($event)" /> + + @@ -285,6 +353,7 @@ export default defineComponent({ v-model="gridCellId" type="number" label="NABat Grid Cell" + @change="gridCellChanged()" /> diff --git a/client/src/views/Recordings.vue b/client/src/views/Recordings.vue index 8a8a344..b6749fe 100644 --- a/client/src/views/Recordings.vue +++ b/client/src/views/Recordings.vue @@ -39,10 +39,19 @@ export default defineComponent({ title:'Recorded Date', key:'recorded_date', }, + { + title:'Recorded Time', + key:'recorded_time', + }, { title:'Public', key:'public', }, + { + title:'GRTS CellId', + key:'grts_cell_id', + }, + { title: 'Location', key:'recording_location' @@ -74,10 +83,18 @@ export default defineComponent({ title:'Recorded Date', key:'recorded_date', }, + { + title:'Recorded Time', + key:'recorded_time', + }, { title:'Public', key:'public', }, + { + title:'GRTS CellId', + key:'grts_cell_id', + }, { title: 'Location', key:'recording_location' @@ -133,6 +150,7 @@ export default defineComponent({ equipment: item.equipment || '', comments: item.comments || '', date: item.recorded_date, + time: item.recorded_time, public: item.public, id: item.id, }; @@ -227,7 +245,7 @@ export default defineComponent({ Date: Fri, 23 Feb 2024 12:45:05 -0500 Subject: [PATCH 2/4] linting fix --- bats_ai/core/admin/grts_cells.py | 10 ++++++- .../management/commands/importGRTSCells.py | 16 +++++++---- .../0008_grtscells_recording_recorded_time.py | 8 ++++-- bats_ai/core/models/grts_cells.py | 5 ++-- bats_ai/core/views/grts_cells.py | 28 +++++++++---------- 5 files changed, 41 insertions(+), 26 deletions(-) diff --git a/bats_ai/core/admin/grts_cells.py b/bats_ai/core/admin/grts_cells.py index 8ca38ab..b4d6ee9 100644 --- a/bats_ai/core/admin/grts_cells.py +++ b/bats_ai/core/admin/grts_cells.py @@ -1,8 +1,16 @@ from django.contrib import admin + from bats_ai.core.models import GRTSCells + @admin.register(GRTSCells) class GRTSCellsAdmin(admin.ModelAdmin): - list_display = ('id', 'grts_cell_id', 'sample_frame_id', 'water_p', 'outside_p') # Add other fields you want to display in the list + list_display = ( + 'id', + 'grts_cell_id', + 'sample_frame_id', + 'water_p', + 'outside_p', + ) # Add other fields you want to display in the list search_fields = ('id', 'grts_cell_id', 'sample_frame_id') # Add fields for searching list_filter = ('location_1_type', 'location_2_type') # Add fields for filtering diff --git a/bats_ai/core/management/commands/importGRTSCells.py b/bats_ai/core/management/commands/importGRTSCells.py index b451c20..4a2da81 100644 --- a/bats_ai/core/management/commands/importGRTSCells.py +++ b/bats_ai/core/management/commands/importGRTSCells.py @@ -1,7 +1,9 @@ import csv + +from django.core.exceptions import ValidationError from django.core.management.base import BaseCommand + from bats_ai.core.models import GRTSCells -from django.core.exceptions import ValidationError class Command(BaseCommand): @@ -16,7 +18,7 @@ def handle(self, *args, **options): # Get all field names of the GRTSCells model model_fields = [field.name for field in GRTSCells._meta.get_fields()] - with open(csv_file, 'r') as file: + with open(csv_file) as file: reader = csv.DictReader(file) total_rows = sum(1 for _ in reader) # Get total number of rows in the CSV file.seek(0) # Reset file pointer to start @@ -30,7 +32,7 @@ def handle(self, *args, **options): for key, value in filtered_row.items(): if value == '': filtered_row[key] = None - + # Convert boolean fields from string to boolean values for boolean_field in ['priority_frame', 'priority_state', 'clipped']: if filtered_row.get(boolean_field): @@ -39,16 +41,18 @@ def handle(self, *args, **options): elif filtered_row[boolean_field].lower() == 'false': filtered_row[boolean_field] = False else: - raise ValidationError(f'Invalid boolean value for field {boolean_field}: {filtered_row[boolean_field]}') + raise ValidationError( + f'Invalid boolean value for field {boolean_field}: {filtered_row[boolean_field]}' + ) # Check if a record with all the data already exists if GRTSCells.objects.filter(**filtered_row).exists(): - #self.stdout.write(f'Skipping row because it already exists: {filtered_row}') + # self.stdout.write(f'Skipping row because it already exists: {filtered_row}') counter += 1 self.stdout.write(f'Processed {counter} of {total_rows} rows') continue - try: + try: GRTSCells.objects.create(**filtered_row) counter += 1 self.stdout.write(f'Processed {counter} of {total_rows} rows') diff --git a/bats_ai/core/migrations/0008_grtscells_recording_recorded_time.py b/bats_ai/core/migrations/0008_grtscells_recording_recorded_time.py index b8d1376..9fc1f95 100644 --- a/bats_ai/core/migrations/0008_grtscells_recording_recorded_time.py +++ b/bats_ai/core/migrations/0008_grtscells_recording_recorded_time.py @@ -5,7 +5,6 @@ class Migration(migrations.Migration): - dependencies = [ ('core', '0007_temporalannotations'), ] @@ -17,7 +16,12 @@ class Migration(migrations.Migration): ('id', models.IntegerField(primary_key=True, serialize=False)), ('grts_cell_id', models.IntegerField()), ('sample_frame_id', models.IntegerField(blank=True, null=True)), - ('grts_geom', django.contrib.gis.db.models.fields.GeometryField(blank=True, null=True, srid=4326)), + ( + 'grts_geom', + django.contrib.gis.db.models.fields.GeometryField( + blank=True, null=True, srid=4326 + ), + ), ('water_p', models.FloatField(blank=True, null=True)), ('outside_p', models.FloatField(blank=True, null=True)), ('location_1_type', models.CharField(blank=True, max_length=255, null=True)), diff --git a/bats_ai/core/models/grts_cells.py b/bats_ai/core/models/grts_cells.py index d9abc80..90ba95d 100644 --- a/bats_ai/core/models/grts_cells.py +++ b/bats_ai/core/models/grts_cells.py @@ -14,6 +14,7 @@ 26: 'Offshore Mexico', } + class GRTSCells(models.Model): id = models.IntegerField(primary_key=True) grts_cell_id = models.IntegerField() @@ -47,6 +48,4 @@ def sampleFrameMapping(self): @staticmethod def sort_order(): - return [ - 14, 20, 15, 24, 21, 19, 12, 22, 23, 25, 26 - ] + return [14, 20, 15, 24, 21, 19, 12, 22, 23, 25, 26] diff --git a/bats_ai/core/views/grts_cells.py b/bats_ai/core/views/grts_cells.py index 6c6c34e..95a3c07 100644 --- a/bats_ai/core/views/grts_cells.py +++ b/bats_ai/core/views/grts_cells.py @@ -1,17 +1,17 @@ -from django.http import JsonResponse -from bats_ai.core.models import GRTSCells -from django.http import HttpRequest +from django.contrib.gis.geos import Point, Polygon +from django.http import HttpRequest, JsonResponse +from ninja import Query from ninja.pagination import RouterPaginated -from django.contrib.gis.geos import Polygon -from django.contrib.gis.geos import Point -from django.contrib.gis.measure import D -from ninja import Query +from bats_ai.core.models import GRTSCells router = RouterPaginated() + @router.get('/grid_cell_id') -def get_grid_cell_id(request: HttpRequest, latitude: float = Query(...), longitude: float = Query(...)): +def get_grid_cell_id( + request: HttpRequest, latitude: float = Query(...), longitude: float = Query(...) # noqa: B008 +): try: # Create a point object from the provided latitude and longitude point = Point(longitude, latitude, srid=4326) @@ -23,7 +23,9 @@ def get_grid_cell_id(request: HttpRequest, latitude: float = Query(...), longitu # Return the grid cell ID return JsonResponse({'grid_cell_id': cell.grts_cell_id}) else: - return JsonResponse({'error': 'No grid cell found for the provided latitude and longitude'}, status=200) + return JsonResponse( + {'error': 'No grid cell found for the provided latitude and longitude'}, status=200 + ) except Exception as e: return JsonResponse({'error': str(e)}, status=200) @@ -32,20 +34,19 @@ def get_grid_cell_id(request: HttpRequest, latitude: float = Query(...), longitu def get_cell_center(request: HttpRequest, id: int, quadrant: str = None): try: cells = GRTSCells.objects.filter(grts_cell_id=id) - + # Define a custom order for sample_frame_id custom_order = GRTSCells.sort_order() # Define your custom order here - + # Define a custom key function to sort cells based on the custom order def custom_sort_key(cell): return custom_order.index(cell.sample_frame_id) - + # Sort the cells queryset based on the custom order sorted_cells = sorted(cells, key=custom_sort_key) cell = sorted_cells[0] geom_4326 = cell.geom_4326 - # Get the centroid of the entire cell polygon center = geom_4326.centroid @@ -80,4 +81,3 @@ def custom_sort_key(cell): return JsonResponse({'latitude': center_latitude, 'longitude': center_longitude}) except GRTSCells.DoesNotExist: return JsonResponse({'error': f'Cell with cellId={id} does not exist'}, status=200) - From ca64db77f085c68a988f89ae900fe7b07cee0a87 Mon Sep 17 00:00:00 2001 From: Bryon Lewis Date: Sat, 24 Feb 2024 08:49:27 -0500 Subject: [PATCH 3/4] update instructions --- .gitignore | 1 + DEPLOYMENT.md | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 783b798..78d3c39 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .DS_Store /**/*.shp /**/*.shx +/**/*.csv diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index 1fe1162..a18ffec 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -41,6 +41,14 @@ for accessing the server. and change the ApplicationId to the ID in the `./client.env.production` 10. Test logging in/out and uploading data to the server. +### GRTS Cell Id suppoer + +Make sure that there is the grts.csv in the /opt/batai/dev/grtsCells folder + +Then run `docker compose run --rm django ./manage.py importGRTSCells /app/csv/grts.csv` + +It may take a few minutes to upload because it is loading around 500k rows into the DB. + ### system.d service Service that will automatically start and launch the server @@ -60,7 +68,7 @@ User=bryon Group=docker TimeoutStartSec=300 RestartSec=20 -WorkingDirectory=/home/bryon/batai +WorkingDirectory=/opt/batai # Shutdown container (if running) when unit is started ExecStartPre=docker compose down # Start container when unit is started From 268dd2a8373fc15dc2b57547d7e69cbf9517fa73 Mon Sep 17 00:00:00 2001 From: Bryon Lewis Date: Sat, 24 Feb 2024 08:51:14 -0500 Subject: [PATCH 4/4] doc linting --- DEPLOYMENT.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index a18ffec..42c716d 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -47,7 +47,8 @@ Make sure that there is the grts.csv in the /opt/batai/dev/grtsCells folder Then run `docker compose run --rm django ./manage.py importGRTSCells /app/csv/grts.csv` -It may take a few minutes to upload because it is loading around 500k rows into the DB. +It may take a few minutes to upload because it is loading +around 500k rows into the DB. ### system.d service