From ce8369676848c117b2635b6c2cb41d50c113856d Mon Sep 17 00:00:00 2001 From: Bryon Lewis Date: Thu, 25 Jan 2024 08:19:29 -0500 Subject: [PATCH 1/6] add location support --- ...0005_alter_recording_recording_location.py | 19 ++++ bats_ai/core/models/recording.py | 2 +- bats_ai/core/views/recording.py | 15 ++- client/src/api/api.ts | 13 ++- client/src/components/UploadRecording.vue | 103 +++++++++++++++--- 5 files changed, 129 insertions(+), 23 deletions(-) create mode 100644 bats_ai/core/migrations/0005_alter_recording_recording_location.py diff --git a/bats_ai/core/migrations/0005_alter_recording_recording_location.py b/bats_ai/core/migrations/0005_alter_recording_recording_location.py new file mode 100644 index 0000000..0315b53 --- /dev/null +++ b/bats_ai/core/migrations/0005_alter_recording_recording_location.py @@ -0,0 +1,19 @@ +# Generated by Django 4.1.13 on 2024-01-25 13:09 + +import django.contrib.gis.db.models.fields +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0004_spectrogram'), + ] + + operations = [ + migrations.AlterField( + model_name='recording', + name='recording_location', + field=django.contrib.gis.db.models.fields.GeometryField(blank=True, null=True, srid=4326), + ), + ] diff --git a/bats_ai/core/models/recording.py b/bats_ai/core/models/recording.py index 28a2bd8..9037911 100644 --- a/bats_ai/core/models/recording.py +++ b/bats_ai/core/models/recording.py @@ -11,7 +11,7 @@ class Recording(TimeStampedModel, models.Model): recorded_date = models.DateField(blank=True, null=True) equipment = models.TextField(blank=True, null=True) comments = models.TextField(blank=True, null=True) - recording_location = models.GeometryField(srid=0, blank=True, null=True) + recording_location = models.GeometryField(srid=4326, blank=True, null=True) grts_cell_id = models.IntegerField(blank=True, null=True) grts_cell = models.IntegerField(blank=True, null=True) diff --git a/bats_ai/core/views/recording.py b/bats_ai/core/views/recording.py index ba91e1d..d47337d 100644 --- a/bats_ai/core/views/recording.py +++ b/bats_ai/core/views/recording.py @@ -1,7 +1,9 @@ from datetime import datetime +import json import logging from django.contrib.auth.models import User +from django.contrib.gis.geos import Point from django.core.files.storage import default_storage from django.http import HttpRequest from ninja import File, Form, Schema @@ -34,6 +36,9 @@ class RecordingUploadSchema(Schema): recorded_date: str equipment: str | None comments: str | None + latitude: float = None + longitude: float = None + gridCellId: int = None class AnnotationSchema(Schema): @@ -61,16 +66,20 @@ def create_recording( request: HttpRequest, payload: Form[RecordingUploadSchema], audio_file: File[UploadedFile] ): converted_date = datetime.strptime(payload.recorded_date, '%Y-%m-%d') + point = None + if payload.latitude and payload.longitude: + point = Point(payload.longitude, payload.latitude) recording = Recording( name=payload.name, owner_id=request.user.pk, audio_file=audio_file, recorded_date=converted_date, equipment=payload.equipment, + grts_cell_id=payload.gridCellId, + recording_location=point, comments=payload.comments, ) recording.save() - return {'message': 'Recording updated successfully', 'id': recording.pk} @@ -79,12 +88,14 @@ def get_recordings(request: HttpRequest): # Filter recordings based on the owner's id recordings = Recording.objects.filter(owner=request.user).values() + # TODO with larger dataset it may be better to do this in a queryset instead of python for recording in recordings: user = User.objects.get(id=recording['owner_id']) recording['owner_username'] = user.username recording['audio_file_presigned_url'] = default_storage.url(recording['audio_file']) + if recording['recording_location']: + recording['recording_location'] = json.loads(recording['recording_location'].json) - # Return the serialized data return list(recordings) diff --git a/client/src/api/api.ts b/client/src/api/api.ts index 41ac3b9..8714d48 100644 --- a/client/src/api/api.ts +++ b/client/src/api/api.ts @@ -81,20 +81,29 @@ interface PaginatedNinjaResponse { items: T[], } - +export type UploadLocation = null | { latitude?: number, longitude?: number, gridCellId?: number}; export const axiosInstance = axios.create({ baseURL: import.meta.env.VUE_APP_API_ROOT as string, }); -async function uploadRecordingFile(file: File, name: string, recorded_date: string, equipment: string, comments: string ) { +async function uploadRecordingFile(file: File, name: string, recorded_date: string, equipment: string, comments: string, location: UploadLocation = null ) { const formData = new FormData(); formData.append('audio_file', file); formData.append('name', name); formData.append('recorded_date', recorded_date); 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()); + } + if (location.gridCellId) { + formData.append('gridCellId', location.gridCellId.toString()); + } + } const recordingParams = { name, diff --git a/client/src/components/UploadRecording.vue b/client/src/components/UploadRecording.vue index 8311c75..974cf54 100644 --- a/client/src/components/UploadRecording.vue +++ b/client/src/components/UploadRecording.vue @@ -2,7 +2,7 @@ import { defineComponent, ref, Ref } from 'vue'; import { RecordingMimeTypes } from '../constants'; import useRequest from '../use/useRequest'; -import { uploadRecordingFile } from '../api/api'; +import { UploadLocation, uploadRecordingFile } from '../api/api'; import { VDatePicker } from 'vuetify/labs/VDatePicker'; export default defineComponent({ @@ -22,6 +22,9 @@ export default defineComponent({ const equipment = ref(''); const comments = ref(''); const validForm = ref(false); + const latitude: Ref = ref(); + const longitude: Ref = ref(); + const gridCellId: Ref = ref(); const readFile = (e: Event) => { const target = (e.target as HTMLInputElement); if (target?.files?.length) { @@ -47,7 +50,20 @@ export default defineComponent({ if (!file) { throw new Error('Unreachable'); } - await uploadRecordingFile(file, name.value, recordedDate.value, equipment.value, comments.value); + let location: UploadLocation = null; + if (latitude.value && longitude.value) { + location = { + latitude: latitude.value, + longitude: longitude.value, + }; + } + if (gridCellId.value !== null) { + if (location === null) { + location = {}; + } + location['gridCellId'] = gridCellId.value; + } + await uploadRecordingFile(file, name.value, recordedDate.value, equipment.value, comments.value, location); emit('done'); }); @@ -69,6 +85,9 @@ export default defineComponent({ comments, recordedDate, validForm, + latitude, + longitude, + gridCellId, selectFile, readFile, submit, @@ -100,6 +119,7 @@ export default defineComponent({ > @@ -157,24 +177,71 @@ export default defineComponent({ /> -

Recorded Date:

- -
- - + + + + - + + + Location + + + + + + + + + + + + Details + + + + + + + + + + From dbfae43da2c5ad42be4179dd33167c2b630d37dc Mon Sep 17 00:00:00 2001 From: Bryon Lewis Date: Thu, 25 Jan 2024 09:03:53 -0500 Subject: [PATCH 2/6] gridCellId script --- scripts/gridCell.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 scripts/gridCell.py diff --git a/scripts/gridCell.py b/scripts/gridCell.py new file mode 100644 index 0000000..8ce32d8 --- /dev/null +++ b/scripts/gridCell.py @@ -0,0 +1,42 @@ +import click +import geopandas as gpd +from shapely.geometry import Point, shape +import fiona +import json +import pyproj + + +# Global variable for the shapefile location +SHAPEFILE_PATH = './conus_mastersample_10km_attributed.shp' +target_crs = 'epsg:5070' +source_crs = 'epsg:4326' +# Load the shapefile using fiona +with fiona.open(SHAPEFILE_PATH, 'r') as shp: + + # Create a coordinate transformer + transformer = pyproj.Transformer.from_crs(source_crs, target_crs, always_xy=True) + geometries = [shape(feature['geometry']) for feature in shp] + + +def get_gridcell_id(latitude, longitude, geometries): + x, y = transformer.transform(longitude, latitude) + point = Point(x, y) + for index, geom in enumerate(geometries): + if point.within(geom): + print(f'Found index {index}') + return index # Returning index as an example, modify as needed + return None + +@click.command() +@click.argument('latitude', type=float) +@click.argument('longitude', type=float) +def main(latitude, longitude): + gridcell_id = get_gridcell_id(latitude, longitude, geometries) + + if gridcell_id is not None: + click.echo(f'The gridcell ID is: {gridcell_id}') + else: + click.echo('Point not found in any grid cell.') + +if __name__ == '__main__': + main() From f9ffb82ad3a6ce4fa834d610cf1d7604583585b4 Mon Sep 17 00:00:00 2001 From: Bryon Lewis Date: Fri, 2 Feb 2024 11:14:37 -0500 Subject: [PATCH 3/6] adding map component and update recording --- .gitignore | 2 + ...0005_alter_recording_recording_location.py | 5 +- bats_ai/core/views/recording.py | 3 + client/src/api/api.ts | 2 +- client/src/components/MapLocation.vue | 95 +++++++++++++++++++ client/src/components/UploadRecording.vue | 29 +++++- client/src/views/Recordings.vue | 64 ++++++++++++- scripts/gridCell.py | 32 ++++--- 8 files changed, 206 insertions(+), 26 deletions(-) create mode 100644 client/src/components/MapLocation.vue diff --git a/.gitignore b/.gitignore index e43b0f9..783b798 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ .DS_Store +/**/*.shp +/**/*.shx diff --git a/bats_ai/core/migrations/0005_alter_recording_recording_location.py b/bats_ai/core/migrations/0005_alter_recording_recording_location.py index 0315b53..1ba7db9 100644 --- a/bats_ai/core/migrations/0005_alter_recording_recording_location.py +++ b/bats_ai/core/migrations/0005_alter_recording_recording_location.py @@ -5,7 +5,6 @@ class Migration(migrations.Migration): - dependencies = [ ('core', '0004_spectrogram'), ] @@ -14,6 +13,8 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='recording', name='recording_location', - field=django.contrib.gis.db.models.fields.GeometryField(blank=True, null=True, srid=4326), + field=django.contrib.gis.db.models.fields.GeometryField( + blank=True, null=True, srid=4326 + ), ), ] diff --git a/bats_ai/core/views/recording.py b/bats_ai/core/views/recording.py index c78d1ff..4fc9323 100644 --- a/bats_ai/core/views/recording.py +++ b/bats_ai/core/views/recording.py @@ -120,6 +120,9 @@ def update_recording(request: HttpRequest, id: int, recording_data: RecordingUpl recording.recorded_date = converted_date 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: + point = Point(recording_data.longitude, recording_data.latitude) + recording.recording_location = point recording.save() diff --git a/client/src/api/api.ts b/client/src/api/api.ts index a6626bf..5a0be0c 100644 --- a/client/src/api/api.ts +++ b/client/src/api/api.ts @@ -20,7 +20,7 @@ export interface Recording { recorded_date: string; equipment?: string, comments?: string; - recording_location?: null | [number, number], + recording_location?: null | GeoJSON.Point, grts_cell_id?: null | number; grts_cell?: null | number; public: boolean; diff --git a/client/src/components/MapLocation.vue b/client/src/components/MapLocation.vue new file mode 100644 index 0000000..6d2b9c5 --- /dev/null +++ b/client/src/components/MapLocation.vue @@ -0,0 +1,95 @@ + + + + + + diff --git a/client/src/components/UploadRecording.vue b/client/src/components/UploadRecording.vue index cd70bef..a36e848 100644 --- a/client/src/components/UploadRecording.vue +++ b/client/src/components/UploadRecording.vue @@ -4,7 +4,7 @@ import { RecordingMimeTypes } from '../constants'; import useRequest from '../use/useRequest'; import { UploadLocation, uploadRecordingFile, patchRecording } from '../api/api'; import { VDatePicker } from 'vuetify/labs/VDatePicker'; - +import MapLocation from './MapLocation.vue'; export interface EditingRecording { id: number, name: string, @@ -12,10 +12,12 @@ export interface EditingRecording { equipment: string, comments: string, public: boolean; + location?: { lat: number, lon: number }, } export default defineComponent({ components: { VDatePicker, + MapLocation, }, props: { editing: { @@ -36,8 +38,8 @@ export default defineComponent({ const equipment = ref(props.editing ? props.editing.equipment : ''); const comments = ref(props.editing ? props.editing.comments : ''); const validForm = ref(false); - const latitude: Ref = ref(); - const longitude: Ref = ref(); + const latitude: Ref = ref(props.editing?.location?.lat ? props.editing.location.lat : undefined); + 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) => { @@ -110,6 +112,11 @@ export default defineComponent({ recordedDate.value = new Date(time as string).toISOString().split('T')[0]; }; + const setLocation = ({lat, lon}: {lat: number, lon: number}) => { + latitude.value = lat; + longitude.value = lon; + }; + return { errorText, fileModel, @@ -131,6 +138,7 @@ export default defineComponent({ readFile, handleSubmit, updateTime, + setLocation, }; }, }); @@ -250,24 +258,35 @@ export default defineComponent({ Location - + + + + + + diff --git a/client/src/views/Recordings.vue b/client/src/views/Recordings.vue index 13b0149..55e23fe 100644 --- a/client/src/views/Recordings.vue +++ b/client/src/views/Recordings.vue @@ -5,11 +5,13 @@ import { VDataTable, } from "vuetify/labs/VDataTable"; import UploadRecording, { EditingRecording } from '../components/UploadRecording.vue'; +import MapLocation from '../components/MapLocation.vue'; export default defineComponent({ components: { VDataTable, UploadRecording, + MapLocation, }, setup() { const itemsPerPage = ref(-1); @@ -40,7 +42,10 @@ export default defineComponent({ title:'Public', key:'public', }, - + { + title: 'Location', + key:'recording_location' + }, { title:'Equipment', key:'equipment', @@ -72,7 +77,10 @@ export default defineComponent({ title:'Public', key:'public', }, - + { + title: 'Location', + key:'recording_location' + }, { title:'Equipment', key:'equipment', @@ -110,6 +118,10 @@ export default defineComponent({ public: item.public, id: item.id, }; + if (item.recording_location) { + const [ lat, lon ] = item.recording_location.coordinates; + editingRecording.value['location'] = {lat, lon}; + } uploadDialog.value = true; }; @@ -165,6 +177,29 @@ export default defineComponent({ {{ item.raw.name }} + + + + @@ -282,7 +285,10 @@ export default defineComponent({ />
- + mdi-close