Skip to content
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 bats_ai/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from oauth2_provider.models import AccessToken

from bats_ai.core.views import (
ConfigurationRouter,
GRTSCellsRouter,
GuanoMetadataRouter,
RecordingAnnotationRouter,
Expand Down Expand Up @@ -35,3 +36,4 @@ def global_auth(request):
api.add_router('/grts/', GRTSCellsRouter)
api.add_router('/guano/', GuanoMetadataRouter)
api.add_router('/recording-annotation/', RecordingAnnotationRouter)
api.add_router('/configuration/', ConfigurationRouter)
25 changes: 25 additions & 0 deletions bats_ai/core/migrations/0013_configuration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Generated by Django 4.1.13 on 2025-01-03 15:14

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
('core', '0012_recordingannotation_additional_data'),
]

operations = [
migrations.CreateModel(
name='Configuration',
fields=[
(
'id',
models.BigAutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name='ID'
),
),
('display_pulse_annotations', models.BooleanField(default=True)),
('display_sequence_annotations', models.BooleanField(default=True)),
],
),
]
2 changes: 2 additions & 0 deletions bats_ai/core/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from .annotations import Annotations
from .compressed_spectrogram import CompressedSpectrogram
from .configuration import Configuration
from .grts_cells import GRTSCells
from .image import Image
from .recording import Recording, colormap
Expand All @@ -21,4 +22,5 @@
'colormap',
'CompressedSpectrogram',
'RecordingAnnotation',
'Configuration',
]
25 changes: 25 additions & 0 deletions bats_ai/core/models/configuration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from django.db import models
from django.db.models.signals import post_migrate
from django.dispatch import receiver


# Define the Configuration model
class Configuration(models.Model):
display_pulse_annotations = models.BooleanField(default=True)
display_sequence_annotations = models.BooleanField(default=True)

def save(self, *args, **kwargs):
# Ensure only one instance of Configuration exists
if not Configuration.objects.exists() and not self.pk:
super().save(*args, **kwargs)
elif self.pk:
super().save(*args, **kwargs)
else:
raise ValueError('Only one instance of Configuration is allowed.')


# Automatically create a Configuration instance after migrations
@receiver(post_migrate)
def create_default_configuration(sender, **kwargs):
if not Configuration.objects.exists():
Configuration.objects.create()
2 changes: 2 additions & 0 deletions bats_ai/core/views/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from .annotations import router as AnnotationRouter
from .configuration import router as ConfigurationRouter
from .grts_cells import router as GRTSCellsRouter
from .guanometadata import router as GuanoMetadataRouter
from .recording import router as RecordingRouter
Expand All @@ -14,4 +15,5 @@
'GRTSCellsRouter',
'GuanoMetadataRouter',
'RecordingAnnotationRouter',
'ConfigurationRouter',
]
53 changes: 53 additions & 0 deletions bats_ai/core/views/configuration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import logging

from django.http import JsonResponse
from ninja import Schema
from ninja.pagination import RouterPaginated

from bats_ai.core.models import Configuration

logger = logging.getLogger(__name__)


router = RouterPaginated()


# Define schema for the Configuration data
class ConfigurationSchema(Schema):
display_pulse_annotations: bool
display_sequence_annotations: bool
is_admin: bool | None = None


# Endpoint to retrieve the configuration status
@router.get('/', response=ConfigurationSchema)
def get_configuration(request):
config = Configuration.objects.first()
if not config:
return JsonResponse({'error': 'No configuration found'}, status=404)
return ConfigurationSchema(
display_pulse_annotations=config.display_pulse_annotations,
display_sequence_annotations=config.display_sequence_annotations,
is_admin=request.user.is_authenticated and request.user.is_superuser,
)


# Endpoint to update the configuration (admin only)
@router.patch('/')
def update_configuration(request, payload: ConfigurationSchema):
if not request.user.is_authenticated or not request.user.is_superuser:
return JsonResponse({'error': 'Permission denied'}, status=403)
config = Configuration.objects.first()
if not config:
return JsonResponse({'error': 'No configuration found'}, status=404)
for attr, value in payload.dict().items():
setattr(config, attr, value)
config.save()
return ConfigurationSchema.from_orm(config)


@router.get('/is_admin/')
def check_is_admin(request):
if request.user.is_authenticated:
return {'is_admin': request.user.is_superuser}
return {'is_admin': False}
2 changes: 1 addition & 1 deletion bats_ai/core/views/temporal_annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class TemporalAnnotationSchema(Schema):
id: int
start_time: int
end_time: int
type: str
type: str | None
comments: str
species: list[SpeciesSchema] | None
owner_email: str = None
Expand Down
31 changes: 25 additions & 6 deletions client/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,22 @@ export default defineComponent({
const oauthClient = inject<OAuthClient>("oauthClient");
const router = useRouter();
const route = useRoute();
const { nextShared, sharedList, sideTab, } = useState();
const { nextShared, sharedList, sideTab, loadConfiguration, configuration } = useState();
const getShared = async () => {
sharedList.value = (await getRecordings(true)).data;
};
if (sharedList.value.length === 0) {
getShared();
}
if (oauthClient === undefined) {
throw new Error('Must provide "oauthClient" into component.');
}

const loginText = ref("Login");
const checkLogin = () => {
const checkLogin = async () => {
if (oauthClient.isLoggedIn) {
loginText.value = "Logout";
loadConfiguration();
if (sharedList.value.length === 0) {
getShared();
}
} else {
loginText.value = "Login";
}
Expand Down Expand Up @@ -57,7 +58,18 @@ export default defineComponent({
}
});

return { oauthClient, containsSpectro, loginText, logInOrOut, activeTab, nextShared, sideTab };
const isAdmin = computed(() => configuration.value.is_admin);

return {
oauthClient,
containsSpectro,
loginText,
logInOrOut,
activeTab,
nextShared,
sideTab,
isAdmin,
};
},
});
</script>
Expand Down Expand Up @@ -88,6 +100,13 @@ export default defineComponent({
>
Spectrogram
</v-tab>
<v-tab
v-show="isAdmin"
to="/admin"
value="admin"
>
Admin
</v-tab>
</v-tabs>
<v-spacer />
<v-tooltip
Expand Down
17 changes: 17 additions & 0 deletions client/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@

export type OtherUserAnnotations = Record<string, {annotations: SpectrogramAnnotation[], temporal: SpectrogramTemporalAnnotation[]}>;

interface PaginatedNinjaResponse<T> {

Check warning on line 148 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[],
}
Expand Down Expand Up @@ -357,6 +357,21 @@
return axiosInstance.get<CellIDReponse>(`/grts/grid_cell_id`, {params: {latitude, longitude}});
}

export interface ConfigurationSettings {
display_pulse_annotations: boolean;
display_sequence_annotations: boolean;
is_admin: boolean;
}

export type Configuration = ConfigurationSettings & { is_admin : boolean };
async function getConfiguration() {
return axiosInstance.get<Configuration>('/configuration/');
}

async function patchConfiguration(config: ConfigurationSettings) {
return axiosInstance.patch('/configuration/', {...config });
}

interface GuanoMetadata {
nabat_grid_cell_grts_id?: string
nabat_latitude?: number
Expand Down Expand Up @@ -409,4 +424,6 @@
putFileAnnotation,
patchFileAnnotation,
deleteFileAnnotation,
getConfiguration,
patchConfiguration,
};
10 changes: 8 additions & 2 deletions client/src/components/AnnotationList.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts">
import { defineComponent, PropType } from "vue";
import { computed, defineComponent, PropType } from "vue";
import { SpectroInfo } from './geoJS/geoJSUtils';
import useState from "../use/useState";
import { watch, ref } from "vue";
Expand Down Expand Up @@ -32,7 +32,7 @@ export default defineComponent({
},
emits: ['select', 'update:annotation', 'delete:annotation'],
setup() {
const { creationType, annotationState, setAnnotationState, annotations, temporalAnnotations, selectedId, selectedType, setSelectedId, sideTab } = useState();
const { creationType, annotationState, setAnnotationState, annotations, temporalAnnotations, selectedId, selectedType, setSelectedId, sideTab, configuration } = useState();
const tab = ref('recording');
const scrollToId = (id: number) => {
const el = document.getElementById(`annotation-${id}`);
Expand All @@ -49,6 +49,8 @@ export default defineComponent({
watch(selectedType, () => {
tab.value = selectedType.value;
});
const pulseEnabled = computed(() => configuration.value.display_pulse_annotations);
const sequenceEnabled = computed(() => configuration.value.display_sequence_annotations);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const tabSwitch = (event: any) => {
// On tab switches we want to deselect the curret annotation
Expand All @@ -73,6 +75,8 @@ export default defineComponent({
tabSwitch,
tab,
sideTab,
pulseEnabled,
sequenceEnabled,
};
},
});
Expand Down Expand Up @@ -105,6 +109,7 @@ export default defineComponent({
<span>Recording/File Level Species Annotations</span>
</v-tooltip>
<v-tooltip
v-if="sequenceEnabled"
location="bottom"
open-delay="400"
>
Expand All @@ -120,6 +125,7 @@ export default defineComponent({
<span>Sequence Level annotations (Approach/Search/Terminal/Social)</span>
</v-tooltip>
<v-tooltip
v-if="pulseEnabled"
location="bottom"
open-delay="400"
>
Expand Down
12 changes: 12 additions & 0 deletions client/src/components/geoJS/LayerManager.vue
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export default defineComponent({
selectedType,
setSelectedId,
viewCompressedOverlay,
configuration,
} = useState();
const selectedAnnotationId: Ref<null | number> = ref(null);
const hoveredAnnotationId: Ref<null | number> = ref(null);
Expand Down Expand Up @@ -371,6 +372,16 @@ export default defineComponent({
editAnnotationLayer.changeData(editingAnnotation.value, selectedType.value);
}, 0);
}
// We need to disable annotations that aren't required for different views
if (!configuration.value.display_pulse_annotations) {
rectAnnotationLayer?.disable();
speciesLayer?.disable();
freqLayer?.disable();
}
if (!configuration.value.display_sequence_annotations) {
temporalAnnotationLayer?.disable();
speciesSequenceLayer?.disable();
}
};
watch(
annotations,
Expand Down Expand Up @@ -494,6 +505,7 @@ export default defineComponent({
speciesLayer.spectroInfo = props.spectroInfo;
}

timeLayer.setDisplaying({pulse: configuration.value.display_pulse_annotations, sequence: configuration.value.display_sequence_annotations});
timeLayer.formatData(localAnnotations.value, temporalAnnotations.value);
freqLayer.formatData(localAnnotations.value);
speciesLayer.formatData(localAnnotations.value);
Expand Down
39 changes: 24 additions & 15 deletions client/src/components/geoJS/geoJSUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ function spectroTemporalToGeoJSon(
const adjustedWidth = scaledWidth > spectroInfo.width ? scaledWidth : spectroInfo.width;
// const adjustedHeight = scaledHeight > spectroInfo.height ? scaledHeight : spectroInfo.height;
//scale pixels to time and frequency ranges
if (spectroInfo.compressedWidth && spectroInfo.start_times === undefined || spectroInfo.end_times === undefined) {
if (spectroInfo.compressedWidth === undefined) {
const widthScale = adjustedWidth / (spectroInfo.end_time - spectroInfo.start_time);
// Now we remap our annotation to pixel coordinates
const start_time = annotation.start_time * widthScale;
Expand All @@ -292,7 +292,7 @@ function spectroTemporalToGeoJSon(
],
],
};
} else if (spectroInfo.start_times && spectroInfo.end_times) {
} else if (spectroInfo.compressedWidth && spectroInfo.start_times && spectroInfo.end_times) {
// Compressed Spectro has different conversion
// Find what section the annotation is in
const start = annotation.start_time;
Expand All @@ -301,21 +301,30 @@ function spectroTemporalToGeoJSon(
const lengths = start_times.length === end_times.length ? start_times.length : 0;
let foundStartIndex = -1;
let foundEndIndex = -1;
if (start < start_times[0]) {
foundStartIndex = 0;
}
for (let i = 0; i < lengths; i += 1) {
if (
foundStartIndex === -1 &&
start_times[i] < start &&
start < end_times[i]
) {
foundStartIndex = i;
if (foundStartIndex === -1) {
if (start < start_times[i]) {
foundStartIndex = i; // Lock to the current index if before the interval
} else if (start_times[i] <= start && start <= end_times[i]) {
foundStartIndex = i; // Found within the interval
} else if (i === lengths - 1 && start > end_times[i]) {
foundStartIndex = i; // Lock to the last interval's end
}
}

// Check for end time
if (foundEndIndex === -1) {
if (end < start_times[i]) {
foundEndIndex = i; // Lock to the current index if before the interval
} else if (start_times[i] <= end && end <= end_times[i]) {
foundEndIndex = i; // Found within the interval
} else if (i === lengths - 1 && end > end_times[i]) {
foundEndIndex = i; // Lock to the last interval's end
}
}
if (
foundEndIndex === -1 &&
start_times[i] < end &&
end < end_times[i]
) {
foundEndIndex = i;
}
}
// We need to build the length of times to pixel size for the time spaces before the annotation
const compressedScale = scaledWidth > (spectroInfo.compressedWidth || 1) ? scaledWidth / (spectroInfo.compressedWidth || spectroInfo.width) : 1;
Expand Down
Loading
Loading