From bc2f2d0e77d31a63581e4b5743e899cfa69b5d24 Mon Sep 17 00:00:00 2001 From: Bryon Lewis <61746913+BryonLewis@users.noreply.github.com> Date: Mon, 8 Apr 2024 14:12:26 -0400 Subject: [PATCH] Guano Metadata Extraction (#95) * initial metadata extraction * add models to recording * client side guano metadata * add fron-end tool for details * linting * batch uploading guano metadata * client linting * support for additional time formats --- bats_ai/api.py | 3 +- bats_ai/core/admin/recording.py | 13 ++ ...ted_species_recording_detector_and_more.py | 51 ++++++ bats_ai/core/models/recording.py | 13 ++ bats_ai/core/views/__init__.py | 2 + bats_ai/core/views/guanometadata.py | 88 ++++++++++ bats_ai/core/views/recording.py | 21 +++ bats_ai/settings.py | 4 + client/src/api/api.ts | 127 +++++++++++--- .../src/components/BatchRecordingElement.vue | 112 ++++++++++++- .../src/components/BatchUploadRecording.vue | 92 ++++++++-- client/src/components/RecordingInfoDialog.vue | 55 +----- .../src/components/RecordingInfoDisplay.vue | 99 +++++++++++ client/src/components/UploadRecording.vue | 158 ++++++++++++++++-- client/src/use/useUtils.ts | 33 ++++ client/src/views/Recordings.vue | 57 +++++-- scripts/guanoInfo.py | 28 ++++ setup.py | 1 + 18 files changed, 826 insertions(+), 131 deletions(-) create mode 100644 bats_ai/core/migrations/0010_recording_computed_species_recording_detector_and_more.py create mode 100644 bats_ai/core/views/guanometadata.py create mode 100644 client/src/components/RecordingInfoDisplay.vue create mode 100644 client/src/use/useUtils.ts create mode 100644 scripts/guanoInfo.py diff --git a/bats_ai/api.py b/bats_ai/api.py index 2ec2f83..88ab5cd 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 GRTSCellsRouter, RecordingRouter, SpeciesRouter +from bats_ai.core.views import GRTSCellsRouter, GuanoMetadataRouter, RecordingRouter, SpeciesRouter logger = logging.getLogger(__name__) @@ -27,3 +27,4 @@ def global_auth(request): api.add_router('/recording/', RecordingRouter) api.add_router('/species/', SpeciesRouter) api.add_router('/grts/', GRTSCellsRouter) +api.add_router('/guano/', GuanoMetadataRouter) diff --git a/bats_ai/core/admin/recording.py b/bats_ai/core/admin/recording.py index b968cf9..8cc217d 100644 --- a/bats_ai/core/admin/recording.py +++ b/bats_ai/core/admin/recording.py @@ -24,6 +24,13 @@ class RecordingAdmin(admin.ModelAdmin): 'recording_location', 'grts_cell_id', 'grts_cell', + 'site_name', + 'detector', + 'software', + 'species_list', + 'unusual_occurrences', + 'get_computed_species', + 'get_official_species', ] list_select_related = True # list_select_related = ['owner'] @@ -34,6 +41,12 @@ class RecordingAdmin(admin.ModelAdmin): autocomplete_fields = ['owner'] readonly_fields = ['created', 'modified'] + def get_official_species(self, instance): + return [species.species_code_6 for species in instance.official_species.all()] + + def get_computed_species(self, instance): + return [species.species_code_6 for species in instance.computed_species.all()] + @admin.display( description='Spectrogram', empty_value='Not computed', diff --git a/bats_ai/core/migrations/0010_recording_computed_species_recording_detector_and_more.py b/bats_ai/core/migrations/0010_recording_computed_species_recording_detector_and_more.py new file mode 100644 index 0000000..1819063 --- /dev/null +++ b/bats_ai/core/migrations/0010_recording_computed_species_recording_detector_and_more.py @@ -0,0 +1,51 @@ +# Generated by Django 4.1.13 on 2024-04-03 13:07 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ('core', '0009_annotations_type'), + ] + + operations = [ + migrations.AddField( + model_name='recording', + name='computed_species', + field=models.ManyToManyField( + related_name='recording_computed_species', to='core.species' + ), + ), + migrations.AddField( + model_name='recording', + name='detector', + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name='recording', + name='official_species', + field=models.ManyToManyField( + related_name='recording_official_species', to='core.species' + ), + ), + migrations.AddField( + model_name='recording', + name='site_name', + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name='recording', + name='software', + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name='recording', + name='species_list', + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name='recording', + name='unusual_occurrences', + field=models.TextField(blank=True, null=True), + ), + ] diff --git a/bats_ai/core/models/recording.py b/bats_ai/core/models/recording.py index 19cf559..6d81036 100644 --- a/bats_ai/core/models/recording.py +++ b/bats_ai/core/models/recording.py @@ -2,6 +2,8 @@ from django.contrib.gis.db import models from django_extensions.db.models import TimeStampedModel +from .species import Species + # TimeStampedModel also provides "created" and "modified" fields class Recording(TimeStampedModel, models.Model): @@ -16,6 +18,17 @@ class Recording(TimeStampedModel, models.Model): grts_cell_id = models.IntegerField(blank=True, null=True) grts_cell = models.IntegerField(blank=True, null=True) public = models.BooleanField(default=False) + software = models.TextField(blank=True, null=True) + detector = models.TextField(blank=True, null=True) + species_list = models.TextField(blank=True, null=True) + site_name = models.TextField(blank=True, null=True) + computed_species = models.ManyToManyField( + Species, related_name='recording_computed_species' + ) # species from a computed sense + official_species = models.ManyToManyField( + Species, related_name='recording_official_species' + ) # species that are detemrined by the owner or from annotations as official species list + unusual_occurrences = models.TextField(blank=True, null=True) @property def has_spectrogram(self): diff --git a/bats_ai/core/views/__init__.py b/bats_ai/core/views/__init__.py index f503fcc..90163e3 100644 --- a/bats_ai/core/views/__init__.py +++ b/bats_ai/core/views/__init__.py @@ -1,5 +1,6 @@ from .annotations import router as AnnotationRouter from .grts_cells import router as GRTSCellsRouter +from .guanometadata import router as GuanoMetadataRouter from .recording import router as RecordingRouter from .species import router as SpeciesRouter from .temporal_annotations import router as TemporalAnnotationRouter @@ -10,4 +11,5 @@ 'AnnotationRouter', 'TemporalAnnotationRouter', 'GRTSCellsRouter', + 'GuanoMetadataRouter', ] diff --git a/bats_ai/core/views/guanometadata.py b/bats_ai/core/views/guanometadata.py new file mode 100644 index 0000000..bf7e4ed --- /dev/null +++ b/bats_ai/core/views/guanometadata.py @@ -0,0 +1,88 @@ +from datetime import datetime +import logging + +from django.http import HttpRequest, JsonResponse +from guano import GuanoFile +from ninja import File, Schema +from ninja.files import UploadedFile +from ninja.pagination import RouterPaginated + +router = RouterPaginated() +logger = logging.getLogger(__name__) + + +class GuanoMetadataSchema(Schema): + nabat_grid_cell_grts_id: str | None = None + nabat_latitude: float | None = None + nabat_longitude: float | None = None + nabat_site_name: str | None = None + nabat_activation_start_time: datetime | None = None + nabat_activation_end_time: datetime | None = None + nabat_software_type: str | None = None + nabat_species_list: list[str] | None = None + nabat_comments: str | None = None + nabat_detector_type: str | None = None + nabat_unusual_occurrences: str | None = None + + +router = RouterPaginated() + + +@router.post('/') +def default_data( + request: HttpRequest, + audio_file: File[UploadedFile], +): + try: + # Read GUANO metadata from the file name provided + gfile = GuanoFile(audio_file.file.name) + + # Extract required NABat fields + nabat_fields = { + 'nabat_grid_cell_grts_id': gfile.get('NABat|Grid Cell GRTS ID', None), + 'nabat_latitude': (gfile.get('NABat|Latitude', None)), + 'nabat_longitude': (gfile.get('NABat|Longitude', None)), + 'nabat_site_name': gfile.get('NABat|Site Name', None), + } + + # Extract additional fields with conditionals + additional_fields = { + 'nabat_activation_start_time': parse_datetime( + gfile.get('NABat|Activation start time', None) + ) + if 'NABat|Activation start time' in gfile + else None, + 'nabat_activation_end_time': parse_datetime( + gfile.get('NABat|Activation end time', None) + ) + if 'NABat|Activation end time' in gfile + else None, + 'nabat_software_type': gfile.get('NABat|Software type', None), + 'nabat_species_list': gfile.get('NABat|Species List', '').split(','), + 'nabat_comments': gfile.get('NABat|Comments', None), + 'nabat_detector_type': gfile.get('NABat|Detector type', None), + 'nabat_unusual_occurrences': gfile.get('NABat|Unusual occurrences', ''), + } + + # Combine all extracted fields + metadata = {**nabat_fields, **additional_fields} + + return JsonResponse(metadata, safe=False) + + except Exception as e: + return JsonResponse({'error': str(e)}, status=500) + + +def parse_datetime(datetime_str): + if datetime_str: + try: + # Try parsing using the custom format + return datetime.strptime(datetime_str, '%Y%m%dT%H%M%S') + except ValueError: + try: + # Try parsing using ISO format + return datetime.fromisoformat(datetime_str) + except ValueError: + # If both formats fail, return None or handle the error accordingly + return None + return None diff --git a/bats_ai/core/views/recording.py b/bats_ai/core/views/recording.py index d0ed2fe..e3c5e2d 100644 --- a/bats_ai/core/views/recording.py +++ b/bats_ai/core/views/recording.py @@ -47,6 +47,11 @@ class RecordingUploadSchema(Schema): longitude: float = None gridCellId: int = None publicVal: bool = None + site_name: str = None + software: str = None + detector: str = None + species_list: str = None + unusual_occurrences: str = None class AnnotationSchema(Schema): @@ -109,7 +114,13 @@ def create_recording( recording_location=point, public=publicVal, comments=payload.comments, + detector=payload.detector, + software=payload.software, + site_name=payload.site_name, + species_list=payload.species_list, + unusual_occurrences=payload.unusual_occurrences, ) + recording.save() # Start generating recording as soon as created # this creates the spectrogram during the upload so it is available immediately afterwards @@ -142,6 +153,16 @@ def update_recording(request: HttpRequest, id: int, recording_data: RecordingUpl if recording_data.latitude and recording_data.longitude: point = Point(recording_data.longitude, recording_data.latitude) recording.recording_location = point + if recording_data.detector: + recording.detector = recording_data.detector + if recording_data.software: + recording.software = recording_data.software + if recording_data.site_name: + recording.site_name = recording_data.site_name + if recording_data.species_list: + recording.species_list = recording_data.species_list + if recording_data.unusual_occurrences: + recording.unusual_occurrences = recording_data.unusual_occurrences recording.save() diff --git a/bats_ai/settings.py b/bats_ai/settings.py index 11c29dd..f64c13e 100644 --- a/bats_ai/settings.py +++ b/bats_ai/settings.py @@ -21,6 +21,10 @@ class BatsAiMixin(ConfigMixin): BASE_DIR = Path(__file__).resolve(strict=True).parent.parent + FILE_UPLOAD_HANDLERS = [ + 'django.core.files.uploadhandler.TemporaryFileUploadHandler', + ] + @staticmethod def mutate_configuration(configuration: ComposedConfiguration) -> None: # Install local apps first, to ensure any overridden resources are found first diff --git a/client/src/api/api.ts b/client/src/api/api.ts index 38127c8..8754ee8 100644 --- a/client/src/api/api.ts +++ b/client/src/api/api.ts @@ -28,6 +28,11 @@ export interface Recording { userMadeAnnotations: boolean; userAnnotations: number; hasSpectrogram: boolean; + site_name?: string; + software?: string; + detector?: string; + species_list?: string; + unusual_occurrences?: string; } export interface AcousticFiles { @@ -127,58 +132,98 @@ export const axiosInstance = axios.create({ baseURL: import.meta.env.VUE_APP_API_ROOT as string, }); +export interface RecordingFileParameters { + name: string; + recorded_date: string; + recorded_time: string; + equipment: string; + comments: string; + location?: UploadLocation; + publicVal: boolean; + site_name?: string; + software?: string; + detector?: string; + species_list?: string; + unusual_occurrences?: string; -async function uploadRecordingFile(file: File, name: string, recorded_date: string, recorded_time: string, equipment: string, comments: string, publicVal = false, location: UploadLocation = null ) { +} + +async function uploadRecordingFile(file: File, params: RecordingFileParameters ) { 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) { - if (location.latitude && location.longitude) { - formData.append('latitude', location.latitude.toString()); - formData.append('longitude', location.longitude.toString()); + formData.append('name', params.name); + formData.append('recorded_date', params.recorded_date); + formData.append('recorded_time', params.recorded_time); + formData.append('equipment', params.equipment); + formData.append('comments', params.comments); + if (params.location) { + if (params.location.latitude && params.location.longitude) { + formData.append('latitude', params.location.latitude.toString()); + formData.append('longitude', params.location.longitude.toString()); } - if (location.gridCellId) { - formData.append('gridCellId', location.gridCellId.toString()); + if (params.location.gridCellId) { + formData.append('gridCellId', params.location.gridCellId.toString()); } } + if (params.software) { + formData.append('software', params.software); + } + if (params.detector) { + formData.append('detector', params.detector); + } + if (params.site_name) { + formData.append('site_name', params.site_name); + } + if (params.species_list) { + formData.append('species_list', params.species_list); + } + if (params.unusual_occurrences) { + formData.append('unusual_occurrences', params.unusual_occurrences); + } const recordingParams = { - name, - equipment, - comments, + name: params.name, + equipment: params.equipment, + comments: params.comments, + site_name: params.site_name, + software: params.software, + detector: params.detector, + species_list: params.species_list, + unusual_occurrences: params.unusual_occurrences }; const payloadBlob = new Blob([JSON.stringify(recordingParams)], { type: 'application/json' }); formData.append('payload', payloadBlob); await axiosInstance.post('/recording/', formData, { - params: { publicVal }, + params: { publicVal: !!params.publicVal }, headers: { 'Content-Type': 'multipart/form-data', } }); } - 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; + async function patchRecording(recordingId: number, params: RecordingFileParameters) { + const latitude = params.location ? params.location.latitude : undefined; + const longitude = params.location ? params.location.longitude : undefined; + const gridCellId = params.location ? params.location.gridCellId : undefined; await axiosInstance.patch(`/recording/${recordingId}`, { - name, - recorded_date, - recorded_time, - equipment, - comments, - publicVal, + name: params.name, + recorded_date: params.recorded_date, + recorded_time: params.recorded_time, + equipment: params.equipment, + comments: params.comments, + publicVal: !!params.publicVal, latitude, longitude, - gridCellId + gridCellId, + site_name: params.site_name, + software: params.software, + detector: params.detector, + species_list: params.species_list, + unusual_occurrences: params.unusual_occurrences, }, { headers: { @@ -271,6 +316,33 @@ async function getCellfromLocation(latitude: number, longitude: number) { return axiosInstance.get(`/grts/grid_cell_id`, {params: {latitude, longitude}}); } +interface GuanoMetadata { + nabat_grid_cell_grts_id?: string + nabat_latitude?: number + nabat_longitude?: number + nabat_site_name?: string + nabat_activation_start_time?: string + nabat_activation_end_time?: string + nabat_software_type?: string + nabat_species_list?: string[] + nabat_comments?: string + nabat_detector_type?: string + nabat_unusual_occurrences?: string + +} + +async function getGuanoMetadata(file: File): Promise { + const formData = new FormData(); + formData.append('audio_file', file); + const results = await axiosInstance.post('/guano/',formData, { + headers: { + 'Content-Type': 'multipart/form-data', + } + }); + return results.data; + +} + export { uploadRecordingFile, getRecordings, @@ -291,4 +363,5 @@ export { deleteTemporalAnnotation, getCellLocation, getCellfromLocation, + getGuanoMetadata, }; \ No newline at end of file diff --git a/client/src/components/BatchRecordingElement.vue b/client/src/components/BatchRecordingElement.vue index 1d4c047..ab5d7ac 100644 --- a/client/src/components/BatchRecordingElement.vue +++ b/client/src/components/BatchRecordingElement.vue @@ -1,9 +1,10 @@ diff --git a/client/src/components/RecordingInfoDisplay.vue b/client/src/components/RecordingInfoDisplay.vue new file mode 100644 index 0000000..6ee1c43 --- /dev/null +++ b/client/src/components/RecordingInfoDisplay.vue @@ -0,0 +1,99 @@ + + + diff --git a/client/src/components/UploadRecording.vue b/client/src/components/UploadRecording.vue index 86ede30..5b5c863 100644 --- a/client/src/components/UploadRecording.vue +++ b/client/src/components/UploadRecording.vue @@ -2,25 +2,24 @@ import { defineComponent, PropType, ref, Ref } from 'vue'; import { RecordingMimeTypes } from '../constants'; import useRequest from '../use/useRequest'; -import { UploadLocation, uploadRecordingFile, patchRecording, getCellLocation, getCellfromLocation } from '../api/api'; +import { UploadLocation, uploadRecordingFile, patchRecording, getCellLocation, getCellfromLocation, getGuanoMetadata, RecordingFileParameters } from '../api/api'; import MapLocation from './MapLocation.vue'; import { useDate } from 'vuetify/lib/framework.mjs'; +import { getCurrentTime, extractDateTimeComponents } from '../use/useUtils'; export interface EditingRecording { - id: number, - name: string, - date: string, - time: string, - equipment: string, - comments: string, + 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; + location?: { lat: number, lon: number }; + siteName?: string; + software?: string; + detector?: string; + speciesList?: string; + unusualOccurrences?: string; } export default defineComponent({ @@ -52,6 +51,12 @@ 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); + // Guano Metadata + const siteName = ref(props.editing?.siteName || ''); + const software = ref(props.editing?.software || ''); + const detector = ref(props.editing?.detector || ''); + const speciesList = ref(props.editing?.speciesList || ''); + const unusualOccurrences = ref(props.editing?.unusualOccurrences || ''); const autoFill = async (filename: string) => { const regexPattern = /^(\d+)_(.+)_(\d{8})_(\d{6})(?:_(.*))?$/; @@ -135,10 +140,63 @@ export default defineComponent({ } location['gridCellId'] = gridCellId.value; } - await uploadRecordingFile(file, name.value, recordedDate.value, recordedTime.value, equipment.value, comments.value, publicVal.value, location); + const fileUploadParams: RecordingFileParameters = { + name: name.value, + recorded_date: recordedDate.value, + recorded_time: recordedTime.value, + equipment: equipment.value, + comments: comments.value, + publicVal: publicVal.value, + location, + site_name: siteName.value, + software: software.value, + detector: detector.value, + species_list: speciesList.value, + unusual_occurrences: unusualOccurrences.value, + }; + + await uploadRecordingFile(file, fileUploadParams); emit('done'); }); + const getMetadata = async () => { + if (fileModel.value) { + const results = await getGuanoMetadata(fileModel.value); + if (results.nabat_site_name) { + siteName.value = results.nabat_site_name; + } + if (results.nabat_software_type) { + software.value = results.nabat_software_type; + } + if (results.nabat_detector_type) { + detector.value = results.nabat_detector_type; + } + if (results.nabat_species_list) { + speciesList.value = results.nabat_species_list.join(','); + } + if (results.nabat_unusual_occurrences) { + unusualOccurrences.value = results.nabat_unusual_occurrences; + } + // Finally we get the latitude/longitude or gridCell Id if it's available. + const startTime = results.nabat_activation_start_time; + const NaBatgridCellId = results.nabat_grid_cell_grts_id; + const NABatlatitude = results.nabat_latitude; + const NABatlongitude = results.nabat_longitude; + if (startTime) { + const {date, time} = extractDateTimeComponents(startTime); + recordedDate.value = date; + recordedTime.value = time; + } + if (NaBatgridCellId) { + gridCellId.value = parseInt(NaBatgridCellId); + } + if (NABatlatitude && NABatlongitude) { + latitude.value = NABatlatitude; + longitude.value = NABatlongitude; + } + } + }; + const handleSubmit = async () => { if (props.editing) { let location: UploadLocation = null; @@ -154,7 +212,22 @@ export default defineComponent({ } location['gridCellId'] = gridCellId.value; } - await patchRecording(props.editing.id, name.value, recordedDate.value, recordedTime.value, equipment.value, comments.value, publicVal.value, location); + const fileUploadParams: RecordingFileParameters = { + name: name.value, + recorded_date: recordedDate.value, + recorded_time: recordedTime.value, + equipment: equipment.value, + comments: comments.value, + publicVal: publicVal.value, + location, + site_name: siteName.value, + software: software.value, + detector: detector.value, + species_list: speciesList.value, + unusual_occurrences: unusualOccurrences.value, + }; + + await patchRecording(props.editing.id, fileUploadParams); emit('done'); } else { submit(); @@ -212,6 +285,12 @@ export default defineComponent({ publicVal, updateMap, recordedTime, + // Guano Metadata + siteName, + software, + detector, + speciesList, + unusualOccurrences, selectFile, readFile, handleSubmit, @@ -219,6 +298,7 @@ export default defineComponent({ setLocation, triggerUpdateMap, gridCellChanged, + getMetadata, dateAdapter, }; }, @@ -399,6 +479,50 @@ export default defineComponent({ + + Guano Metadata + + + + Get Guano Metadata + + + + + + + + + + + + + + + + + + + diff --git a/client/src/use/useUtils.ts b/client/src/use/useUtils.ts new file mode 100644 index 0000000..44d3942 --- /dev/null +++ b/client/src/use/useUtils.ts @@ -0,0 +1,33 @@ +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; + } +function extractDateTimeComponents(dateTimeString: string) { + const dateTime = new Date(dateTimeString); + + // Extracting date components + const year = dateTime.getFullYear(); + const month = String(dateTime.getMonth() + 1).padStart(2, '0'); + const day = String(dateTime.getDate()).padStart(2, '0'); + + // Forming date string in the format YYYY-MM-DD + const dateString = `${year}-${month}-${day}`; + + // Extracting time components + const hours = String(dateTime.getHours()).padStart(2, '0'); + const minutes = String(dateTime.getMinutes()).padStart(2, '0'); + const seconds = String(dateTime.getSeconds()).padStart(2, '0'); + + // Forming time string in the format HHMMSS + const timeString = `${hours}${minutes}${seconds}`; + + return { date: dateString, time: timeString }; + } + +export { + getCurrentTime, + extractDateTimeComponents, +}; diff --git a/client/src/views/Recordings.vue b/client/src/views/Recordings.vue index a71b5c7..42394e2 100644 --- a/client/src/views/Recordings.vue +++ b/client/src/views/Recordings.vue @@ -5,11 +5,13 @@ import UploadRecording, { EditingRecording } from '../components/UploadRecording import MapLocation from '../components/MapLocation.vue'; import useState from '../use/useState'; import BatchUploadRecording from '../components/BatchUploadRecording.vue'; +import RecordingInfoDisplay from '../components/RecordingInfoDisplay.vue'; export default defineComponent({ components: { UploadRecording, MapLocation, BatchUploadRecording, + RecordingInfoDisplay }, setup() { const itemsPerPage = ref(-1); @@ -55,12 +57,8 @@ export default defineComponent({ key:'recording_location' }, { - title:'Equipment', - key:'equipment', - }, - { - title:'Comments', - key:'comments', + title: 'Details', + key:'comments' }, { title:'Users Annotated', @@ -95,16 +93,13 @@ export default defineComponent({ }, { title: 'Location', - key:'recording_location' + key:'details' }, { - title:'Equipment', - key:'equipment', - }, - { - title:'Comments', - key:'comments', + title: 'Details', + key:'comments' }, + { title:'Annotated by Me', key:'userMadeAnnotations', @@ -278,6 +273,24 @@ export default defineComponent({ + + + +