Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
d5d1ef5
modify grts_cell to sample_frame_id, the real name
BryonLewis Apr 2, 2026
8705dcb
add species range model, management loading command, admin interface
BryonLewis Apr 2, 2026
4a9b595
GET species support for recordingId, GRTSCell and Sample_frame_Id for…
BryonLewis Apr 2, 2026
9291c8d
mark geojson file as generated to remove line count
BryonLewis Apr 2, 2026
c844931
mark geojson file as vendored to remove line count
BryonLewis Apr 2, 2026
a6f3e15
inital testing for specie suggestion front-end
BryonLewis Apr 2, 2026
37e1802
add category label back to Species results
BryonLewis Apr 3, 2026
42bcfaa
Merge branch 'species-suggestion-backend' into species-suggestion-fro…
BryonLewis Apr 3, 2026
02d04c7
suggested species display
BryonLewis Apr 6, 2026
4f2b7a9
Update .gitattributes
BryonLewis Apr 8, 2026
3b1e2fd
Update bats_ai/core/management/commands/load_species_geojson.py
BryonLewis Apr 8, 2026
6db3150
swap to tuples to lists for admin
BryonLewis Apr 8, 2026
b886db0
prevent nullable for SpeciesRange.source_feature_id field
BryonLewis Apr 8, 2026
7dfd479
update GEOJSON location, add loading to migration
BryonLewis Apr 8, 2026
14c969e
default to CONUS_SAMPLE_FRAME_ID=14 for unknown sample_frame_ids
BryonLewis Apr 8, 2026
202aa00
cleanup species view
BryonLewis Apr 8, 2026
e31ebb9
Merge branch 'species-suggestion-backend' into species-suggestion-fro…
BryonLewis Apr 8, 2026
1c6cae4
cleanup species view
BryonLewis Apr 8, 2026
4b658f4
Merge branch 'species-suggestion-backend' into species-suggestion-fro…
BryonLewis Apr 8, 2026
5cfc201
pathlib for DEFAULT_GEOJSON
BryonLewis Apr 8, 2026
9bb636a
rename species.geojson to species-ranges.geojson
BryonLewis Apr 8, 2026
39bbdd3
Update bats_ai/core/management/commands/load_species_geojson.py
BryonLewis Apr 8, 2026
fadceef
allow more failing errors in load species range
BryonLewis Apr 8, 2026
aaa2a01
Merge branch 'species-suggestion-backend' into species-suggestion-fro…
BryonLewis Apr 8, 2026
fdc273d
remove unecessary pk, and default to false if geom exists and overlap…
BryonLewis Apr 8, 2026
a95324b
remove unecessary pk, and default to false if geom exists and overlap…
BryonLewis Apr 8, 2026
b048a59
in_range output comments, species-range ingestion description
BryonLewis Apr 8, 2026
b69c626
Merge branch 'species-suggestion-backend' into species-suggestion-fro…
BryonLewis Apr 8, 2026
6272f15
Merge branch 'main' into species-suggestion-frontend
BryonLewis Apr 8, 2026
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
9 changes: 7 additions & 2 deletions client/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ export type RecordingLocationsFeatureProperties = {

export type RecordingLocationsGeoJson = FeatureCollection<Point, RecordingLocationsFeatureProperties>;

export interface SpeciesResponse {
items: Species[];
count: number;
}
export interface Species {
species_code: string;
family: string;
Expand All @@ -48,6 +52,7 @@ export interface Species {
species?: string;
id: number;
category: "single" | "multiple" | "frequency" | "noid";
in_range?: boolean;
}

export interface SpectrogramAnnotation {
Expand Down Expand Up @@ -408,8 +413,8 @@ async function getSequenceAnnotations(recordingId: string) {
);
}

async function getSpecies() {
return axiosInstance.get<Species[]>("/species/");
async function getSpecies({recordingId, grtsCellId, sampleFrameId}: {recordingId?: number, grtsCellId?: number, sampleFrameId?: number}) {
return axiosInstance.get<SpeciesResponse>("/species/", { params: { recording_id: recordingId, grts_cell_id: grtsCellId, sample_frame_id: sampleFrameId } });
}

async function patchAnnotation(
Expand Down
45 changes: 42 additions & 3 deletions client/src/components/SingleSpecieEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -58,21 +58,46 @@
single: "primary",
multiple: "secondary",
frequency: "warning",
"suggested species by range location": "success",
noid: "",
};

const categoryPriority: Record<string, number> = {
"suggested species by range location": 0,
single: 1,
multiple: 2,
frequency: 3,
noid: 4,
};

const inRangeTooltip =
"This species is in the same range as the recording.";

const groupedItems = computed(() => {
const inRangeSpecies = props.speciesList.filter((s) => s.in_range === true);
const rest = props.speciesList.filter((s) => s.in_range !== true);

const groups: Record<string, Species[]> = {};
for (const s of props.speciesList) {
for (const s of rest) {
const cat =
s.category.charAt(0).toUpperCase() + s.category.slice(1);
if (!groups[cat]) groups[cat] = [];
groups[cat].push(s);
}
const result: Array<
{ type: "subheader"; title: string } | (Species & { category: string })
{ type: "subheader"; title: string } | Species
> = [];
const groupsOrder = ["Single", "Multiple", "Frequency", "Noid"];
if (inRangeSpecies.length > 0) {
result.push({ type: "subheader", title: "Suggested Species by Range Location" });
const sortedInRange = [...inRangeSpecies].sort((a, b) => {
const aCat = categoryPriority[a.category] ?? 999;
const bCat = categoryPriority[b.category] ?? 999;
if (aCat !== bCat) return aCat - bCat;
return a.species_code.localeCompare(b.species_code);
});
result.push(...sortedInRange);
}
const groupsOrder = ["In range", "Single", "Multiple", "Frequency", "Noid"];
groupsOrder.forEach((key) => {
result.push({ type: "subheader", title: key });
result.push(...(groups[key] ?? []));
Expand Down Expand Up @@ -115,6 +140,7 @@
categoryColors,
speciesAutocomplete,
onClearOrDeleteClick,
inRangeTooltip,
};
},
});
Expand Down Expand Up @@ -163,7 +189,8 @@
categoryColors[String(subProps.title).toLowerCase()]
? `bg-${categoryColors[String(subProps.title).toLowerCase()]}`
: ''
"

Check warning on line 192 in client/src/components/SingleSpecieEditor.vue

View workflow job for this annotation

GitHub Actions / Lint [eslint]

Expected 1 line break before closing bracket, but 2 line breaks found

>
{{ subProps.title }}
</v-list-subheader>
Expand All @@ -184,6 +211,18 @@
{{ (item.raw as Species).category }}
</v-chip>
</template>
<template
v-if="(item.raw as Species).in_range === true"
#append
>
<v-icon
v-tooltip="inRangeTooltip"
size="small"
color="#b8860b"
>
mdi-map
</v-icon>
</template>
</v-list-item>
</template>
</v-autocomplete>
Expand Down
39 changes: 36 additions & 3 deletions client/src/components/SingleSpecieInfo.vue
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,20 @@ export default defineComponent({

const orderedSpecies = ref<Species[]>([]);

const inRangeTooltip =
"This species is in the same range as the recording.";

function sortSpecies(species: Species[], selectedCode: string | null) {
const copied = cloneDeep(species);
copied.sort((a, b) => {
const aSelected = selectedCode === a.species_code;
const bSelected = selectedCode === b.species_code;
if (aSelected && !bSelected) return -1;
if (!aSelected && bSelected) return 1;
const aIn = a.in_range === true;
const bIn = b.in_range === true;
if (aIn && !bIn) return -1;
if (!aIn && bIn) return 1;
const aCat = categoryPriority[a.category] ?? 999;
const bCat = categoryPriority[b.category] ?? 999;
if (aCat !== bCat) return aCat - bCat;
Expand Down Expand Up @@ -131,6 +138,7 @@ export default defineComponent({
saveAndClose,
buttonLabel,
selectedSpecies,
inRangeTooltip,
};
},
});
Expand Down Expand Up @@ -232,7 +240,12 @@ export default defineComponent({
class="elevation-1 my-recordings"
>
<template #item="{ item }">
<tr :class="item.selected ? 'selected-row' : ''">
<tr
:class="[
item.selected ? 'selected-row' : '',
item.in_range === true ? 'species-in-range-row' : '',
]"
>
<td>
<v-checkbox
:model-value="item.selected"
Expand All @@ -242,7 +255,17 @@ export default defineComponent({
@update:model-value="toggleSpecies(item.species_code)"
/>
</td>
<td>{{ item.species_code }}</td>
<td class="d-flex align-center ga-1 flex-nowrap">
<span>{{ item.species_code }}</span>
<v-icon
v-if="item.in_range === true"
v-tooltip="inRangeTooltip"
size="small"
class="species-in-range-map-icon flex-shrink-0"
>
mdi-map
</v-icon>
</td>
<td>
<span
:class="
Expand Down Expand Up @@ -305,8 +328,18 @@ export default defineComponent({
</template>

<style scoped>
.selected-row {
.species-in-range-row {
background-color: rgba(212, 175, 55, 0.14);
}
.selected-row.species-in-range-row {
background-color: rgba(0, 0, 255, 0.05);
box-shadow: inset 0 0 0 1px rgba(25, 118, 210, 0.35);
}
.selected-row:not(.species-in-range-row) {
background-color: rgba(0, 0, 255, 0.05);
}
.selected-row {
/* default selected tint when not in-range handled above */
}

.text-primary {
Expand Down
43 changes: 39 additions & 4 deletions client/src/components/SpeciesInfo.vue
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ export default defineComponent({

const orderedSpecies = ref<Species[]>([]);

const inRangeTooltip =
"This species is in the same range as the recording.";

function sortSpecies(species: Species[], selectedCodes: string[]) {
const copied = cloneDeep(species);
copied.sort((a, b) => {
Expand All @@ -83,6 +86,11 @@ export default defineComponent({
if (aSelected && !bSelected) return -1;
if (!aSelected && bSelected) return 1;

const aIn = a.in_range === true;
const bIn = b.in_range === true;
if (aIn && !bIn) return -1;
if (!aIn && bIn) return 1;

const aCat = categoryPriority[a.category] ?? 999;
const bCat = categoryPriority[b.category] ?? 999;
if (aCat !== bCat) return aCat - bCat;
Expand Down Expand Up @@ -153,6 +161,7 @@ export default defineComponent({
hasChanges,
closeDialog,
saveAndClose,
inRangeTooltip,
};
},
});
Expand Down Expand Up @@ -217,7 +226,12 @@ export default defineComponent({
class="elevation-1 my-recordings"
>
<template #item="{ item }">
<tr :class="item.selected ? 'selected-row' : ''">
<tr
:class="[
item.selected ? 'selected-row' : '',
item.in_range === true ? 'species-in-range-row' : '',
]"
>
<td>
<v-checkbox
:model-value="item.selected"
Expand All @@ -227,7 +241,19 @@ export default defineComponent({
@update:model-value="toggleSpecies(item.species_code)"
/>
</td>
<td>{{ item.species_code }}</td>
<td>
<span class="d-inline-flex align-center flex-nowrap ga-1">
<span>{{ item.species_code }}</span>
<v-icon
v-if="item.in_range === true"
v-tooltip="inRangeTooltip"
size="small"
class="species-in-range-map-icon flex-shrink-0"
>
mdi-map
</v-icon>
</span>
</td>
<td>
<span :class="categoryColors[item.category] ? `text-${categoryColors[item.category]}` : ''">
{{ item.category.charAt(0).toUpperCase() + item.category.slice(1) }}
Expand Down Expand Up @@ -270,9 +296,18 @@ export default defineComponent({
</template>

<style scoped>
.selected-row {
.species-in-range-row:not(.selected-row) {
background-color: rgba(212, 175, 55, 0.14);
}
.selected-row:not(.species-in-range-row) {
background-color: rgba(0, 0, 255, 0.05);
/* Light blue tint */
}
.selected-row.species-in-range-row {
background-color: rgba(212, 175, 55, 0.18);
box-shadow: inset 0 0 0 1px rgba(25, 118, 210, 0.35);
}
.species-in-range-map-icon {
color: #b8860b;
}

.text-primary {
Expand Down
15 changes: 14 additions & 1 deletion client/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,20 @@ import { axiosInstance } from './api/api';
import { installPrompt } from './use/prompt-service';

const app = createApp(App);
const Vuetify = createVuetify({});
const Vuetify = createVuetify({
theme: {
themes: {
light: {
colors: {
primary: "#1976d2",
secondary: "#9c27b0",
warning: "#fb8c00",
golden: "#b8860b",
}
}
}
}
});

Sentry.init({
app,
Expand Down
4 changes: 2 additions & 2 deletions client/src/views/NABat/NABatSpectrogram.vue
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,9 @@ export default defineComponent({
spectroInfo.value.end_times = response.data.compressed.end_times;
viewCompressedOverlay.value = false;
}
const speciesResponse = await getSpecies();
const speciesResponse = await getSpecies({recordingId: parseInt(props.id)});
// Removing NOISE species from list and any duplicates
speciesList.value = speciesResponse.data.filter(
speciesList.value = speciesResponse.data.items.filter(
(value, index, self) => value.species_code !== "NOISE" && index === self.findIndex((t) => t.species_code === value.species_code)
);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down
4 changes: 2 additions & 2 deletions client/src/views/Spectrogram.vue
Original file line number Diff line number Diff line change
Expand Up @@ -228,9 +228,9 @@ export default defineComponent({
if (spectrogramData.value.currentUser) {
currentUser.value = spectrogramData.value.currentUser;
}
const speciesResponse = await getSpecies();
const speciesResponse = await getSpecies({recordingId: parseInt(props.id)});
// Removing NOISE species from list and any duplicates
speciesList.value = speciesResponse.data .filter(
speciesList.value = speciesResponse.data.items.filter(
(value, index, self) => index === self.findIndex((t) => t.species_code === value.species_code)
);
if (spectrogramData.value.otherUsers && spectroInfo.value) {
Expand Down