from bokeh.models import AutocompleteInput
from bokeh.models import Div, Select, Spinner, RadioButtonGroup
from bokeh.layouts import row, column
import json
import copy
from collections import OrderedDict, Counter
[docs]class AutocompleteTaxonomy:
"""Class to create a Bokeh widget to autocomplete species names. Corresponding GBIF data from \
``species_template.json`` is used to fill in correct information into ``data.json`` for each measurement. The user \
has to specify a confidence greater zero in his insect classification to activate the autocomplete and select \
widgets. Then a species, genus or family can be set via autocomplete input and only then taxonomy data is written \
to ``data.json``.
:param config: configuration including data format dictionary, species template dictionary and current Bokeh \
document
:type config: object
"""
def __init__(self, config):
self.config = config
self.curdoc = config.curdoc
self.data_template_dict = self.config.data
self.species_template_dict = self.config.species
self.TAX_LABELS = ["family", "genus", "species"]
self.template = ("""<div class='content' style="background-color: {background};color: {colour};">
<div class='info'> {status_text} </div> </div>""")
self.template = """<div class='content' style="width: {w}; background-color: {background};color: {colour};">
<div class='info'> {status_text} </div> </div>"""
self.species_list, self.species_indices, self.synonym_list = self.autocomp_list("species")
self.genus_list, self.genus_indices, _ = self.autocomp_list("genus")
self.family_list, self.family_indices, _ = self.autocomp_list("family")
self._widget = self.make_widget()
@property
def widget(self):
"""Calls method :meth:`make_widget`.
"""
return self._widget
[docs] def autocomp_list(self, tax_name):
"""Creates the autocomplete list of all species, genera or families from the ``species_template.json`` \
template depending on the parameter `tax_name` for Bokeh's AutocompletInput object. \
The second list contains the corresponding indices within the ``species_template.json`` list to look up the \
needed GBIF information for a chosen, autocompleted species, genus or family within Bokeh.
:param tax_name: "species", "genus" or "family"
:type tax_name: string
:return: Tuple[list of all species/genera/families, list of corresponding indices in ``species_template.json``,\
list of species synonym flags]
:rtype: Tuple[string, integer, boolean]
"""
tax_list, syn_list = [], []
for i in range(len(self.species_template_dict["results"])):
vernacularName = self.species_template_dict["results"][i]['vernacularName']
canonicalName = self.species_template_dict["results"][i]["canonicalName"]
synonym = self.species_template_dict["results"][i]["synonym"]
rank = self.species_template_dict["results"][i]["rank"]
if tax_name == 'species' and tax_name.upper() in rank:
species = self.species_template_dict["results"][i][tax_name]
if not synonym and vernacularName != '':
if species in tax_list:
idx = tax_list.index(species)
tax_list[idx] = '{} ({})'.format(species, vernacularName)
tax_i = '{} ({})'.format(species, vernacularName)
syn_i = None
elif not synonym and vernacularName == '':
tax_i = '{}'.format(species)
syn_i = None
elif synonym and vernacularName != '':
syn_i = '{} ({})'.format(canonicalName, vernacularName)
tax_i = '{}'.format(species)
elif synonym and vernacularName == '':
syn_i = '{}'.format(canonicalName)
tax_i = '{}'.format(species)
else:
tax_i, syn_i = None, None
else:
tax_i = '{}'.format(self.species_template_dict["results"][i][tax_name])
syn_i = None
tax_list.append(tax_i)
syn_list.append(syn_i)
duplicates = [item for item, count in Counter(tax_list).items() if count > 0]
duplicates_syn = [item for item, count in Counter(syn_list).items() if count > 0]
duplicates.extend(duplicates_syn)
autocomplete_list, lookup_indices, synonym_list = [], [], []
i = 0
for dup in duplicates:
if dup is not None:
i += 1
try:
lookup_indices.append(tax_list.index(dup))
autocomplete_list.append(dup)
synonym_list.append(False)
except:
lookup_indices.append(syn_list.index(dup))
autocomplete_list.append(dup)
synonym_list.append(True)
return autocomplete_list, lookup_indices, synonym_list
[docs] def update_any_change(self, wttr, old, new):
""" Gets called on any change of value input in AutocompleteInput object.
"""
self.auto_comp.value = self.auto_comp.value_input
[docs] def update_selected(self, wttr, old, new):
""" Gets called on change of value in AutocompleteInput object. Due to :meth:`update_any_change` will always be
updated on any change.
"""
if self.auto_comp.value in self.auto_comp.completions:
index_auto_comp_list = self.auto_comp.completions.index(self.auto_comp.value)
lookup_index = self.lookup_indices[index_auto_comp_list]
if self.taxonomy_select.value != "family":
self.tax_status.text = self.template.\
format(status_text='{} {}'.format(self.species_template_dict["results"][lookup_index]['order'],
self.species_template_dict["results"][lookup_index]['family']),
background='#3EA639', colour='#FFFFFF', w='310px')
else:
self.tax_status.text = self.template.\
format(status_text='{}'.format(self.species_template_dict["results"][lookup_index]['order']),
background='#3EA639', colour='#FFFFFF', w='310px')
else:
self.tax_status.text = self.template.format(status_text='{}'.format('Use Autocompletion!'),
background='#DC524C', colour='#FFFFFF', w='310px')
[docs] def change_tax_list(self, wttr, old, new):
""" Gets called on change of value in Select object if other taxonomy level gets chosen.
Updates corresponding lists for Bokeh's AutocompletionInput and the text within the div container to output
chosen species, genus, family.
"""
self.auto_comp.value = ''
if self.taxonomy_select.value == "species":
self.auto_comp.completions = self.species_list
self.lookup_indices = self.species_indices
self.tax_status.text = self.template.format(status_text='{} {}'.format('Order', 'Family'),
background='#999999', colour='#FFFFFF', w='310px')
elif self.taxonomy_select.value == "genus":
self.auto_comp.completions = self.genus_list
self.lookup_indices = self.genus_indices
self.tax_status.text = self.template.format(status_text='{} {}'.format('Order', 'Family'),
background='#999999', colour='#FFFFFF', w='310px')
else:
self.auto_comp.completions = self.family_list
self.lookup_indices = self.family_indices
self.tax_status.text = self.template.format(status_text='Order',
background='#999999', colour='#FFFFFF', w='310px')
[docs] def update_spinner(self, wttr, old, new):
"""Gets called on change of value in Spinner object. Activates Autocomplete and Select widgets if value is
greater zero.
"""
if self.confidence.value == 0:
self.taxonomy_select.value = "species"
self.taxonomy_select.disabled = True
self.auto_comp.value = ''
self.auto_comp.disabled = True
self.gender.active = 0
self.gender.disabled = True
self.tax_status.text = self.template.format(status_text='{} {}'.format('Order', 'Family'),
background='#999999', colour='#FFFFFF', w='310px')
else:
self.auto_comp.disabled = False
self.taxonomy_select.disabled = False
self.gender.disabled = False
[docs] def write_tax_to_data(self):
"""Gets called when any images or Wingbeats get saved to disc.
:return: copy of ``data_template.json`` dictionary filled with taxonomy information and GBIF IDs from \
``species_template.json`` dictionary corresponding to AutocompleteInput value
:rtype: json dictionary
"""
# deepcopy template, insert dummy data e.g. datetime into duplicate
data = copy.deepcopy(self.data_template_dict)
# get index of selected species/genus/family
if self.auto_comp.value in self.auto_comp.completions:
index_auto_comp_list = self.auto_comp.completions.index(self.auto_comp.value)
lookup_index = self.lookup_indices[index_auto_comp_list]
synonym = self.synonym_list[index_auto_comp_list]
data['main_classifications'][0]['type'] = "HUMAN"
data['main_classifications'][0]['probability'] = self.confidence.value
data['main_classifications'][0]['gender'] = self.labels_gender[self.gender.active].upper()
# fill in order/family and if possible genus/species information into data from species_template
data['main_classifications'][0]['order']['name'] = \
self.species_template_dict["results"][lookup_index]['order']
data['main_classifications'][0]['order']['gbif_id'] = \
self.species_template_dict["results"][lookup_index]['orderKey']
data['main_classifications'][0]['family']['name'] = \
self.species_template_dict["results"][lookup_index]['family']
data['main_classifications'][0]['family']['gbif_id'] = \
self.species_template_dict["results"][lookup_index]['familyKey']
if any(entry in self.taxonomy_select.value for entry in ["genus", "species"]):
data['main_classifications'][0]['genus']['name'] = \
self.species_template_dict["results"][lookup_index]['genus']
data['main_classifications'][0]['genus']['gbif_id'] = \
self.species_template_dict["results"][lookup_index]['genusKey']
if self.taxonomy_select.value == "species":
if not synonym:
data['main_classifications'][0]['species']['name'] = \
self.species_template_dict["results"][lookup_index]['species']
data['main_classifications'][0]['species']['gbif_id'] = \
self.species_template_dict["results"][lookup_index]['speciesKey']
else:
data['main_classifications'][0]['species']['name'] = \
self.species_template_dict["results"][lookup_index]['canonicalName']
data['main_classifications'][0]['species']['gbif_id'] = \
self.species_template_dict["results"][lookup_index]['key']
return data