Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Location Support #23

Merged
merged 7 commits into from
Feb 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
.DS_Store
/**/*.shp
/**/*.shx
20 changes: 20 additions & 0 deletions bats_ai/core/migrations/0006_alter_recording_recording_location.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Generated by Django 4.1.13 on 2024-02-02 16:19

import django.contrib.gis.db.models.fields
from django.db import migrations


class Migration(migrations.Migration):
dependencies = [
('core', '0005_recording_public'),
]

operations = [
migrations.AlterField(
model_name='recording',
name='recording_location',
field=django.contrib.gis.db.models.fields.GeometryField(
blank=True, null=True, srid=4326
),
),
]
2 changes: 1 addition & 1 deletion bats_ai/core/models/recording.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
public = models.BooleanField(default=False)
Expand Down
22 changes: 18 additions & 4 deletions bats_ai/core/views/recording.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -32,8 +34,11 @@ class RecordingSchema(Schema):
class RecordingUploadSchema(Schema):
name: str
recorded_date: str
equipment: str = None
comments: str = None
equipment: str | None
comments: str | None
latitude: float = None
longitude: float = None
gridCellId: int = None
publicVal: bool = None


Expand Down Expand Up @@ -79,17 +84,21 @@ def create_recording(
publicVal: bool = False,
):
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,
public=publicVal,
comments=payload.comments,
)
recording.save()

return {'message': 'Recording updated successfully', 'id': recording.pk}


Expand All @@ -111,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()

Expand All @@ -125,10 +137,13 @@ def get_recordings(request: HttpRequest, public: bool | None = None):
else:
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)
unique_users_with_annotations = (
Annotations.objects.filter(recording_id=recording['id'])
.values('owner')
Expand All @@ -141,7 +156,6 @@ def get_recordings(request: HttpRequest, public: bool | None = None):
).exists()
recording['userMadeAnnotations'] = user_has_annotations

# Return the serialized data
return list(recordings)


Expand Down
26 changes: 21 additions & 5 deletions client/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
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;
Expand Down Expand Up @@ -89,25 +89,34 @@

export type OtherUserAnnotations = Record<string, SpectrogramAnnotation[]>;

interface PaginatedNinjaResponse<T> {

Check warning on line 92 in client/src/api/api.ts

View workflow job for this annotation

GitHub Actions / Lint [eslint]

'PaginatedNinjaResponse' is defined but never used
count: number,
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, publicVal = false ) {
async function uploadRecordingFile(file: File, name: string, recorded_date: 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('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,
Expand All @@ -126,14 +135,21 @@
});
}

async function patchRecording(recordingId: number, name: string, recorded_date: string, equipment: string, comments: string, publicVal = false ) {
async function patchRecording(recordingId: number, name: string, recorded_date: 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;

await axiosInstance.patch(`/recording/${recordingId}`,
{
name,
recorded_date,
equipment,
comments,
publicVal
publicVal,
latitude,
longitude,
gridCellId
},
{
headers: {
Expand Down
124 changes: 124 additions & 0 deletions client/src/components/MapLocation.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
<!-- eslint-disable @typescript-eslint/no-explicit-any -->
<script lang="ts">
import { defineComponent, PropType, Ref, ref } from "vue";
import useState from "../use/useState";

Check warning on line 4 in client/src/components/MapLocation.vue

View workflow job for this annotation

GitHub Actions / Lint [eslint]

'useState' is defined but never used
import { watch } from "vue";
import geo, { GeoEvent } from "geojs";

export default defineComponent({
name: "MapLocation",
components: {},
props: {
editor: {
type: Boolean,
default: true,
},
size: {
type: Object as PropType<{ width: number; height: number }>,
default: () => ({ width: 300, height: 300 }),
},
location: {
type: Object as PropType<{ x?: number; y?: number } | undefined>,
default: () => undefined,
},
updateMap: {
type: Number,
default: 0,
},
},
emits: ['location'],
setup(props, { emit }) {
const usCenter = { x: -90.5855, y: 39.8333 };
const mapRef: Ref<HTMLDivElement | null> = ref(null);
const map: Ref<any> = ref();
const mapLayer: Ref<any> = ref();
const markerLayer: Ref<any> = ref();
const markerFeature: Ref<any> = ref();
const markerLocation: Ref<{ x: number; y: number } | null> = ref(null);
watch(mapRef, () => {
if (mapRef.value) {
const centerPoint = props.location && props.location.x && props.location.y ? props.location : usCenter;
const zoomLevel = props.location && props.location.x && props.location.y ? 6 : 3;
map.value = geo.map({ node: mapRef.value, center: centerPoint, zoom: zoomLevel });
mapLayer.value = map.value.createLayer("osm");
markerLayer.value = map.value.createLayer("feature", { features: ["marker"] });
markerFeature.value = markerLayer.value.createFeature("marker");
if (props.location?.x && props.location?.y) {
markerLocation.value = { x: props.location?.x, y: props.location.y };
markerFeature.value
.data([markerLocation.value])
.style({
symbol: geo.markerFeature.symbols.drop,
symbolValue: 1 / 3,
rotation: -Math.PI / 2,
radius: 30,
strokeWidth: 5,
strokeColor: "blue",
fillColor: "yellow",
rotateWithMap: false,
})
.draw();
}
if (props.editor) {
mapLayer.value.geoOn(geo.event.mouseclick, (e: GeoEvent) => {
// Place a marker at the point
const { x, y } = e.geo;
markerLocation.value = { x, y };
markerFeature.value
.data([markerLocation.value])
.style({
symbol: geo.markerFeature.symbols.drop,
symbolValue: 1 / 3,
rotation: -Math.PI / 2,
radius: 30,
strokeWidth: 5,
strokeColor: "blue",
fillColor: "yellow",
rotateWithMap: false,
})
.draw();
emit('location', { lon: x, lat:y });

});
}
}
});
watch(() => props.updateMap, () => {
if (props.location?.x && props.location?.y && markerLocation.value) {
markerLocation.value = { x: props.location?.x, y: props.location.y };
markerFeature.value
.data([markerLocation.value])
.style({
symbol: geo.markerFeature.symbols.drop,
symbolValue: 1 / 3,
rotation: -Math.PI / 2,
radius: 30,
strokeWidth: 5,
strokeColor: "blue",
fillColor: "yellow",
rotateWithMap: false,
})
.draw();
const centerPoint = props.location && props.location.x && props.location.y ? props.location : usCenter;
const zoomLevel = props.location && props.location.x && props.location.y ? 6 : 3;
if (map.value) {
map.value.zoom(zoomLevel);
map.value.center(centerPoint);
}

}
});
return {
mapRef,
};
},
});
</script>

<template>
<v-card class="pa-0 ma-0">
<div ref="mapRef" :style="`width:${size.width}px; height:${size.height}px`" />

Check warning on line 120 in client/src/components/MapLocation.vue

View workflow job for this annotation

GitHub Actions / Lint [eslint]

':style' should be on a new line
</v-card>
</template>

<style lang="scss" scoped></style>
Loading
Loading