Source code for sensors.GUIclasses.taxonomy_autocomplete_widget

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 make_widget(self): """ Creates the autocomplete widget for species selection. :return: Bokeh widget for species selection from static dictionary exported from GBIF database :rtype: Bokeh object """ # widget's information Div self.text_class = Div(text=self.template.format(status_text='Classification', background='#eaeaea', colour='#000000', w='310px')) # define buttons self.taxonomy_select = Select(title="", value=self.TAX_LABELS[-1], options=self.TAX_LABELS, width=100) self.taxonomy_select.on_change("value", self.change_tax_list) self.taxonomy_select.disabled = True # define autocomplete, select and spinner widgets self.lookup_indices = self.species_indices self.auto_comp = AutocompleteInput(completions=self.species_list, min_characters=1, case_sensitive=False) self.auto_comp.on_change('value_input', self.update_any_change) self.auto_comp.on_change('value', self.update_selected) self.auto_comp.disabled = True self.confidence = Spinner(low=0, high=1, step=0.05, value=0, width=75) self.confidence.on_change('value', self.update_spinner) self.tax_status = Div(text=self.template.format(status_text='Order Family', background='#999999', colour='#FFFFFF', w='310px'), render_as_text=False) self.labels_gender = ['unspecified', 'male', 'female'] self.gender = RadioButtonGroup(labels=self.labels_gender, active=0) self.gender.disabled = True confidence_text = Div(text=self.template.format(status_text='Confidence', background='#999999', colour='#FFFFFF', w='112px'), render_as_text=False) return column(self.text_class, row(self.taxonomy_select, row(confidence_text, sizing_mode='stretch_both'), self.confidence), self.tax_status, self.auto_comp, self.gender)
[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