Skip to content

Commit aa979c2

Browse files
Species Suggestion Frontend (#468)
* modify grts_cell to sample_frame_id, the real name * add species range model, management loading command, admin interface * GET species support for recordingId, GRTSCell and Sample_frame_Id for listing * mark geojson file as generated to remove line count * mark geojson file as vendored to remove line count * inital testing for specie suggestion front-end * add category label back to Species results * suggested species display * Update .gitattributes Co-authored-by: Brian Helba <brian.helba@kitware.com> * Update bats_ai/core/management/commands/load_species_geojson.py Co-authored-by: Brian Helba <brian.helba@kitware.com> * swap to tuples to lists for admin * prevent nullable for SpeciesRange.source_feature_id field * update GEOJSON location, add loading to migration * default to CONUS_SAMPLE_FRAME_ID=14 for unknown sample_frame_ids * cleanup species view * cleanup species view * pathlib for DEFAULT_GEOJSON * rename species.geojson to species-ranges.geojson * Update bats_ai/core/management/commands/load_species_geojson.py Co-authored-by: Brian Helba <brian.helba@kitware.com> * allow more failing errors in load species range * remove unecessary pk, and default to false if geom exists and overlap is false * remove unecessary pk, and default to false if geom exists and overlap is false * in_range output comments, species-range ingestion description * linting * providing speices range key --------- Co-authored-by: Brian Helba <brian.helba@kitware.com>
1 parent 47e96c6 commit aa979c2

File tree

4 files changed

+131
-12
lines changed

4 files changed

+131
-12
lines changed

client/src/components/SingleSpecieEditor.vue

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
} from "vue";
1212
import type { Species } from "@api/api";
1313
import SingleSpecieInfo from "./SingleSpecieInfo.vue";
14-
14+
const suggestedSpeciesKey = "suggested species by range location";
1515
export default defineComponent({
1616
name: "SingleSpecieEditor",
1717
components: { SingleSpecieInfo },
@@ -58,21 +58,46 @@ export default defineComponent({
5858
single: "primary",
5959
multiple: "secondary",
6060
frequency: "warning",
61+
[suggestedSpeciesKey]: "success",
6162
noid: "",
6263
};
6364
65+
const categoryPriority: Record<string, number> = {
66+
[suggestedSpeciesKey]: 0,
67+
single: 1,
68+
multiple: 2,
69+
frequency: 3,
70+
noid: 4,
71+
};
72+
73+
const inRangeTooltip =
74+
"This species is in the same range as the recording.";
75+
6476
const groupedItems = computed(() => {
77+
const inRangeSpecies = props.speciesList.filter((s) => s.in_range === true);
78+
const rest = props.speciesList.filter((s) => s.in_range !== true);
79+
6580
const groups: Record<string, Species[]> = {};
66-
for (const s of props.speciesList) {
81+
for (const s of rest) {
6782
const cat =
6883
s.category.charAt(0).toUpperCase() + s.category.slice(1);
6984
if (!groups[cat]) groups[cat] = [];
7085
groups[cat].push(s);
7186
}
7287
const result: Array<
73-
{ type: "subheader"; title: string } | (Species & { category: string })
88+
{ type: "subheader"; title: string } | Species
7489
> = [];
75-
const groupsOrder = ["Single", "Multiple", "Frequency", "Noid"];
90+
if (inRangeSpecies.length > 0) {
91+
result.push({ type: "subheader", title: "Suggested Species by Range Location" });
92+
const sortedInRange = [...inRangeSpecies].sort((a, b) => {
93+
const aCat = categoryPriority[a.category] ?? 999;
94+
const bCat = categoryPriority[b.category] ?? 999;
95+
if (aCat !== bCat) return aCat - bCat;
96+
return a.species_code.localeCompare(b.species_code);
97+
});
98+
result.push(...sortedInRange);
99+
}
100+
const groupsOrder = ["In range", "Single", "Multiple", "Frequency", "Noid"];
76101
groupsOrder.forEach((key) => {
77102
result.push({ type: "subheader", title: key });
78103
result.push(...(groups[key] ?? []));
@@ -115,6 +140,7 @@ export default defineComponent({
115140
categoryColors,
116141
speciesAutocomplete,
117142
onClearOrDeleteClick,
143+
inRangeTooltip,
118144
};
119145
},
120146
});
@@ -184,6 +210,18 @@ export default defineComponent({
184210
{{ (item.raw as Species).category }}
185211
</v-chip>
186212
</template>
213+
<template
214+
v-if="(item.raw as Species).in_range === true"
215+
#append
216+
>
217+
<v-icon
218+
v-tooltip="inRangeTooltip"
219+
size="small"
220+
color="#b8860b"
221+
>
222+
mdi-map
223+
</v-icon>
224+
</template>
187225
</v-list-item>
188226
</template>
189227
</v-autocomplete>

client/src/components/SingleSpecieInfo.vue

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,20 @@ export default defineComponent({
4949
5050
const orderedSpecies = ref<Species[]>([]);
5151
52+
const inRangeTooltip =
53+
"This species is in the same range as the recording.";
54+
5255
function sortSpecies(species: Species[], selectedCode: string | null) {
5356
const copied = cloneDeep(species);
5457
copied.sort((a, b) => {
5558
const aSelected = selectedCode === a.species_code;
5659
const bSelected = selectedCode === b.species_code;
5760
if (aSelected && !bSelected) return -1;
5861
if (!aSelected && bSelected) return 1;
62+
const aIn = a.in_range === true;
63+
const bIn = b.in_range === true;
64+
if (aIn && !bIn) return -1;
65+
if (!aIn && bIn) return 1;
5966
const aCat = categoryPriority[a.category] ?? 999;
6067
const bCat = categoryPriority[b.category] ?? 999;
6168
if (aCat !== bCat) return aCat - bCat;
@@ -131,6 +138,7 @@ export default defineComponent({
131138
saveAndClose,
132139
buttonLabel,
133140
selectedSpecies,
141+
inRangeTooltip,
134142
};
135143
},
136144
});
@@ -232,7 +240,12 @@ export default defineComponent({
232240
class="elevation-1 my-recordings"
233241
>
234242
<template #item="{ item }">
235-
<tr :class="item.selected ? 'selected-row' : ''">
243+
<tr
244+
:class="[
245+
item.selected ? 'selected-row' : '',
246+
item.in_range === true ? 'species-in-range-row' : '',
247+
]"
248+
>
236249
<td>
237250
<v-checkbox
238251
:model-value="item.selected"
@@ -242,7 +255,17 @@ export default defineComponent({
242255
@update:model-value="toggleSpecies(item.species_code)"
243256
/>
244257
</td>
245-
<td>{{ item.species_code }}</td>
258+
<td>
259+
<span>{{ item.species_code }}</span>
260+
<v-icon
261+
v-if="item.in_range === true"
262+
v-tooltip="inRangeTooltip"
263+
size="small"
264+
class="species-in-range-map-icon flex-shrink-0"
265+
>
266+
mdi-map
267+
</v-icon>
268+
</td>
246269
<td>
247270
<span
248271
:class="
@@ -305,9 +328,19 @@ export default defineComponent({
305328
</template>
306329

307330
<style scoped>
308-
.selected-row {
331+
.species-in-range-row {
332+
background-color: rgba(212, 175, 55, 0.14);
333+
}
334+
.selected-row.species-in-range-row {
335+
background-color: rgba(0, 0, 255, 0.05);
336+
box-shadow: inset 0 0 0 1px rgba(25, 118, 210, 0.35);
337+
}
338+
.selected-row:not(.species-in-range-row) {
309339
background-color: rgba(0, 0, 255, 0.05);
310340
}
341+
.selected-row {
342+
/* default selected tint when not in-range handled above */
343+
}
311344
312345
.text-primary {
313346
color: #1976d2;

client/src/components/SpeciesInfo.vue

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@ export default defineComponent({
7474
7575
const orderedSpecies = ref<Species[]>([]);
7676
77+
const inRangeTooltip =
78+
"This species is in the same range as the recording.";
79+
7780
function sortSpecies(species: Species[], selectedCodes: string[]) {
7881
const copied = cloneDeep(species);
7982
copied.sort((a, b) => {
@@ -83,6 +86,11 @@ export default defineComponent({
8386
if (aSelected && !bSelected) return -1;
8487
if (!aSelected && bSelected) return 1;
8588
89+
const aIn = a.in_range === true;
90+
const bIn = b.in_range === true;
91+
if (aIn && !bIn) return -1;
92+
if (!aIn && bIn) return 1;
93+
8694
const aCat = categoryPriority[a.category] ?? 999;
8795
const bCat = categoryPriority[b.category] ?? 999;
8896
if (aCat !== bCat) return aCat - bCat;
@@ -153,6 +161,7 @@ export default defineComponent({
153161
hasChanges,
154162
closeDialog,
155163
saveAndClose,
164+
inRangeTooltip,
156165
};
157166
},
158167
});
@@ -217,7 +226,12 @@ export default defineComponent({
217226
class="elevation-1 my-recordings"
218227
>
219228
<template #item="{ item }">
220-
<tr :class="item.selected ? 'selected-row' : ''">
229+
<tr
230+
:class="[
231+
item.selected ? 'selected-row' : '',
232+
item.in_range === true ? 'species-in-range-row' : '',
233+
]"
234+
>
221235
<td>
222236
<v-checkbox
223237
:model-value="item.selected"
@@ -227,7 +241,19 @@ export default defineComponent({
227241
@update:model-value="toggleSpecies(item.species_code)"
228242
/>
229243
</td>
230-
<td>{{ item.species_code }}</td>
244+
<td>
245+
<span class="d-inline-flex align-center flex-nowrap ga-1">
246+
<span>{{ item.species_code }}</span>
247+
<v-icon
248+
v-if="item.in_range === true"
249+
v-tooltip="inRangeTooltip"
250+
size="small"
251+
class="species-in-range-map-icon flex-shrink-0"
252+
>
253+
mdi-map
254+
</v-icon>
255+
</span>
256+
</td>
231257
<td>
232258
<span :class="categoryColors[item.category] ? `text-${categoryColors[item.category]}` : ''">
233259
{{ item.category.charAt(0).toUpperCase() + item.category.slice(1) }}
@@ -270,9 +296,18 @@ export default defineComponent({
270296
</template>
271297

272298
<style scoped>
273-
.selected-row {
299+
.species-in-range-row:not(.selected-row) {
300+
background-color: rgba(212, 175, 55, 0.14);
301+
}
302+
.selected-row:not(.species-in-range-row) {
274303
background-color: rgba(0, 0, 255, 0.05);
275-
/* Light blue tint */
304+
}
305+
.selected-row.species-in-range-row {
306+
background-color: rgba(212, 175, 55, 0.18);
307+
box-shadow: inset 0 0 0 1px rgba(25, 118, 210, 0.35);
308+
}
309+
.species-in-range-map-icon {
310+
color: #b8860b;
276311
}
277312
278313
.text-primary {

client/src/main.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,20 @@ import { axiosInstance } from './api/api';
1212
import { installPrompt } from './use/prompt-service';
1313

1414
const app = createApp(App);
15-
const Vuetify = createVuetify({});
15+
const Vuetify = createVuetify({
16+
theme: {
17+
themes: {
18+
light: {
19+
colors: {
20+
primary: "#1976d2",
21+
secondary: "#9c27b0",
22+
warning: "#fb8c00",
23+
golden: "#b8860b",
24+
}
25+
}
26+
}
27+
}
28+
});
1629

1730
Sentry.init({
1831
app,

0 commit comments

Comments
 (0)