diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 7bbc71c..0000000 --- a/.gitignore +++ /dev/null @@ -1,101 +0,0 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -env/ -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -*.egg-info/ -.installed.cfg -*.egg - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -.hypothesis/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# pyenv -.python-version - -# celery beat schedule file -celerybeat-schedule - -# SageMath parsed files -*.sage.py - -# dotenv -.env - -# virtualenv -.venv -venv/ -ENV/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ diff --git a/code_BigReport.py b/code_BigReport.py index 3ef9e09..531a3e0 100644 --- a/code_BigReport.py +++ b/code_BigReport.py @@ -5,6 +5,7 @@ import code_Individual import code_Lists import code_MapHtml +import code_Stylesheet # import basic Python libraries from copy import deepcopy @@ -48,6 +49,9 @@ class BigReport(QMdiSubWindow, form_BigReport.Ui_frmBigReport): def __init__(self): super(self.__class__, self).__init__() self.setupUi(self) + + self.setAttribute(Qt.WA_DeleteOnClose,True) + self.mdiParent = "" self.myHtml = "" self.resized.connect(self.resizeMe) @@ -130,6 +134,7 @@ def CreateLocation(self, callingWidget): sub.show() QApplication.restoreOverrideCursor() + def CreateIndividual(self, callingWidget): QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) if callingWidget.objectName() in (["lstSpecies", @@ -158,6 +163,7 @@ def CreateIndividual(self, callingWidget): sub.resizeMe() QApplication.restoreOverrideCursor() + def CreateSpeciesList(self, callingWidget): QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) if callingWidget.objectName() == "lstDates": @@ -175,10 +181,15 @@ def CreateSpeciesList(self, callingWidget): sub.show() QApplication.restoreOverrideCursor() + def FillAnalysisReport(self, filter): # save filter for later use self.filter = filter + # set up a bold font to use in columns as needed + font = QFont() + font.setBold(True) + # create subset of master sightings list for this filter self.filteredSightingList = deepcopy(self.mdiParent.db.GetSightings(filter)) filteredSightingList = self.filteredSightingList @@ -187,15 +198,15 @@ def FillAnalysisReport(self, filter): # get species and first/last date data from db speciesListWithDates = self.mdiParent.db.GetSpeciesWithData(filter, self.filteredSightingList, "Subspecies") - # abort if filter produced no sightings + # abort if filter produced no sightings if len(speciesListWithDates) == 0: return(False) - # set up tblSpecies column headers and widths - self.tblSpecies.setColumnCount(4) + # set up tblSpecies column headers and widths + self.tblSpecies.setColumnCount(5) self.tblSpecies.setRowCount(len(speciesListWithDates)) self.tblSpecies.horizontalHeader().setVisible(True) - self.tblSpecies.setHorizontalHeaderLabels(['Tax', 'Species', 'First', 'Last']) + self.tblSpecies.setHorizontalHeaderLabels(['Tax', 'Species', 'Count', 'First', 'Last']) header = self.tblSpecies.horizontalHeader() header.setSectionResizeMode(1, QHeaderView.Stretch) self.tblSpecies.setShowGrid(False) @@ -207,16 +218,29 @@ def FillAnalysisReport(self, filter): taxItem.setData(Qt.DisplayRole, R+1) speciesItem = QTableWidgetItem() speciesItem.setText(species[0]) - speciesItem.setData(Qt.UserRole, QVariant(species[4])) + speciesItem.setData(Qt.UserRole, QVariant(species[4])) + countItem = QTableWidgetItem() + countItem.setData(Qt.DisplayRole, species[7]) firstDateItem = QTableWidgetItem() firstDateItem.setData(Qt.DisplayRole, species[1]) lastDateItem = QTableWidgetItem() lastDateItem.setData(Qt.DisplayRole, species[2]) self.tblSpecies.setItem(R, 0, taxItem) self.tblSpecies.setItem(R, 1, speciesItem) - self.tblSpecies.setItem(R, 2, firstDateItem) - self.tblSpecies.setItem(R, 3, lastDateItem) - + self.tblSpecies.setItem(R, 2, countItem) + self.tblSpecies.setItem(R, 3, firstDateItem) + self.tblSpecies.setItem(R, 4, lastDateItem) + + # set the species column to bold font (set up above) + self.tblSpecies.item(R, 1).setFont(font) + + # color code the entry. Stylesheet color for full species, gray if not + # set the species to gray if it's not a true species + if " x " in species[0] or "sp." in species[0] or "/" in species[0]: + self.tblSpecies.item(R, 1).setForeground(Qt.gray) + else: + self.tblSpecies.item(R, 1).setForeground(code_Stylesheet.speciesColor) + self.speciesList.append(species[4]) R = R + 1 @@ -228,6 +252,8 @@ def FillAnalysisReport(self, filter): if len(listDates) > 0: self.lstDates.setCurrentRow(0) self.FillSpeciesForDate() + + self.lblDatesSeen.setText("Dates: " + str(len(listDates))) # ****Setup Locations page**** listLocations = self.mdiParent.db.GetLocations(filter, "OnlyLocations", filteredSightingList) @@ -237,7 +263,9 @@ def FillAnalysisReport(self, filter): if len(listLocations) > 0: self.lstLocations.setCurrentRow(0) self.FillSpeciesForLocation() - self.lblLocations.setText("Locations (" + str(len(listLocations)) + ")") + + self.lblLocations.setText("Locations: " + str(len(listLocations))) + self.lblLocationsVisited.setText("Locations: " + str(len(listLocations))) # ****Setup New Species for Dates page**** speciesListFilter = code_Filter.Filter() @@ -256,8 +284,12 @@ def FillAnalysisReport(self, filter): header.setSectionResizeMode(1, QHeaderView.Stretch) self.tblNewYearSpecies.setShowGrid(False) + count = 0 + nonSpeciesTaxaCount = 0 + if len(yearSpecies) > 0: - R = 1 + + R = 0 for ys in yearSpecies: yearItem = QTableWidgetItem() yearItem.setText(ys[0]) @@ -265,12 +297,27 @@ def FillAnalysisReport(self, filter): newYearSpeciesItem.setText(ys[1]) self.tblNewYearSpecies.setItem(R, 0, yearItem) self.tblNewYearSpecies.setItem(R, 1, newYearSpeciesItem) + self.tblNewYearSpecies.item(R, 1).setFont(font) + + # color code the entry. Stylesheet color for full species, gray if not + # set the species to gray if it's not a true species + if " x " in ys[1] or "sp." in ys[1] or "/" in ys[1]: + self.tblNewYearSpecies.item(R, 1).setForeground(Qt.gray) + nonSpeciesTaxaCount += 1 + else: + self.tblNewYearSpecies.item(R, 1).setForeground(code_Stylesheet.speciesColor) + count += 1 + R = R + 1 - self.tblNewYearSpecies.removeRow(0) - - self.lblNewYearSpecies.setText("New year species (" + str(len(yearSpecies)) + ")") + + labelText = "New year species: " + str(count) + + if nonSpeciesTaxaCount > 0: + labelText = labelText + " + " + str(nonSpeciesTaxaCount) + " other taxa" + + self.lblNewYearSpecies.setText(labelText) - # set up tblNewMonthSpecies column headers and widths + # set up tblNewMonthSpecies column headers and widths self.tblNewMonthSpecies.setColumnCount(2) self.tblNewMonthSpecies.setRowCount(len(monthSpecies)+1) self.tblNewMonthSpecies.horizontalHeader().setVisible(False) @@ -278,8 +325,11 @@ def FillAnalysisReport(self, filter): header.setSectionResizeMode(1, QHeaderView.Stretch) self.tblNewMonthSpecies.setShowGrid(False) + count = 0 + nonSpeciesTaxaCount = 0 + if len(monthSpecies) > 0: - R = 1 + R = 0 for ms in monthSpecies: monthItem = QTableWidgetItem() monthItem.setText(ms[0]) @@ -287,17 +337,55 @@ def FillAnalysisReport(self, filter): newMonthSpeciesItem.setText(ms[1]) self.tblNewMonthSpecies.setItem(R, 0, monthItem) self.tblNewMonthSpecies.setItem(R, 1, newMonthSpeciesItem) - R = R + 1 - self.tblNewMonthSpecies.removeRow(0) - - self.lblNewMonthSpecies.setText("New month species (" + str(len(monthSpecies)) + ")") + self.tblNewMonthSpecies.item(R, 1).setFont(font) + + # color code the entry. Stylesheet color for full species, gray if not + # set the species to gray if it's not a true species + if " x " in ms[1] or "sp." in ms[1] or "/" in ms[1]: + self.tblNewMonthSpecies.item(R, 1).setForeground(Qt.gray) + nonSpeciesTaxaCount += 1 + else: + self.tblNewMonthSpecies.item(R, 1).setForeground(code_Stylesheet.speciesColor) + count += 1 + + R += 1 + + labelText = "New month species: " + str(count) + + if nonSpeciesTaxaCount > 0: + labelText = labelText + " + " + str(nonSpeciesTaxaCount) + " other taxa" + + self.lblNewMonthSpecies.setText(labelText) - # set up lstNewLifeSpecies + # set up lstNewLifeSpecies if len(lifeSpecies) > 0: - self.lstNewLifeSpecies.addItems(lifeSpecies) + + R = 0 + + for ls in lifeSpecies: + + self.lstNewLifeSpecies.addItem(ls) + + self.lstNewLifeSpecies.item(R).setFont(font) + + if "/" in ls or "sp." in ls or " x " in ls: + self.lstNewLifeSpecies.item(R).setForeground(Qt.gray) + + else: + self.lstNewLifeSpecies.item(R).setForeground(code_Stylesheet.speciesColor) + + R += 1 + self.lstNewLifeSpecies.setSpacing(2) - - self.lblNewLifeSpecies.setText("New life species (" + str(len(lifeSpecies)) + ")") + + count = self.mdiParent.db.CountSpecies(lifeSpecies) + nonSpeciesTaxaCount = len(lifeSpecies) - count + + labelText = "New life species: " + str(count) + if nonSpeciesTaxaCount > 0: + labelText = labelText + " + " + str(nonSpeciesTaxaCount) + " other taxa" + + self.lblNewLifeSpecies.setText(labelText) # ****Setup new Location Species page**** countrySpecies = self.mdiParent.db.GetNewCountrySpecies(filter, filteredSightingList, sightingListForSpeciesSubset, self.speciesList) @@ -313,18 +401,39 @@ def FillAnalysisReport(self, filter): header.setSectionResizeMode(1, QHeaderView.Stretch) self.tblNewCountrySpecies.setShowGrid(False) + count = 0 + nonSpeciesTaxaCount = 0 + if len(countrySpecies) > 0: R = 0 - for ms in countrySpecies: + for cs in countrySpecies: + countryItem = QTableWidgetItem() - countryItem.setText(self.mdiParent.db.GetCountryName(ms[0])) + countryItem.setText(self.mdiParent.db.GetCountryName(cs[0])) newCountrySpeciesItem = QTableWidgetItem() - newCountrySpeciesItem.setText(ms[1]) + newCountrySpeciesItem.setText(cs[1]) self.tblNewCountrySpecies.setItem(R, 0, countryItem) self.tblNewCountrySpecies.setItem(R, 1, newCountrySpeciesItem) + # color code the entry. Stylesheet color for full species, gray if not + # set the species to gray if it's not a true species + + self.tblNewCountrySpecies.item(R, 1).setFont(font) + + if " x " in cs[1] or "sp." in cs[1] or "/" in cs[1]: + self.tblNewCountrySpecies.item(R, 1).setForeground(Qt.gray) + nonSpeciesTaxaCount += 1 + + else: + self.tblNewCountrySpecies.item(R, 1).setForeground(code_Stylesheet.speciesColor) + count += 1 + R = R + 1 - - self.lblNewCountrySpecies.setText("New country species (" + str(len(countrySpecies)) + ")") + labelText = "New country species: " + str(count) + + if nonSpeciesTaxaCount > 0: + labelText = labelText + " + " + str(nonSpeciesTaxaCount) + " taxa" + + self.lblNewCountrySpecies.setText(labelText) # set up tblNewStateSpecies column headers and widths self.tblNewStateSpecies.setColumnCount(2) @@ -334,20 +443,43 @@ def FillAnalysisReport(self, filter): header.setSectionResizeMode(1, QHeaderView.Stretch) self.tblNewStateSpecies.setShowGrid(False) + count = 0 + nonSpeciesTaxaCount = 0 + if len(stateSpecies) > 0: R = 0 - for ms in stateSpecies: + for ss in stateSpecies: stateItem = QTableWidgetItem() - stateItem.setText(self.mdiParent.db.GetStateName(ms[0])) + stateItem.setText(self.mdiParent.db.GetStateName(ss[0])) newStateSpeciesItem = QTableWidgetItem() - newStateSpeciesItem.setText(ms[1]) + newStateSpeciesItem.setText(ss[1]) self.tblNewStateSpecies.setItem(R, 0, stateItem) self.tblNewStateSpecies.setItem(R, 1, newStateSpeciesItem) + + self.tblNewStateSpecies.item(R, 1).setFont(font) + + if " x " in ss[1] or "sp." in ss[1] or "/" in ss[1]: + self.tblNewStateSpecies.item(R, 1).setForeground(Qt.gray) + nonSpeciesTaxaCount += 1 + + else: + self.tblNewStateSpecies.item(R, 1).setForeground(code_Stylesheet.speciesColor) + count += 1 + R = R + 1 - self.tblNewStateSpecies.sortByColumn(0, Qt.AscendingOrder) - - self.lblNewStateSpecies.setText("New state species (" + str(len(stateSpecies)) + ")") - + + labelText = "New state species: " + str(count) + + if nonSpeciesTaxaCount > 0: + labelText = labelText + " + " + str(nonSpeciesTaxaCount) + " taxa" + + self.lblNewStateSpecies.setText(labelText) + + self.tblNewStateSpecies.sortByColumn(0, Qt.AscendingOrder) + + count = 0 + nonSpeciesTaxaCount = 0 + # set up tblNewCountySpecies column headers and widths self.tblNewCountySpecies.setColumnCount(2) self.tblNewCountySpecies.setRowCount(len(countySpecies)) @@ -358,16 +490,35 @@ def FillAnalysisReport(self, filter): if len(countySpecies) > 0: R = 0 - for ms in countySpecies: + for cs in countySpecies: countyItem = QTableWidgetItem() - countyItem.setText(ms[0]) + countyItem.setText(cs[0]) newCountySpeciesItem = QTableWidgetItem() - newCountySpeciesItem.setText(ms[1]) + newCountySpeciesItem.setText(cs[1]) self.tblNewCountySpecies.setItem(R, 0, countyItem) self.tblNewCountySpecies.setItem(R, 1, newCountySpeciesItem) + + self.tblNewCountySpecies.item(R, 1).setFont(font) + + if " x " in cs[1] or "sp." in cs[1] or "/" in cs[1]: + self.tblNewCountySpecies.item(R, 1).setForeground(Qt.gray) + nonSpeciesTaxaCount += 1 + + else: + self.tblNewCountySpecies.item(R, 1).setForeground(code_Stylesheet.speciesColor) + count += 1 + R = R + 1 - - self.lblNewCountySpecies.setText("New county species (" + str(len(countySpecies)) + ")") + + labelText = "New county species: " + str(count) + + if nonSpeciesTaxaCount > 0: + labelText = labelText + " + " + str(nonSpeciesTaxaCount) + " taxa" + + self.lblNewCountySpecies.setText(labelText) + + count = 0 + nonSpeciesTaxaCount = 0 # set up tblNewLocationSpecies column headers and widths self.tblNewLocationSpecies.setColumnCount(2) @@ -379,22 +530,44 @@ def FillAnalysisReport(self, filter): if len(locationSpecies) > 0: R = 0 - for ms in locationSpecies: + for ls in locationSpecies: locationItem = QTableWidgetItem() - locationItem.setText(ms[0]) + locationItem.setText(ls[0]) newLocationSpeciesItem = QTableWidgetItem() - newLocationSpeciesItem.setText(ms[1]) + newLocationSpeciesItem.setText(ls[1]) self.tblNewLocationSpecies.setItem(R, 0, locationItem) self.tblNewLocationSpecies.setItem(R, 1, newLocationSpeciesItem) + + self.tblNewLocationSpecies.item(R, 1).setFont(font) + + if " x " in ls[1] or "sp." in ls[1] or "/" in ls[1]: + self.tblNewLocationSpecies.item(R, 1).setForeground(Qt.gray) + nonSpeciesTaxaCount += 1 + + else: + self.tblNewLocationSpecies.item(R, 1).setForeground(code_Stylesheet.speciesColor) + count += 1 + R = R + 1 - - self.lblNewLocationSpecies.setText("New location species (" + str(len(locationSpecies)) + ")") + + labelText = "New location species: " + str(count) + + if nonSpeciesTaxaCount > 0: + labelText = labelText + " + " + str(nonSpeciesTaxaCount) + " taxa" + + self.lblNewLocationSpecies.setText(labelText) # ****Setup window's main labels**** - # set main species seen lable text + # set main species seen label text count = self.mdiParent.db.CountSpecies(self.speciesList) + nonSpeciesTaxaCount = self.tblSpecies.rowCount() - count - self.lblTopSpeciesSeen.setText("Species seen: " + str(count)) + labelText = "Species: " + str(count) + + if nonSpeciesTaxaCount > 0: + labelText = labelText + " + " + str(nonSpeciesTaxaCount) + " taxa" + + self.lblTopSpeciesSeen.setText(labelText) # set main location label, using "All Locations" if none others are selected self.mdiParent.SetChildDetailsLabels(self, filter) @@ -427,7 +600,34 @@ def FillSpeciesForDate(self): self.lstSpecies.addItems(speciesList) self.lstSpecies.setSpacing(2) - self.lblSpeciesSeen.setText("Species seen on selected date (" + str(len(speciesList)) + "):") + count = 0 + nonSpeciesTaxaCount = 0 + + font = QFont() + font.setBold(True) + + for R in range(self.lstSpecies.count()): + + self.lstSpecies.item(R).setFont(font) + + speciesName = self.lstSpecies.item(R).text() + + if " x " in speciesName or "sp." in speciesName or "/" in speciesName: + self.lstSpecies.item(R).setForeground(Qt.gray) + nonSpeciesTaxaCount += 1 + + else: + self.lstSpecies.item(R).setForeground(code_Stylesheet.speciesColor) + count += 1 + + R = R + 1 + + labelText = "Species: " + str(count) + + if nonSpeciesTaxaCount > 0: + labelText = labelText + " + " + str(nonSpeciesTaxaCount) + " taxa" + + self.lblSpeciesSeen.setText(labelText) def FillMap(self): @@ -480,8 +680,58 @@ def FillSpeciesForLocation(self): self.lstLocationUniqueSpecies.addItems(uniqueSpecies) self.lstLocationUniqueSpecies.setSpacing(2) - self.lblLocationSpecies.setText("Species at selected location (" + str(len(speciesList)) + ")") - self.lblLocationUniqueSpecies.setText("Species seen ONLY at selected location (" + str(len(uniqueSpecies)) + ")") + count = 0 + nonSpeciesTaxaCount = 0 + + # set up a bold font + font = QFont() + font.setBold(True) + + for R in range(self.lstLocationSpecies.count()): + + # set font to bold for all entries + self.lstLocationSpecies.item(R).setFont(font) + + # color code the entry. Stylesheet color for full species, gray if not + # set the species to gray if it's not a true species + if " x " in self.lstLocationSpecies.item(R).text() or "sp." in self.lstLocationSpecies.item(R).text() or "/" in self.lstLocationSpecies.item(R).text(): + self.lstLocationSpecies.item(R).setForeground(Qt.gray) + nonSpeciesTaxaCount += 1 + else: + self.lstLocationSpecies.item(R).setForeground(code_Stylesheet.speciesColor) + count += 1 + + labelText = "Species: " + str(count) + + if nonSpeciesTaxaCount > 0: + labelText = labelText + " + " + str(nonSpeciesTaxaCount) + " taxa" + + self.lblLocationSpecies.setText(labelText) + + # reset counts + count = 0 + nonSpeciesTaxaCount = 0 + + for R in range(self.lstLocationUniqueSpecies.count()): + + # set font to bold for all entries + self.lstLocationUniqueSpecies.item(R).setFont(font) + + # color code the entry. Stylesheet color for full species, gray if not + # set the species to gray if it's not a true species + if " x " in self.lstLocationUniqueSpecies.item(R).text() or "sp." in self.lstLocationUniqueSpecies.item(R).text() or "/" in self.lstLocationUniqueSpecies.item(R).text(): + self.lstLocationUniqueSpecies.item(R).setForeground(Qt.gray) + nonSpeciesTaxaCount += 1 + else: + self.lstLocationUniqueSpecies.item(R).setForeground(code_Stylesheet.speciesColor) + count += 1 + + labelText = "Observed only at location: " + str(count) + + if nonSpeciesTaxaCount > 0: + labelText = labelText + " + " + str(nonSpeciesTaxaCount) + " taxa" + + self.lblLocationUniqueSpecies.setText(labelText) def TblSpeciesClicked(self): @@ -1132,14 +1382,14 @@ def setDateFilter(self): def setFirstDateFilter(self): # get location name and type from focus widget. Varies for tables. if self.focusWidget().objectName() == "tblSpecies": - date = self.focusWidget().item(self.focusWidget().currentRow(), 2).text() + date = self.focusWidget().item(self.focusWidget().currentRow(), 3).text() self.mdiParent.setDateFilter(date) def setLastDateFilter(self): # get location name and type from focus widget. Varies for tables. if self.focusWidget().objectName() == "tblSpecies": - date = self.focusWidget().item(self.focusWidget().currentRow(), 3).text() + date = self.focusWidget().item(self.focusWidget().currentRow(), 4).text() self.mdiParent.setDateFilter(date) @@ -1241,7 +1491,10 @@ def scaleMe(self): self.tblNewCountySpecies ]): header = t.horizontalHeader() - header.resizeSection(0, floor(1.2 * textWidth)) + if t == self.tblNewYearSpecies or t == self.tblNewMonthSpecies: + header.resizeSection(0, floor(.6 * textWidth)) + else: + header.resizeSection(0, floor(textWidth)) for r in range(t.rowCount()): t.setRowHeight(r, textHeight * 1.1) diff --git a/code_Compare.py b/code_Compare.py index 99a6c01..d2b690f 100644 --- a/code_Compare.py +++ b/code_Compare.py @@ -1,6 +1,7 @@ # import the GUI forms that we create with Qt Creator import code_Individual import form_Compare +import code_Stylesheet # import the Qt components we'll use # do this so later we won't have to clutter our code with references to parent Qt classes @@ -10,7 +11,8 @@ ) from PyQt5.QtCore import ( - pyqtSignal + pyqtSignal, + Qt ) from PyQt5.QtWidgets import ( @@ -34,6 +36,7 @@ class Compare(QMdiSubWindow, form_Compare.Ui_frmCompare): def __init__(self): super(self.__class__, self).__init__() self.setupUi(self) + self.setAttribute(Qt.WA_DeleteOnClose,True) self.mdiParent = "" self.resized.connect(self.resizeMe) self.btnCompare.clicked.connect(self.CompareLists) @@ -43,6 +46,13 @@ def __init__(self): self.webView = QWebEngineView() self.myPrinter = QPrinter(QPrinter.HighResolution) + red = str(code_Stylesheet.speciesColor.red()) + green = str(code_Stylesheet.speciesColor.green()) + blue = str(code_Stylesheet.speciesColor.blue()) + + self.lstLeftOnly.setStyleSheet("QListWidget {color: rgb(" + red + "," + green + "," + blue + "); font-weight: bold}") + self.lstRightOnly.setStyleSheet("QListWidget {color: rgb(" + red + "," + green + "," + blue + "); font-weight: bold}") + self.lstBoth.setStyleSheet("QListWidget {color: rgb(" + red + "," + green + "," + blue + "); font-weight: bold}") def html(self): @@ -198,7 +208,8 @@ def CompareLists(self): for s in window.currentSpeciesList: if "(" in s: s = s.split(" (")[0] - leftListSpecies.append(s) + if "/" not in s and " x " not in s and "sp." not in s: + leftListSpecies.append(s) # get right list species rightListSpecies = [] @@ -209,7 +220,8 @@ def CompareLists(self): for s in window.currentSpeciesList: if "(" in s: s = s.split(" (")[0] - rightListSpecies.append(s) + if "/" not in s and " x " not in s and "sp." not in s: + rightListSpecies.append(s) bothLists = [] leftListOnly = [] @@ -244,9 +256,9 @@ def CompareLists(self): self.lstBoth.setSpacing(2) self.lstRightOnly.addItems(rightListOnly) self.lstRightOnly.setSpacing(2) - self.lblLeftListOnly.setText("Species only on This List (" + str(self.lstLeftOnly.count())+")") - self.lblBothLists.setText("Species on Both Lists (" + str(self.lstBoth.count()) + ")") - self.lblRightListOnly.setText("Species only on This List (" + str(self.lstRightOnly.count()) + ")") + self.lblLeftListOnly.setText("Species only on this list: " + str(self.lstLeftOnly.count())) + self.lblBothLists.setText("Species on both lists: " + str(self.lstBoth.count())) + self.lblRightListOnly.setText("Species only on this list: " + str(self.lstRightOnly.count())) def CreateNewIndividual(self, speciesName): @@ -275,7 +287,7 @@ def resizeMe(self): def scaleMe(self): scaleFactor = self.mdiParent.scaleFactor - windowWidth = 800 * scaleFactor + windowWidth = 900 * scaleFactor windowHeight = 500 * scaleFactor self.resize(windowWidth, windowHeight) diff --git a/code_DataBase.py b/code_DataBase.py index 3dfbc7c..d401ba1 100644 --- a/code_DataBase.py +++ b/code_DataBase.py @@ -3,11 +3,18 @@ import csv import zipfile import io +import sys from copy import deepcopy from collections import defaultdict +import datetime +import code_Filter +import json +import piexif +from math import floor +from natsort import natsorted from PyQt5.QtWidgets import ( - QApplication, + QApplication, QMessageBox ) @@ -22,12 +29,22 @@ def __init__(self): self.orderList = [] self.masterFamilyOrderList = [] self.masterLocationList = [] + self.regionList = [] self.countryList = [] self.stateList = [] self.countyList = [] self.locationList = [] + self.validPhotoSpecies = [] + self.cameraList = [] + self.lensList = [] + self.shutterSpeedList = [] + self.apertureList = [] + self.focalLengthList=[] + self.isoList = [] self.sightingList = [] self.eBirdFileOpenFlag = False + self.photoDataFileOpenFlag = False + self.photosNeedSaving = False self.countryStateCodeFileFound = False self.speciesDict = defaultdict() self.yearDict = defaultdict() @@ -40,51 +57,528 @@ def __init__(self): self.checklistDict = defaultdict() self.familySpeciesDict = defaultdict() self.orderSpeciesDict = defaultdict() + self.bblCodeDict = defaultdict() + self.photoDataFile = "" + self.startupFolder = "" self.monthNameDict = ({ - "01":"Jan", - "02":"Feb", - "03":"Mar", - "04":"Apr", - "05":"May", - "06":"Jun", - "07":"Jul", - "08":"Aug", - "09":"Sep", - "10":"Oct", - "11":"Nov", + "01":"Jan", + "02":"Feb", + "03":"Mar", + "04":"Apr", + "05":"May", + "06":"Jun", + "07":"Jul", + "08":"Aug", + "09":"Sep", + "10":"Oct", + "11":"Nov", "12":"Dec" }) self.monthNumberDict = ({ - "Jan":"01", - "Feb":"02", - "Mar":"03", - "Apr":"04", - "May":"05", - "Jun":"06", - "Jul":"07", - "Aug":"08", - "Sep":"09", - "Oct":"10", - "Nov":"11", + "Jan":"01", + "Feb":"02", + "Mar":"03", + "Apr":"04", + "May":"05", + "Jun":"06", + "Jul":"07", + "Aug":"08", + "Sep":"09", + "Oct":"10", + "Nov":"11", "Dec":"12" }) + self.regionNameDict = ({ + "ABA":"ABA Area", + "AOU":"AOU Area", + "NAM":"North America", + "SAM":"South America", + "AFR":"Africa", + "EUR":"Europe", + "ASI":"Asia", + "SPO":"South Polar", + "WHE":"Western Hemisphere", + "EHE":"Eastern Hemisphere", + "WIN":"West Indies", + "CAM":"Central America", + "USL":"USA Lower 48", + "AUA":"Australasia (ABA)", + "AUE":"Australasia (eBird)", + "AUS":"Australia and Territories" + }) + self.regionCodeDict = ({ + "ABA Area":"ABA", + "AOU Area":"AOU", + "North America":"NAM", + "South America":"SAM", + "Africa":"AFR", + "Europe":"EUR", + "Asia":"ASI", + "South Polar":"SPO", + "Western Hemisphere":"WHE", + "Eastern Hemisphere":"EHE", + "West Indies":"WIN", + "Central America":"CAM", + "USA Lower 48":"USL", + "Australasia (ABA)":"AUA", + "Australasia (eBird)":"AUE", + "Australia and Territories":"AUS" + }) + + # look for json files. They must be in the same directory as the script directory + if getattr(sys, 'frozen', False): + # frozen + scriptDirectory = os.path.dirname(sys.executable) + else: + # unfrozen + scriptDirectory = os.path.dirname(os.path.realpath(__file__)) + + # format file names so we can find them regardless of whether we're frozen + stateJsonFile = os.path.join(scriptDirectory, "us-states.json") + countyJsonFile = os.path.join(scriptDirectory, "us-counties-lower48.json") + countryJsonFile = os.path.join(scriptDirectory, "world-countries.json") + + # load us-states json shape file for later use with choropleths + self.state_geo = json.loads(open(stateJsonFile).read()) + +# # create list of US state abbreviations for later use with choropleths +# self.stateCodeList = set() +# for s in self.state_geo["features"]: +# self.stateCodeList.add(s["id"]) +# self.stateCodeList = list(self.stateCodeList) + + # load us-counties-lower48 json shape file for later use with choropleths + self.county_geo = json.loads(open(countyJsonFile).read()) + + # save county code data for easy lookup when processing eBird data file + self.countyCodeDict = defaultdict() + self.countyCodeList = set() + for c in self.county_geo["features"]: + + # save master list of all US county codes for use with choropleths + self.countyCodeList.add(c["id"]) + + if c["properties"]["name"] not in self.countyCodeDict.keys(): + self.countyCodeDict[c["properties"]["name"]] = [[c["id"],c["properties"]["state"]]] + + else: + self.countyCodeDict[c["properties"]["name"]].append([c["id"],c["properties"]["state"]]) + +# self.countyCodeList = list(self.countyCodeList) + + # load world-countries json shape file for later use with choropleths + self.country_geo = json.loads(open(countryJsonFile).read()) + + + + def matchPhoto(self, file): + + # method to suggest a commonName, date, time, and location for a photo + + # get just the filename portion from the full-path filename + fileName = os.path.basename(file) + + # get the EXIF data from the file, if possible + try: + photoExif = piexif.load(file) + except: + photoExif = "" + + # get photo date from EXIF + try: + photoDateTime = photoExif["Exif"][piexif.ExifIFD.DateTimeOriginal].decode("utf-8") + + # parse EXIF data for date/time components + photoDate = photoDateTime[0:4] + "-" + photoDateTime[5:7] + "-" + photoDateTime[8:10] + photoTime = photoDateTime[11:13] + ":" + photoDateTime[14:16] + except: + photoDate = "" + photoTime = "" + + # use date and time to select a sighting location from db + filter = code_Filter.Filter() + filter.setStartDate(photoDate) + filter.setEndDate(photoDate) + filter.setTime(photoTime) + locations = self.GetLocations(filter, "OnlyLocations") + + # if we find only one location, return it + if len(locations) == 1: + photoLocation = locations[0] + + else: + photoLocation = "" + + # if no single location matched the photo, but we have a date, find which checklist is closest to the photo datetime + if photoLocation == "" and photoDate != "": + # try finding a checklist with the right date + filter = code_Filter.Filter() + filter.setStartDate(photoDate) + filter.setEndDate(photoDate) + locations = self.GetLocations(filter, "OnlyLocations") + + if len(locations) == 1: + photoLocation = locations[0] + possibleStartTimes = self.GetStartTimes(filter) + if len(possibleStartTimes) > 0: + photoTime = possibleStartTimes[0] + + # if more than one location was found, find which was visited closest to the photo time + elif len(locations) > 1 and photoTime != "": + filter = code_Filter.Filter() + filter.setStartDate(photoDate) + filter.setEndDate(photoDate) + checklists = self.GetChecklists(filter) + # create list to store location and checklist time for closest one to our photo's datetime + # third list entry starts with total minutes in a day (most gap possible) + checklistTimeDifference = ["", "", 24 * 60] + photoMinutesSinceMidnight = 60 * int(photoTime[0:2]) + int(photoTime[3:5]) + + for c in checklists: + + checklistTime = c[5] + + checklistDuration = c[7] + if checklistDuration == "": + checklistDuration = "0" + + checklistStartMinSinceMidnight = 60 * int(checklistTime[0:2]) + int(checklistTime[3:5]) + checklistEndMinSinceMidnight = checklistStartMinSinceMidnight + int(checklistDuration) + + # check if this checklist's time is closer to our photo's time than any other checklist looped through so far + # If so, save its data into checklistTimeDifference + if abs(photoMinutesSinceMidnight - checklistStartMinSinceMidnight) < checklistTimeDifference[2]: + checklistTimeDifference = [c[3], c[5], abs(photoMinutesSinceMidnight - checklistStartMinSinceMidnight)] + + if abs(photoMinutesSinceMidnight - checklistEndMinSinceMidnight) < checklistTimeDifference[2]: + checklistTimeDifference = [c[3], c[5], abs(photoMinutesSinceMidnight - checklistEndMinSinceMidnight)] + + if checklistTimeDifference[0] != "": + photoLocation = checklistTimeDifference[0] + photoTime = checklistTimeDifference[1] + + + # try to use file name to match commonName using date, time, and location found already + if photoLocation != "" and photoDate != "" and photoTime != "": + filter = code_Filter.Filter() + filter.setLocationType("Location") + filter.setLocationName(photoLocation) + filter.setStartDate(photoDate) + filter.setEndDate(photoDate) + filter.setTime(photoTime) + possibleCommonNames = self.GetSpecies(filter) + + # set up translation table to remove inconvenient characters from file name + # and possiblecommonNames + translation_table = dict.fromkeys(map(ord, "0123456789-' "), None) + + # make file name lower case and remove inconvenient characters + fileName = str(fileName) + fileName = fileName.lower() + fileName = fileName.translate(translation_table) + + # create list to store most likely commonName and number of matching characters + mostLikelyCommonName = [0, "", ""] + + # cycle through possibleCommonNames, testing them piece by piece + # to find which matches the most number of consecutive characters + + for pcn in possibleCommonNames: + lowerCasePcn = pcn.lower() + lowerCasePcn = lowerCasePcn.translate(translation_table) + possibleCommonNameLength = len(lowerCasePcn) + for i in range(possibleCommonNameLength): + for ii in range(i + 1, possibleCommonNameLength + 1): + if lowerCasePcn[i:ii] in fileName: + if len(lowerCasePcn[i:ii]) > mostLikelyCommonName[0]: + mostLikelyCommonName[0] = len(lowerCasePcn[i:ii]) + mostLikelyCommonName[1] = pcn + mostLikelyCommonName[2] = lowerCasePcn[i:ii] + if len(lowerCasePcn[i:ii]) == len(lowerCasePcn): + # matched the full possible name, so stop checking + break + + photoCommonName = mostLikelyCommonName[1] + + else: + photoCommonName = "" + + photoMatchData = {} + photoMatchData["photoLocation"] = photoLocation + photoMatchData["photoDate"] = photoDate + photoMatchData["photoTime"] = photoTime + photoMatchData["photoCommonName"] = photoCommonName + + return(photoMatchData) + + + def removeUnfoundPhotos(self): + + count = 0 + + # method to remove photos from database + # create filter with no settings to retrieve every photo in db + filter = code_Filter.Filter() + + # get all sightings with photos + sightings = self.GetSightingsWithPhotos(filter) + + for s in sightings: + for p in s["photos"]: + if not os.path.isfile(p): + s["photos"].remove(p) + count += 1 + + # return the number of removed files + return(count) + + + def removePhotoFromDatabase(self, location, date, time, commonName, photoFileName): + + # method to remove photos from database + filter = code_Filter.Filter() + filter.setLocationType("Location") + filter.setLocationName(location) + if date != "": + filter.setStartDate(date) + filter.setEndDate(date) + if time != "": + filter.setTime(time) + filter.setSpeciesName(commonName) + + # get sightings that match the filter. Should only be a single sighting. + sightings = self.GetSightingsWithPhotos(filter) + + for s in sightings: + if "photos" in s.keys(): + for p in s["photos"]: + if p["fileName"] == photoFileName: + s["photos"].remove(p) + + # if we've removed all the photos for this sighting, we need to remove the "photos" entry in the dict + if len(s["photos"]) == 0: + s.pop("photos", None) + + + def getPhotoData(self, fileName): + photoData = {} + + # try to get EXIF data + + try: + exif_dict = piexif.load(fileName) + except: + exif_dict = "" + + try: + photoCamera = exif_dict["0th"][piexif.ImageIFD.Model].decode("utf-8") + except: + photoCamera = "" + try: + photoLens = exif_dict["Exif"][piexif.ExifIFD.LensModel].decode("utf-8") + except: + photoLens = "" + try: + shutterSpeedFraction = exif_dict["Exif"][piexif.ExifIFD.ExposureTime] + photoShutterSpeed = floor(shutterSpeedFraction[1]/shutterSpeedFraction[0]) + photoShutterSpeed = "1/" + str(photoShutterSpeed) + except: + photoShutterSpeed = "" + try: + photoAperture = exif_dict["Exif"][piexif.ExifIFD.FNumber] + photoAperture = round(photoAperture[0] / photoAperture[1], 1) + except: + photoAperture = "" + try: + photoISO = exif_dict["Exif"][piexif.ExifIFD.ISOSpeedRatings] + except: + photoISO = "" + try: + photoFocalLength = exif_dict["Exif"][piexif.ExifIFD.FocalLength] + photoFocalLength = floor(photoFocalLength[0] / photoFocalLength[1]) + photoFocalLength = str(photoFocalLength) + " mm" + except: + photoFocalLength = "" + + # get photo date from EXIF + try: + photoDateTime = exif_dict["Exif"][piexif.ExifIFD.DateTimeOriginal].decode("utf-8") + + #parse EXIF data for date/time components + photoExifDate = photoDateTime[0:4] + "-" + photoDateTime[5:7] + "-" + photoDateTime[8:10] + photoExifTime = photoDateTime[11:13] + ":" + photoDateTime[14:16] + except: + photoExifDate = "Date unknown" + photoExifTime = "Time unknown" + + photoData["fileName"] = fileName + photoData["camera"] = photoCamera + photoData["lens"] = photoLens + photoData["shutterSpeed"] = str(photoShutterSpeed) + photoData["aperture"] = str(photoAperture) + photoData["iso"] = str(photoISO) + photoData["focalLength"] = str(photoFocalLength) + photoData["time"] = photoExifTime + photoData["date"] = photoExifDate + photoData["rating"] = "0" + + return(photoData) + + + def addPhotoToDatabase(self, filter, photoData): + + # check that file still exists before proceeding + if os.path.isfile(photoData["fileName"]): + + # get sightings that match the filter. Should only be a single sighting. + sightings = self.GetSightings(filter) + + if len(sightings) > 0: + + s = sightings[0] + + if "photos" not in s.keys(): + s["photos"] = [photoData] + + else: + if photoData not in s["photos"]: + s["photos"].append(photoData) + + # add photoData to db lists + self.addPhotoDataToDb(photoData) + + + def writePhotoDataToFile(self, fileName): + + # get all sightings with photos + filter = code_Filter.Filter() + sightings = self.GetSightingsWithPhotos(filter) + + # set up writing CSV to fileName + with open(fileName, mode='w') as photoDataFile: + photoDataWriter = csv.writer(photoDataFile, delimiter=',', quotechar='"', quoting=csv.QUOTE_MINIMAL) + photoDataWriter.writerow(["ChecklistID", "CommonName", "FileName", "Camera", "Lens", "ShutterSpeed", "Aperture", "ISO", "FocalLength", "Rating"]) + + for s in sightings: + for p in s["photos"]: + try: + photoDataWriter.writerow([ + s["checklistID"], + s["commonName"], + p["fileName"], + p["camera"], + p["lens"], + p["shutterSpeed"], + p["aperture"], + p["iso"], + p["focalLength"], + p["rating"] + ]) + except IOError as err: + msg = QMessageBox() + msg.setIcon(QMessageBox.Warning) + msg.setText("Error occurred while saving the photo data to disk.\n" + s["commonName"] + " " + s["checklistID"] + "\n"+ str(err)) + msg.setWindowTitle("Save Error") + msg.setStandardButtons(QMessageBox.Ok) + msg.exec_() + + + def readPhotoDataFromFile(self, fileName): + + with open(fileName, mode='r') as photoDataFile: + csv_reader = csv.DictReader(photoDataFile) + + try: + + for row in csv_reader: + + photoData = {} + filter = code_Filter.Filter() + filter.setChecklistID(row["ChecklistID"]) + filter.setSpeciesName(row["CommonName"]) + + photoData["fileName"] = row["FileName"] + photoData["camera"] = row["Camera"] + photoData["lens"] = row["Lens"] + photoData["shutterSpeed"] = row["ShutterSpeed"] + photoData["aperture"] = row["Aperture"] + photoData["iso"] = row["ISO"] + photoData["focalLength"] = row["FocalLength"] + + if "Rating" in row.keys(): + if row["Rating"] in ["0", "1", "2", "3", "4", "5"]: + photoData["rating"] = row["Rating"] + else: + photoData["rating"] = "0" + else: + photoData["rating"] = "0" + + self.addPhotoToDatabase(filter, photoData) + + except: + pass + + self.photoDataFileOpenFlag = True + + + def refreshPhotoLists(self): + + self.cameraList = [] + self.lensList = [] + self.shutterSpeedList = [] + self.apertureList = [] + self.focalLengthList=[] + self.isoList = [] - def CountSpecies(self, speciesList): + cameraSet = set() + lensSet = set() + shutterSpeedSet = set() + apertureSet= set() + focalLengthSet = set() + isoSet = set() - # method to count true species in a list. Entries with parens, /, or sp. should not be counted, + for s in self.sightingList: + if "photos" in s.keys(): + for p in s["photos"]: + if p["camera"] != "": + cameraSet.add(p["camera"]) + if p["lens"] != "": + lensSet.add(p["lens"]) + if p["shutterSpeed"] != "": + shutterSpeedSet.add(p["shutterSpeed"]) + if p["aperture"] != "": + apertureSet.add(p["aperture"]) + if p["iso"] != "": + isoSet.add(p["iso"]) + if p["focalLength"] != "": + focalLengthSet.add(p["focalLength"]) + + self.cameraList = list(cameraSet) + self.lensList = list(lensSet) + self.shutterSpeedList = natsorted(list(shutterSpeedSet), reverse=True) + self.apertureList = natsorted(list(apertureSet)) + self.isoList = natsorted(list(isoSet)) + self.focalLengthList = natsorted(list(focalLengthSet)) + + + def CountSpecies(self, speciesList): + + # method to count true species in a list. Entries with parens, /, or sp. or hybrids should not be counted, # unless no non-paren entries exist for that species.append speciesSet = set() # use a set (which deletes duplicates) to hold species names # remove parens from species names if they exist for s in speciesList: - if "(" in s: + if "(" in s and " x " not in s: speciesSet.add(s.split(" (")[0]) elif "sp." in s: pass elif "/" in s: pass + elif " x " in s: + pass else: speciesSet.add(s) @@ -93,14 +587,15 @@ def CountSpecies(self, speciesList): return(count) - def ReadCountryStateCodeFile(self, dataFile): + + def ReadCountryStateCodeFile(self, dataFile): # initialize variable used to store CSV file's data countryCodeData = [] - # open the country and stae code data file + # open the country and state code data file # and read its lines into a list for future searching - with open(dataFile, 'r', errors='replace') as csvfile: + with open(dataFile, 'r', errors='replace') as csvfile: csvdata = csv.reader(csvfile, delimiter=',', quotechar='"') for line in csvdata: countryCodeData.append(line) @@ -114,26 +609,26 @@ def ReadCountryStateCodeFile(self, dataFile): # append the long country and state names when found in the country state code data for l in self.masterLocationList: - # append two place holders in the masterLocationList for long country and state names - l.append("") - l.append("") - +# # append two place holders in the masterLocationList for long country and state names +# l.append("") +# l.append("") +# for c in countryCodeData: # find the long country name by looking for a perfect match of "cc-" for the state code # this match is actually for the country because states have characters # after the - character - if l[0] + "-" == c[1]: + if l["countryCode"] + "-" == c[1]: # when found, save the long country name to the masterLocationList - l[4] = c[2] + l["countryName"] = c[2] self.countryList.append(c[2]) # look for a perfect match for the state code - if l[1] == c[1]: + if l["stateCode"] == c[1]: # when found, save the long state name to the masterLocationList - l[5] = c[2] + l["stateName"] = c[2] self.stateList.append(c[2]) # no need to keep searching. We've found our long names. @@ -147,11 +642,14 @@ def ReadCountryStateCodeFile(self, dataFile): self.countryList.sort() self.stateList.sort() + self.countryCodeData = countryCodeData self.countryStateCodeFileFound = True def ReadDataFile(self, DataFile): + self.ClearDatabase() + # extract data from zip file if user has chosen a zip file if os.path.splitext(DataFile[0])[1] == ".zip": @@ -174,82 +672,375 @@ def ReadDataFile(self, DataFile): if os.path.splitext(DataFile[0])[1] == ".csv": csvfile = open(DataFile[0], 'r') - # try to process CSV values from csvfile. Go to except if we have problems -# try: - csvdata = csv.reader(csvfile, delimiter=',', quotechar='"') - + # process CSV values from csvfile. + csvdata = csv.DictReader(csvfile) + # initialize temporary list variable to hold location data thisMasterLocationEntry = [] for line in csvdata: - + # convert date format from mm-dd-yyyy to yyyy-mm-dd for international standard and sorting ability - line[10] = line[10][6:]+ "-" + line[10][0:2] + "-" + line[10][3:5] + # THIS IS NO LONGER NECESSARY, AS IN MARCH 2019 EBIRD CHANGED THE DATA FORMAT TO THE ONE WE PREFER + # line[10] = line[10][6:] + "-" + line[10][0:2] + "-" + line[10][3:5] # append state name in parentheses to county name to differentiate between # counties that have the same name but are in different states - if line[6] != "": - line[6] = line[6] + " (" + line[5] + ")" - + if line["County"] != "": + line["County"] = line["County"] + " (" + line["State/Province"] + ")" + # add blank elements to list so we can later add family and order names if taxonomic file exists # also add blank line for subspecies name - while len(line) < 24: - line.append("") # store full name (maybe a subspecies) in sighting - subspeciesName = deepcopy(line[1]) - line[23]= subspeciesName + subspeciesName = deepcopy(line["Common Name"]) + line["Subspecies"] = subspeciesName # remove any subspecies data in parentheses in species name - if "(" in line[1]: - line[1] = line[1][:line[1].index("(")-1] + # but keep "(hybrid)" if it's in the name + if "(" in line["Common Name"]: + if "hybrid" not in line["Common Name"]: + line["Common Name"] = line["Common Name"][:line["Common Name"].index("(") - 1] # convert 12-hour time format to 24-hour format for easier sorting and display - time = line[11] + time = line["Time"] if "AM" in time: - time = line[11][0:5] + time = line["Time"][0:5] if "PM" in time: - time = line[11][0:5] - hour = int(line[11][0:2]) + time = line["Time"][0:5] + hour = int(line["Time"][0:2]) hour = str(hour + 12) if hour == "24": hour = "12" - time = hour + line[11][2:5] - line[11] = time + time = hour + line["Time"][2:5] + line["Time"] = time # add sighting to the main database for use by later searches etc. # this sightingList will be used in nearly every search performed by user - # convert line's CSV data into a dictionary for code legibility + # convert line's CSV data into our own dictionary to remove spaces, abbreviations, etc. thisSightingDict = defaultdict( - checklistID = line[0], - commonName = line[1], - scientificName = line[2], - subspeciesName= line[23], - taxonomicOrder = line[3], - count = line[4], - country = line[5][0:2], - state = line[5], - county = line[6], - location = line[7], - latitude = line[8], - longitude = line[9], - date = line[10], - time = line[11], - protocol = line[12], - duration = line[13], - allObsReported = line[14], - distance = line[15], - areaCovered = line[16], - observers = line[17], - breedingCode = line[18], - speciesComments = line[19], - checklistComments = line[20] + checklistID=line["Submission ID"], + commonName=line["Common Name"], + scientificName=line["Scientific Name"], + subspeciesName=line["Subspecies"], + taxonomicOrder=line["Taxonomic Order"], + count=line["Count"], + country=line["State/Province"][0:2], + state=line["State/Province"], + county=line["County"], + location=line["Location"], + latitude=line["Latitude"], + longitude=line["Longitude"], + date=line["Date"], + time=line["Time"], + protocol=line["Protocol"], + duration=line["Duration (Min)"], + allObsReported=line["All Obs Reported"], + distance=line["Distance Traveled (km)"], + areaCovered=line["Area Covered (ha)"], + observers=line["Number of Observers"], + breedingCode=line["Breeding Code"], + checklistComments=line["Checklist Comments"], + regionCodes=[] ) - self.sightingList.append(thisSightingDict) + if thisSightingDict["areaCovered"] is None: + thisSightingDict["areaCovered"] = "" + if thisSightingDict["distance"] is None: + thisSightingDict["distance"] = "" + if thisSightingDict["observers"] is None: + thisSightingDict["observers"] = "" + if thisSightingDict["breedingCode"] is None: + thisSightingDict["breedingCode"] = "" + if "Observation Details" in line.keys(): + thisSightingDict["speciesComments"]=line["Observation Details"] + if "Species Comments" in line.keys(): + thisSightingDict["speciesComments"]=line["Species Comments"] + if thisSightingDict["speciesComments"] is None: + thisSightingDict["speciesComments"] = "" + if thisSightingDict["checklistComments"] is None: + thisSightingDict["checklistComments"] = "" + thisSightingDict["family"] = "" + thisSightingDict["order"] = "" + + # If a US sighting, use countyCodeDict to assign the unique 5-digit FIPS code for the county + # we'll use this for choropleths + if thisSightingDict["country"] == "US" and thisSightingDict["state"] not in ["US-HI", "US-AK"]: + countyNameWithoutParentheses = thisSightingDict["county"].split(" (")[0] + if countyNameWithoutParentheses != "": + for c in self.countyCodeDict[countyNameWithoutParentheses]: + if c[1] == thisSightingDict["state"][3:5]: + thisSightingDict["countyCode"] = c[0] + + + # add abbreviations for the regions recognized by eBird + + # ---------------------------------------- + # add ABA if in US, Canada, or St. Pierre et Miquelon, but exclude Hawaii + if thisSightingDict["country"] in ["US", "CA", "PM"]: + if thisSightingDict["state"] != "US-HI": + thisSightingDict["regionCodes"].append("ABA") + + # ---------------------------------------- + # add AOU if if in US, Canada, St. Pierre et Miquelon, Mexico, Central America + if thisSightingDict["country"] in [ + "US", "CA", "PM", "MX", "GT", "SV", "HN", "CR", "PA", + "CU", "HT", "DO", "JM", "KY", "PR", "VG", "VI", "AI", + "MF", "BL", "BQ", "KN", "AG", "MS", "GP", "DM", "BS", + "BB", "GD", "LC", "VC", "BM", "CP", "NI" + ]: + thisSightingDict["regionCodes"].append("AOU") + + if thisSightingDict["state"] in [ + "CO-SAP", "UM-67", "UM-71" + ]: + thisSightingDict["regionCodes"].append("AOU") + + # ---------------------------------------- + # add USL if if in Lower 48 US states listing region as designated by eBird + + if thisSightingDict["country"] == "US": + if thisSightingDict["state"] not in ["US-AK", "US-HI"]: + thisSightingDict["regionCodes"].append("USL") + + + #--------------------------------------- + # add AFR if if in Africa + if thisSightingDict["country"] in [ + "DZ", "AO", "SH", "BJ", "BW", "BF", "BI", "CM", "CV", "CF", "TD", + "KM", "CG", "CD", "DJ", "GQ", "ER", "SZ", "ET", "GA", "GM", + "GH", "GN", "GW", "CI", "KE", "LS", "LR", "LY", "MG", "MW", "ML", + "MR", "MU", "YT", "MA", "MZ", "NA", "NE", "NG", "ST", "RE", "RW", + "ST", "SN", "SC", "SL", "SO", "ZA", "SS", "SH", "SD", "SZ", "TZ", + "TG", "TN", "UG", "CD", "ZM", "TZ", "ZW" + ]: + thisSightingDict["regionCodes"].append("AFR") + + if thisSightingDict["state"] == "ES-CN": + thisSightingDict["regionCodes"].append("AFR") + + if thisSightingDict["country"] == "EG": + if thisSightingDict["state"] not in ["EG-PTS", "EG-JS", "EG-SIN"]: + thisSightingDict["regionCodes"].append("AFR") + + # ---------------------------------------- + # add ASI if if in Asia + if thisSightingDict["country"] in [ + "AF", "AM", "AZ", "BH", "BD", "BT", "BN", "KH", "CN", "CX", "CC", + "IO", "GE", "HK", "IN", "IR", "IQ", "IL", "JP", "JO", + "KW", "KG", "LA", "LB", "MO", "MY", "MV", "MN", "MM", "NP", "KP", + "OM", "PK", "PS", "PH", "QA", "SA", "SG", "KR", "LK", "SY", "TW", + "TJ", "TH", "TM", "AE", "UZ", "VN", "YE", "SD", "SZ", "TZ", + "TG", "TN", "UG", "CD", "ZM", "TZ", "ZW" + ]: + thisSightingDict["regionCodes"].append("ASI") + + if thisSightingDict["country"] == "TR": + if thisSightingDict["state"] not in ["TR-22", "TR-39", "TR-59"]: + thisSightingDict["regionCodes"].append("ASI") + + if thisSightingDict["country"] == "KZ": + if thisSightingDict["state"] not in ["KZ-ATY", "KZ-ZAP"]: + thisSightingDict["regionCodes"].append("ASI") - #add sighting to checklistDict, even if it's a sp or / species + if thisSightingDict["country"] == "ID": + if thisSightingDict["state"] != "ID-IJ": + thisSightingDict["regionCodes"].append("ASI") + + if thisSightingDict["country"] == "EG": + if thisSightingDict["state"] in ["EG-PTS", "EG-JS", "EG-SIN"]: + thisSightingDict["regionCodes"].append("ASI") + + if thisSightingDict["country"] == "RU": + if thisSightingDict["state"] in [ + "RU-YAN", "RU-KHM", "RU-TYU", "RU-OMS", "RU-TOM", "RU-NVS", + "RU-ALT", "RU-KEM", "RU-AL", "RU-KK", "RU-KYA", "RU-TY", + "RU-IRK", "RU-SA", "RU-BU", "RU-ZAB", "RU-AMU", "RU-KHA", + "RU-YEV", "RU-PRI", "RU-MAG", "RU-CHU", "RU-KAM", "RU-SAK" + ]: + thisSightingDict["regionCodes"].append("ASI") + + # ---------------------------------------- + # add ATL if if in Atlantic listing region + + # Note that pelagic sightings more than 200 miles from a state + # will not be counted here because I don't know how to calculate + # if a log/lat sighting is 200 miles from a state + + if thisSightingDict["country"] in [ + "BM", "FK", "SH" + ]: + thisSightingDict["regionCodes"].append("ATL") + + + # ---------------------------------------- + # add AUE if if in Australasia (eBird) listing region + + if thisSightingDict["country"] in [ + "AU", "AC", "NF", "NZ", "SB", "VU", "NC", "PG", + ]: + thisSightingDict["regionCodes"].append("AUE") + + if thisSightingDict["state"] in [ + "ID-IJ", "ID-MA" + ]: + thisSightingDict["regionCodes"].append("AUE") + + + # ---------------------------------------- + # add AUA if if in Australasia (ABA) listing region + + if thisSightingDict["country"] in [ + "AU", "ID" + ]: + thisSightingDict["regionCodes"].append("AUA") + + + # ---------------------------------------- + # add AUS if if in Australia listing region as designated by eBird + + if thisSightingDict["country"] in [ + "AU", "HM", "CX", "CC", "NF", "AC" + ]: + thisSightingDict["regionCodes"].append("AUS") + + + # ---------------------------------------- + # add EUR if if in Europe listing region as designated by eBird + + if thisSightingDict["country"] in [ + "AL", "AD", "AT", "BY", "BE", "BA", "BG", "HR", "CY", "CZ", "DK", + "EE", "FO", "FI", "FR", "DE", "GI", "GR", "HU", "IS", "IE", "IM", + "IT", "RS", "LV", "LI", "LT", "LU", "MK", "MT", "MD", "MC", "ME", + "NL", "NO", "PL", "PT", "RO", "RU", "SM", "RS", "SK", "SI", + "SE", "CH", "UA", "GB", "VA", "JE" + ]: + thisSightingDict["regionCodes"].append("EUR") + + # include Spain, but not Canaries + if thisSightingDict["country"] == "ES": + if thisSightingDict["state"] != "ES-CN": + thisSightingDict["regionCodes"].append("EUR") + + # include Russia, but exclude Asian regions of Russia + if thisSightingDict["country"] == "RU": + if thisSightingDict["state"] not in [ + "RU-YAN", "RU-KHM", "RU-TYU", "RU-OMS", "RU-TOM", "RU-NVS", + "RU-ALT", "RU-KEM", "RU-AL", "RU-KK", "RU-KYA", "RU-TY", + "RU-IRK", "RU-SA", "RU-BU", "RU-ZAB", "RU-AMU", "RU-KHA", + "RU-YEV", "RU-PRI", "RU-MAG", "RU-CHU", "RU-KAM", "RU-SAK" + ]: + thisSightingDict["regionCodes"].append("EUR") + + # include European areas of Turkey + if thisSightingDict["state"] in ["TR-22", "TR-39", "TR-59"]: + thisSightingDict["regionCodes"].append("EUR") + + # include European areas of Kazakhstan + if thisSightingDict["state"] in ["KZ-ATY", "KZ-ZAP"]: + thisSightingDict["regionCodes"].append("EUR") + + + # ---------------------------------------- + # add NAM if if in North America listing region as designated by eBird + + if thisSightingDict["country"] in [ + "CA", "PM", "MX", "GT", "SV", "HN", "CR", "PA", + "CU", "HT", "DO", "JM", "KY", "PR", "VG", "VI", "AI", + "MF", "BL", "BQ", "KN", "AG", "MS", "GP", "DM", "BS", + "BB", "GD", "LC", "VC", "BM", "CP", "GL", "NI" + ]: + thisSightingDict["regionCodes"].append("NAM") + + if thisSightingDict["state"] in [ + "CO-SAP" + ]: + thisSightingDict["regionCodes"].append("NAM") + + if thisSightingDict["country"] == "US": + if thisSightingDict["state"] != "US-HI": + thisSightingDict["regionCodes"].append("NAM") + + + # ---------------------------------------- + # add SAM if if in South America listing region as designated by eBird + + if thisSightingDict["country"] in [ + "AR", "BO", "BR", "CL", "CO", "FK", "GF", + "GY", "GY", "PY", "PE", "SR", "UY", "VE", "TT", "CW", "AW" + ]: + thisSightingDict["regionCodes"].append("SAM") + + if thisSightingDict["state"] in [ + "BQ-BO" + ]: + thisSightingDict["regionCodes"].append("SAM") + + if thisSightingDict["country"] == "EC" and thisSightingDict["state"] != "EC-W": + thisSightingDict["regionCodes"].append("SAM") + + + # ---------------------------------------- + # add WIN if if in West Indies listing region as designated by eBird + + if thisSightingDict["country"] in [ + "AI", "AG", "AW", "BS", "BB", "VG", "KY", "VI" + "CU", "DM", "DO", "GD", "GP", "HT", "JM", "MQ", + "MS", "AN", "PR", "KN", "LC", "VC", "TT", "TC" + ]: + thisSightingDict["regionCodes"].append("WIN") + + if thisSightingDict["state"] == "CO-SAP": + thisSightingDict["regionCodes"].append("WIN") + + + # ---------------------------------------- + # add CAM if if in Central America listing region as designated by eBird + + if thisSightingDict["country"] in [ + "BZ", "CR", "SV", "GT", "HN", "NI", "PA" + ]: + thisSightingDict["regionCodes"].append("CAM") + + + # ---------------------------------------- + # add SPO if if in South Polar listing region as designated by eBird + + if thisSightingDict["country"] in [ + "AQ", "BV", "GS", "HM" + ]: + thisSightingDict["regionCodes"].append("SPO") + + + # ---------------------------------------- + # add WH if if in Western Hemisphere listing region as designated by eBird + + if "NAM" in thisSightingDict["regionCodes"]: + thisSightingDict["regionCodes"].append("WHE") + + if "SAM" in thisSightingDict["regionCodes"]: + thisSightingDict["regionCodes"].append("WHE") + + if thisSightingDict["country"] in [ + "CP", "BM", "FK" + ]: + thisSightingDict["regionCodes"].append("WHE") + + if thisSightingDict["state"] in [ + "US-HI", "UM-67", "UM-71", "EC-W" + ]: + thisSightingDict["regionCodes"].append("WHE") + + + # ------------------------------------------------------ + # add EH if in Eastern Hemisphere (not Western, and not South Polar) + + if "WHE" not in thisSightingDict["regionCodes"] and "SPO" not in thisSightingDict["regionCodes"]: + thisSightingDict["regionCodes"].append("EHE") + + + # add sighting to checklistDict, even if it's a sp or / species # use checklistID as the key checklistID = thisSightingDict["checklistID"] if checklistID not in self.checklistDict.keys(): @@ -257,9 +1048,12 @@ def ReadDataFile(self, DataFile): else: self.checklistDict[checklistID].append(thisSightingDict) - #add sighting to other dicts only if it's a full species, not a / or sp. + # add sighting to other dicts only if it's a full species, not a / or sp. commonName = thisSightingDict["commonName"] if ("/" not in commonName) and ("sp." not in commonName): + + self.allSpeciesList.append(commonName) + # use species common name as key if commonName not in self.speciesDict.keys(): self.speciesDict[commonName] = [thisSightingDict] @@ -267,7 +1061,7 @@ def ReadDataFile(self, DataFile): self.speciesDict[commonName].append(thisSightingDict) # also add subspecies as key to speciesDict - # to faciliate lookup + # to facilitate lookup subspeciesName = thisSightingDict["subspeciesName"] if subspeciesName not in self.speciesDict.keys(): self.speciesDict[subspeciesName] = [thisSightingDict] @@ -297,22 +1091,31 @@ def ReadDataFile(self, DataFile): self.dateDict[date] = [thisSightingDict] else: self.dateDict[date].append(thisSightingDict) - + + # add sighting to regionDict + # use 3-character code as key + regionCodes = thisSightingDict["regionCodes"] + for r in regionCodes: + if r not in self.regionDict.keys(): + self.regionDict[r] = [thisSightingDict] + else: + self.regionDict[r].append(thisSightingDict) + # add sighting to countryDict # use 2-character code as key - country = thisSightingDict["country"] - if country not in self.countryDict.keys(): - self.countryDict[country] = [thisSightingDict] + countryCode = thisSightingDict["country"] + if countryCode not in self.countryDict.keys(): + self.countryDict[countryCode] = [thisSightingDict] else: - self.countryDict[country].append(thisSightingDict) + self.countryDict[countryCode].append(thisSightingDict) # add sighting to stateDict # use cc-ss code as the key - state = thisSightingDict["state"] - if state not in self.stateDict.keys(): - self.stateDict[state] = [thisSightingDict] + stateCode = thisSightingDict["state"] + if stateCode not in self.stateDict.keys(): + self.stateDict[stateCode] = [thisSightingDict] else: - self.stateDict[state].append(thisSightingDict) + self.stateDict[stateCode].append(thisSightingDict) # add sighting to countyDict # use county name as key @@ -333,58 +1136,44 @@ def ReadDataFile(self, DataFile): self.locationDict[location].append(thisSightingDict) # get just the location data from this particular sighting - thisMasterLocationEntry = [country, state, county, location] + # thisMasterLocationEntry = [country, state, county, location] + thisMasterLocationEntry = defaultdict() + thisMasterLocationEntry["regionCodes"] = regionCodes + thisMasterLocationEntry["countryCode"] = countryCode + thisMasterLocationEntry["stateCode"] = stateCode + thisMasterLocationEntry["county"] = county + thisMasterLocationEntry["location"] = location + # if this location isn't already in the list, add it # we use this list later for populating the filter list of countries, states, counties, locations if thisMasterLocationEntry not in self.masterLocationList: self.masterLocationList.append(thisMasterLocationEntry) - self.allSpeciesList.append(commonName) - self.countryList.append(country) - self.stateList.append(state) + for r in regionCodes: + self.regionList.append(self.GetRegionName(r)) + self.countryList.append(countryCode) + self.stateList.append(stateCode) if county != "": self.countyList.append(county) self.locationList.append(location) - -# except (IndexError, RuntimeError, TypeError, NameError, KeyError): -# self.allSpeciesList = [] -# self.currentSpeciesList = [] -# self.masterLocationList = [] -# self.countryList = [] -# self.stateList = [] -# self.countyList = [] -# self.locationList = [] -# self.sightingList = [] -# self.eBirdFileOpenFlag = False -# msg = QMessageBox() -# msg.setIcon(QMessageBox.Warning) -# msg.setText("The file failed to load.\n\nPlease check that it is a valid eBird data file.\n") -# msg.setWindowTitle("File failed to load") -# msg.setStandardButtons(QMessageBox.Ok) -# msg.exec_() -# csvfile.close() -# return + + self.sightingList.append(thisSightingDict) csvfile.close() - # remove csv header row from list - self.allSpeciesList.pop(0) - self.locationList.pop(0) - self.sightingList.pop(0) - self.countryList.pop(0) - self.stateList.pop(0) - # use set function to remove duplicates and then return to a list self.allSpeciesList = list(set(self.allSpeciesList)) + self.regionList = list(set(self.regionList)) self.countryList = list(set(self.countryList)) self.stateList = list(set(self.stateList)) self.locationList = list(set(self.locationList)) # sort 'em + self.regionList.sort() self.locationList.sort() self.countryList.sort() self.stateList.sort() - self.masterLocationList.sort() + # self.masterLocationList.sort() # remove parenthetical state names from counties, unless needed to # differentiate counties with same name in different states @@ -397,13 +1186,13 @@ def ReadDataFile(self, DataFile): if countyNamesWithoutParens.count((cdk.split(" (")[0])) == 1: for s in self.countyDict[cdk]: s["county"] = cdk.split(" (")[0] - countyKeyChanges.append([cdk, cdk.split(" (")[0]]) + countyKeyChanges.append([cdk, cdk.split(" (")[0]]) for ckc in countyKeyChanges: self.countyDict[str(ckc[1])] = self.countyDict.pop(str(ckc[0])) for ml in self.masterLocationList: - if ml[2] == str(ckc[0]): - ml[2] = str(ckc[1]) + if ml["county"] == str(ckc[0]): + ml["county"] = str(ckc[1]) self.countyList = list(self.countyDict.keys()) self.countyList.sort() @@ -417,33 +1206,32 @@ def ReadTaxonomyDataFile(self, taxonomyDataFile): # initialize variable to hold the csv data from the taxonomy file taxonomyData = [] # open the CSV taxonomy file, using "replace" for any problematic characters - with open(taxonomyDataFile, "r", errors='replace') as csvfile: + with open(taxonomyDataFile, "r", errors='replace') as csvfile: csvdata = csv.reader(csvfile, delimiter=',', quotechar='"') # store the csv data in a list for easier searching later on for row in csvdata: taxonomyData.append(row) - # initialize variables that will save the family and order names for faster lookup + # initialize thisSciName variable thisSciName = "" - thisOrder = "" - thisFamily = "" - + # loop through sighting list to get each species, compare it, and add order and family for s in self.sightingList: # if this species already has a order/family found, no need to search database again. - # But if species does not have order/family found, we need to get search the database + # But if species does not have order/family found, we need to search the database if thisSciName != s["scientificName"]: - + for line in taxonomyData: # if species matches, save the order and family names for the next time we find the species # species will be found in the sighting file in taxonomic order, so each species will be chunked together - if s["scientificName"] == line[4]: # sci names match - + if s["scientificName"] == line[4]: # sci names match + thisSciName = line[4] - thisOrder= line[5] - thisFamily = line[6] + thisOrder = line[5] + thisFamily = line[6] + thisQuickEntryCode = line[2] if thisFamily not in self.familyList: self.familyList.append(thisFamily) @@ -454,28 +1242,41 @@ def ReadTaxonomyDataFile(self, taxonomyDataFile): if [thisFamily, thisOrder] not in self.masterFamilyOrderList: self.masterFamilyOrderList.append([thisFamily, thisOrder]) - #add species to orderSpeciesDict: + # add species to orderSpeciesDict: if thisOrder not in self.orderSpeciesDict.keys(): self.orderSpeciesDict[thisOrder] = [s["commonName"]] else: self.orderSpeciesDict[thisOrder].append(s["commonName"]) - #add species to familySpeciesDict: + # add species to familySpeciesDict: if thisFamily not in self.familySpeciesDict.keys(): self.familySpeciesDict[thisFamily] = [s["commonName"]] else: self.familySpeciesDict[thisFamily].append(s["commonName"]) - + + # already found info, no need to continue for this species break - - # append the order and family names to the species in the sighting list. + + # append the order and family names to the sighting s["order"] = thisOrder - s["family"] = thisFamily + s["family"] = thisFamily + s["quickEntryCode"] = thisQuickEntryCode csvfile.close() - def GetFamilies(self, filter, filteredSightingList = []): + def ReadBBLCodeFile(self, bblFile): + + # initialize variable to hold the csv data from the BBL Code file + # open the CSV taxonomy file, using "replace" for any problematic characters + with open(bblFile, "r", errors='replace') as csvfile: + csvdata = csv.reader(csvfile, delimiter=',', quotechar='"') + # store the BBL code in a dictionary, using the sci name as the key + for row in csvdata: + self.bblCodeDict[str(row[2]).strip()] = row[0].strip() + + + def GetFamilies(self, filter, filteredSightingList=[]): familiesList = [] # set filteredSightingList to master list if no filteredSightingList specified @@ -485,71 +1286,140 @@ def GetFamilies(self, filter, filteredSightingList = []): # for each sighting, test date if necessary. Append new dates to return list. # don't consider spuh or slash species for s in filteredSightingList: - if "sp." not in s["commonName"] and "/" not in s["commonName"]: - if self.TestSighting(s, filter) is True: + if "sp." not in s["commonName"] and "/" not in s["commonName"] and " x " not in s["commonName"]: + if self.TestSighting(s, filter) is True: if s["family"] not in familiesList: familiesList.append(s["family"]) return(familiesList) - - def GetSightings(self, filter): + def GetSightings(self, filter): returnList = [] - filteredSightingList = self.GetMinimalFilteredSightingsList(filter) + filteredSightingList = self.GetMinimalFilteredSightingsList(filter) for s in filteredSightingList: # this is not a single checklist, so remove spuh and slash sightings if filter.getChecklistID() == "": commonName = s["commonName"] if "/" not in commonName and "sp." not in commonName: - if self.TestSighting(s, filter) is True: + if self.TestSighting(s, filter) is True: returnList.append(s) else: # this is a single checklist, so allow spuh and slash sightings - if self.TestSighting(s, filter) is True: + if self.TestSighting(s, filter) is True: returnList.append(s) return(returnList) - def GetSpecies(self, filter, filteredSightingList = []): + def GetSpeciesWithPhotos(self, filter): + returnSet = set() + + filteredSightingList = self.GetMinimalFilteredSightingsList(filter) + + for s in filteredSightingList: + commonName = s["commonName"] + if "/" not in commonName and "sp." not in commonName: + if self.TestSighting(s, filter) is True: + if "photos" in s.keys(): + returnSet.add(s["commonName"]) + + return(returnSet) + + + def GetSpeciesWithoutPhotos(self, filter): + + unphotographedSpeciesSet = set() + + photographedSpeciesSet = self.GetSpeciesWithPhotos(filter) + + for species in self.allSpeciesList: + if species not in photographedSpeciesSet: + unphotographedSpeciesSet.add(species) + + return(unphotographedSpeciesSet) + + + def GetSightingsWithPhotos(self, filter): + returnList = [] + photosFound = [] + + filteredSightingList = self.GetMinimalFilteredSightingsList(filter) + + for s in filteredSightingList: + commonName = s["commonName"] + if "/" not in commonName and "sp." not in commonName: + if self.TestSighting(s, filter) is True: + if "photos" in s.keys(): + if s["photos"][0]["fileName"] not in photosFound: + photosFound.append(s["photos"][0]["fileName"]) + returnList.append(s) + + returnList = sorted(returnList, key=lambda x: (float(x["taxonomicOrder"]), x["date"], x["time"])) + + return(returnList) + + + def GetPhotos(self, filter): + returnList = [] + + filteredSightingList = self.GetMinimalFilteredSightingsList(filter) + + for s in filteredSightingList: + if "photos" in s.keys(): + for p in s["photos"]: + if p["fileName"] not in returnList: + returnList.append(p["fileName"]) + + returnList.sort() + + return(returnList) + + + def GetSpecies(self, filter, filteredSightingList=[]): speciesList = [] # set filteredSightingList to master list if no filteredSightingList specified if filteredSightingList == []: filteredSightingList = self.GetMinimalFilteredSightingsList(filter) - + # for each sighting, test filter. Append filtered species to return list. for s in filteredSightingList: - if self.TestSighting(s, filter) is True: + if self.TestSighting(s, filter) is True: if s["commonName"] not in speciesList: speciesList.append(s["commonName"]) return(speciesList) - def GetMinimalFilteredSightingsList(self, filter): + def GetMinimalFilteredSightingsList(self, filter): returnList = [] speciesName = filter.getSpeciesName() speciesList = filter.getSpeciesList() startDate = filter.getStartDate() - endDate= filter.getEndDate() + endDate = filter.getEndDate() locationType = filter.getLocationType() locationName = filter.getLocationName() checklistID = filter.getChecklistID() + # if we're dealing with a sp. or slash species, + # we need to return the whole database since those sightings + # aren't in dictionaries + if "sp." in speciesName or "/" in speciesName: + return(self.sightingList) + # use narrowest subset possible, according to filter if checklistID != "": returnList = self.checklistDict[checklistID] - elif speciesName != "": + elif speciesName != "" and speciesName in self.speciesDict.keys(): returnList = self.speciesDict[speciesName] elif speciesList != []: for sp in speciesList: for s in self.speciesDict[sp]: returnList. append(s) - elif startDate !="" and startDate == endDate: + elif startDate != "" and startDate == endDate: if startDate in self.dateDict.keys(): returnList = self.dateDict[startDate] else: @@ -560,15 +1430,14 @@ def GetMinimalFilteredSightingsList(self, filter): returnList = self.stateDict[locationName] elif locationType == "County": returnList = self.countyDict[locationName] - elif locationType == "Location": + elif locationType == "Location" and locationName in self.locationDict.keys(): returnList = self.locationDict[locationName] else: returnList = self.sightingList return(returnList) - - def GetSpeciesWithData(self, filter, filteredSightingList = [], includeSpecies = "Species"): + def GetSpeciesWithData(self, filter, filteredSightingList=[], includeSpecies="Species"): filteredDictWithDates = {} checklistIDs = {} returnList = [] @@ -577,19 +1446,19 @@ def GetSpeciesWithData(self, filter, filteredSightingList = [], includeSpecies # if no filteredSightingList is specified, create one. # use narrowest subset possible, according to filter if filteredSightingList == []: + filteredSightingList = self.GetMinimalFilteredSightingsList(filter) # loop through sightingList and check each sighting for the filtered list criteria for sighting in filteredSightingList: - if self.TestSighting(sighting, filter) is True: + if self.TestSighting(sighting, filter) is True: # store the sighting date so we can get first and last later # include the taxonomy entry so we can sort the list by taxonomy later # include the main species name so we can store it in SpeciesList hidden data # include the checklist number so we can count checklists for each species - thisDateTaxSpecies = [sighting["date"], sighting["taxonomicOrder"], sighting["commonName"], sighting["checklistID"]] - + thisDateTaxSpecies = [sighting["date"], sighting["taxonomicOrder"], sighting["commonName"], sighting["checklistID"], sighting["count"]] # decide whether we're returning only species or also subspecies if includeSpecies == "Species": @@ -626,28 +1495,36 @@ def GetSpeciesWithData(self, filter, filteredSightingList = [], includeSpecies tempSpeciesList.sort() thisCommonName = s thisFirstDate = tempSpeciesList[0][0] - thisLastDate = tempSpeciesList[len(tempSpeciesList)-1][0] + thisLastDate = tempSpeciesList[len(tempSpeciesList) - 1][0] thisTaxNumber = float(tempSpeciesList[0][1]) thisTopLevelSpeciesName = tempSpeciesList[0][2] thisChecklistCount = len(checklistIDs[s]) percentageOfChecklists = round(100 * thisChecklistCount / allChecklistCount, 2) + # count up the number of individuals seen + count = 0 + for sighting in tempSpeciesList: + if sighting[4] == "X" or count == "X": + count = "X" + else: + count = count + int(sighting[4]) + returnList.append([ - thisCommonName, - thisFirstDate, - thisLastDate, - thisTaxNumber, - thisTopLevelSpeciesName, - thisChecklistCount, - percentageOfChecklists + thisCommonName, + thisFirstDate, + thisLastDate, + thisTaxNumber, + thisTopLevelSpeciesName, + thisChecklistCount, + percentageOfChecklists, + count ]) - - returnList = sorted(returnList, key=lambda x: (x[3])) + + returnList = sorted(returnList, key=lambda x: (x[3])) return(returnList) - - def GetUniqueSpeciesForLocation(self, filter, location, speciesList, filteredSightingList = [],): + def GetUniqueSpeciesForLocation(self, filter, location, speciesList, filteredSightingList=[],): uniqueSpeciesList = [] # set filteredSightingList to master list if no filteredSightingList specified @@ -658,38 +1535,307 @@ def GetUniqueSpeciesForLocation(self, filter, location, speciesList, filtere for species in speciesList: isSeenNowhereElse = True for s in filteredSightingList: - if self.TestSighting(s, filter) is True: + if self.TestSighting(s, filter) is True: if s["commonName"] == species and s["location"] != location: isSeenNowhereElse = False break if isSeenNowhereElse == True: - if species not in uniqueSpeciesList: + if species not in uniqueSpeciesList: uniqueSpeciesList.append(species) return(uniqueSpeciesList) - def TestSighting(self, sighting, filter): - locationType = filter.getLocationType() # str choices are Country, County, State, Location, or "" - locationName = filter.getLocationName() # str name of region or location or "" - startDate = filter.getStartDate() # str format yyyy-mm-dd or "" - endDate = filter.getEndDate() # str format yyyy-mm-dd or "" - startSeasonalMonth = filter.getStartSeasonalMonth() # str format mm - startSeasonalDay = filter.getStartSeasonalDay() # str format dd - endSeasonalMonth = filter.getEndSeasonalMonth() # str format dd - endSeasonalDay = filter.getEndSeasonalDay() # str format dd - checklistID = filter.getChecklistID() # str checklistID - sightingDate = sighting["date"] # str format yyyy-mm-dd - speciesName = filter.getSpeciesName() # str species Name - speciesList = filter.getSpeciesList() # list of species names - order = filter.getOrder() # str order name - family = filter.getFamily() # str family name - time = filter.getTime() # str format HH:DD in 24-hour format + def TestIndividualPhoto(self, photoData, filter): + + filterCamera = filter.getCamera() + filterLens = filter.getLens() + filterStartShutterSpeed = filter.getStartShutterSpeed() + filterEndShutterSpeed = filter.getEndShutterSpeed() + filterStartAperture = filter.getStartAperture() + filterEndAperture = filter.getEndAperture() + filterStartFocalLength = filter.getStartFocalLength() + filterEndFocalLength = filter.getEndFocalLength() + filterStartIso = filter.getStartIso() + filterEndIso = filter.getEndIso() + filterStartRating = filter.getStartRating() + filterEndRating = filter.getEndRating() + + # check photo settings + # note that a sighting can have several attached photos + # we only reject a sighting here if all attached photos fail + if filterCamera != "": + if photoData["camera"] != filterCamera: + return(False) + + if filterLens != "": + if photoData["lens"] != filterLens: + return(False) + + if filterStartShutterSpeed != "" and filterEndShutterSpeed != "": + # strip away the 1/ characters at start of shutter speeds + filterStartShutterSpeed = int(filterStartShutterSpeed[2:]) + filterEndShutterSpeed = int(filterEndShutterSpeed[2:]) + shutterSpeed = photoData["shutterSpeed"] + if shutterSpeed != "": + shutterSpeed = int(shutterSpeed[2:]) + if not (shutterSpeed <= filterStartShutterSpeed and shutterSpeed >= filterEndShutterSpeed): + return(False) + else: + return(False) + + if filterStartShutterSpeed != "" and filterEndShutterSpeed == "": + # strip away the 1/ characters at start of shutter speeds + filterStartShutterSpeed = int(filterStartShutterSpeed[2:]) + shutterSpeed = photoData["shutterSpeed"] + if shutterSpeed != "": + shutterSpeed = int(shutterSpeed[2:]) + if shutterSpeed > filterStartShutterSpeed: + return(False) + else: + return(False) + + if filterStartShutterSpeed == "" and filterEndShutterSpeed != "": + # strip away the 1/ characters at start of shutter speeds + filterEndShutterSpeed = int(filterEndShutterSpeed[2:]) + shutterSpeed = photoData["shutterSpeed"] + if shutterSpeed != "": + shutterSpeed = int(shutterSpeed[2:]) + if shutterSpeed < filterEndShutterSpeed: + return(False) + else: + return(False) + + if filterStartAperture != "" and filterEndAperture != "": + filterStartAperture = float(filterStartAperture) + filterEndAperture = float(filterEndAperture) + aperture = photoData["aperture"] + if aperture != "": + aperture = float(aperture) + if aperture < filterStartAperture or aperture > filterEndAperture: + return(False) + else: + return(False) + + if filterStartAperture != "" and filterEndAperture == "": + filterStartAperture = float(filterStartAperture) + aperture = photoData["aperture"] + if aperture != "": + aperture = float(aperture) + if aperture < filterStartAperture: + return(False) + else: + return(False) + + if filterStartAperture == "" and filterEndAperture != "": + filterEndAperture = float(filterEndAperture) + aperture = photoData["aperture"] + if aperture != "": + aperture = float(aperture) + if aperture > filterEndAperture: + return(False) + else: + return(False) + + if filterStartIso != "" and filterEndIso != "": + filterStartIso = int(filterStartIso) + filterEndIso = int(filterEndIso) + iso = photoData["iso"] + if iso != "": + iso = int(iso) + if iso < filterStartIso or iso > filterEndIso: + return(False) + else: + return(False) + + if filterStartIso != "" and filterEndIso == "": + filterStartIso = int(filterStartIso) + iso = photoData["iso"] + if iso != "": + iso = int(iso) + if iso < filterStartIso: + return(False) + else: + return(False) + + if filterStartIso == "" and filterEndIso != "": + filterEndIso = int(filterEndIso) + iso = photoData["iso"] + if iso != "": + iso = int(iso) + if iso > filterEndIso: + return(False) + else: + return(False) + + if filterStartFocalLength != "" and filterEndFocalLength != "": + filterStartFocalLength = filterStartFocalLength.split(" mm")[0] + filterStartFocalLength = int(filterStartFocalLength) + filterEndFocalLength = filterEndFocalLength.split(" mm")[0] + filterEndFocalLength = int(filterEndFocalLength) + focalLength = photoData["focalLength"] + if focalLength != "": + focalLength = focalLength.split(" mm")[0] + focalLength = int(focalLength) + if focalLength < filterStartFocalLength or focalLength > filterEndFocalLength: + return(False) + else: + return(False) + + if filterStartFocalLength != "" and filterEndFocalLength == "": + filterStartFocalLength = filterStartFocalLength.split(" mm")[0] + filterStartFocalLength = int(filterStartFocalLength) + focalLength = photoData["focalLength"] + if focalLength != "": + focalLength = focalLength.split(" mm")[0] + focalLength = int(focalLength) + if focalLength < filterStartFocalLength: + return(False) + else: + return(False) + + if filterStartFocalLength == "" and filterEndFocalLength != "": + filterEndFocalLength = filterEndFocalLength.split(" mm")[0] + filterEndFocalLength = int(filterEndFocalLength) + focalLength = photoData["focalLength"] + if focalLength != "": + focalLength = focalLength.split(" mm")[0] + focalLength = int(focalLength) + if focalLength > filterEndFocalLength: + return(False) + else: + return(False) + + if filterStartRating != "" and filterEndRating != "": + filterStartRating = int(filterStartRating) + filterEndRating = int(filterEndRating) + rating = photoData["rating"] + if rating != "" and rating is not None: + rating = int(rating) + if rating < filterStartRating or rating > filterEndRating: + return(False) + else: + return(False) + + if filterStartRating != "" and filterEndRating == "": + filterStartRating = int(filterStartRating) + rating = photoData["rating"] + if rating != "": + rating = int(rating) + if rating < filterStartRating: + return(False) + else: + return(False) + + if filterStartRating == "" and filterEndRating != "": + filterEndRating = int(filterEndRating) + rating = photoData["rating"] + if rating != "": + rating = int(rating) + if rating > filterEndRating: + return(False) + else: + return(False) + + # if we've arrived here, the photo passes the filter. + return(True) + + + def TestSighting(self, sighting, filter): + + locationType = filter.getLocationType() # str choices are Region, Country, County, State, Location, or "" + locationName = filter.getLocationName() # str name of region or location or "" + startDate = filter.getStartDate() # str format yyyy-mm-dd or "" + endDate = filter.getEndDate() # str format yyyy-mm-dd or "" + startSeasonalMonth = filter.getStartSeasonalMonth() # str format mm + startSeasonalDay = filter.getStartSeasonalDay() # str format dd + endSeasonalMonth = filter.getEndSeasonalMonth() # str format dd + endSeasonalDay = filter.getEndSeasonalDay() # str format dd + checklistID = filter.getChecklistID() # str checklistID + sightingDate = sighting["date"] # str format yyyy-mm-dd + speciesName = filter.getSpeciesName() # str species Name + speciesList = filter.getSpeciesList() # list of species names + scientificName = filter.getScientificName() #str scientific name + order = filter.getOrder() # str order name + family = filter.getFamily() # str family name + time = filter.getTime() # str format HH:DD in 24-hour format + commonNameSearch = filter.getCommonNameSearch() + + sightingHasPhoto = filter.getSightingHasPhoto() + speciesHasPhoto = filter.getSpeciesHasPhoto() + validPhotoSpecies = filter.getValidPhotoSpecies() + camera = filter.getCamera() + lens = filter.getLens() + startShutterSpeed = filter.getStartShutterSpeed() + endShutterSpeed = filter.getEndShutterSpeed() + startAperture = filter.getStartAperture() + endAperture = filter.getEndAperture() + startFocalLength = filter.getStartFocalLength() + endFocalLength = filter.getEndFocalLength() + startIso = filter.getStartIso() + endIso = filter.getEndIso() + startRating = filter.getStartRating() + endRating = filter.getEndRating() + # Check every filter setting. Return False immediately if sighting fails. # If sighting survives the filter, return True + # if any photo settings have been specified, check whether sighting has photos + # reject sighting if it doesn't have a photo + if ( + sightingHasPhoto == "Has photo" or + camera != "" or + lens != "" or + startShutterSpeed != "" or + endShutterSpeed != "" or + startAperture != "" or + endAperture != "" or + startFocalLength != "" or + endFocalLength != "" or + startIso != "" or + endIso != "" or + speciesHasPhoto == "Photographed" or + startRating != "" or + endRating != "" + ): + if "photos" not in sighting.keys(): + return(False) + + # reject if "no photo" was specified but sighting has photo + if sightingHasPhoto == "No photo": + if "photos" in sighting.keys(): + return(False) + + # if user selected a value for whether a species has a photo, + # check if commonName is in the supplied set. + # if we're checking for "Photographed," the supplied set contains photographed species + # if we're checking for "Not pPhotographed," the supplied set contains UNphotographed species + if validPhotoSpecies != []: + if sighting["commonName"] not in validPhotoSpecies: + return(False) + + # if a commonNameSearch string has been specified, check if sighting matches + if commonNameSearch != "": + # check to see if s: prepends the search, in which case we need to search sci name + if "s:" in commonNameSearch: + commonNameSearch = commonNameSearch.strip() + if len(commonNameSearch) > 2: + if commonNameSearch[0:2].lower() == "s:": + sciNameSearch = commonNameSearch[2:] + if sciNameSearch.lower() not in sighting["scientificName"].lower(): + return(False) + else: + return(False) + else: + return(False) + + else: + if commonNameSearch.lower() not in sighting["commonName"].lower(): + if commonNameSearch.lower() not in sighting["subspeciesName"].lower(): + return(False) + # if a checklistID has been specified, check if sighting matches if checklistID != "": if checklistID != sighting["checklistID"]: @@ -700,6 +1846,11 @@ def TestSighting(self, sighting, filter): if speciesName != sighting["commonName"] and speciesName != sighting["subspeciesName"]: return(False) + # if scientificName has been specified, check it + if scientificName != "": + if scientificName != sighting["scientificName"]: + return(False) + # if order has been specified, check it if order != "": if order != sighting["order"]: @@ -716,13 +1867,37 @@ def TestSighting(self, sighting, filter): return(False) # if time has been specified, check it - if time != "": + # don't try checking if sighting is an historical sighting without a time + if time != "" and sighting["time"] != "": + # if time exactly matches sighting start time, it passes the filter test + # if time doesn't match exactly, check whether time is between start time and end time if time != sighting["time"]: - return(False) + + # now use DateTime functions so we can add duration minutes to find end time efficiently + # adding duration minutes could take us to a new hour, day, month, or year + durationMinutes = sighting["duration"] + if durationMinutes is None or durationMinutes == "": + durationMinutes = 0 + else: + durationMinutes = int(durationMinutes) + + filterDateTime = datetime.datetime(int(startDate[0:4]), int(startDate[5:7]), int(startDate[8:10]), int(time[0:2]), int(time[3:5])) + + sightingStartDateTime = datetime.datetime(int(sightingDate[0:4]), int(sightingDate[5:7]), int(sightingDate[8:10]), int(sighting["time"][0:2]), int(sighting["time"][3:5])) + + sightingTimeDelta = datetime.timedelta(0, 0, 0, 0, durationMinutes) + sightingEndDateTime = sightingStartDateTime + sightingTimeDelta + + if not ((sightingStartDateTime <= filterDateTime) and (filterDateTime <= sightingEndDateTime)): + + return(False) # check if location matches for sighting; flag species that fit the location # no need to check if locationType is "" if not locationType == "": + if locationType == "Region": + if not locationName in sighting["regionCodes"]: + return(False) if locationType == "Country": if not locationName == sighting["country"]: return(False) @@ -746,7 +1921,7 @@ def TestSighting(self, sighting, filter): return(False) # check for seasonal range using month and date parts; disqualify sighting if date doesn't fit in seasonal range - if not ((startSeasonalMonth == "") or (endSeasonalMonth== "")): + if not ((startSeasonalMonth == "") or (endSeasonalMonth == "")): sightingMonth = sightingDate[5:7] sightingDay = sightingDate[8:] # if startSeasonalMonth is earlier than endSeasonalMonth (e.g., July to October) @@ -795,11 +1970,256 @@ def TestSighting(self, sighting, filter): if not sightingMonth == startSeasonalMonth: return(False) + # check photo settings + # note that a sighting can have several attached photos + # we only reject a sighting here if all attached photos fail + # For each sighting, test each photo. If any photo passes filter, the sighting passes filter + + if camera != "": + cameraOK = False + for p in sighting["photos"]: + if camera == p["camera"]: + cameraOK = True + if cameraOK is False: + return(False) + + if lens != "": + lensOK = False + for p in sighting["photos"]: + if lens == p["lens"]: + lensOK= True + if lensOK is False: + return(False) + + if startShutterSpeed != "" and endShutterSpeed == "": + shutterSpeedOK = False + filterStartShutterSpeed = startShutterSpeed[2:] + filterStartShutterSpeed = float(filterStartShutterSpeed) + # check each photo and set flag to true if at least one photo passes test + # reject if all photos fail this test + for p in sighting["photos"]: + shutterSpeed = p["shutterSpeed"] + if shutterSpeed != "": + shutterSpeed = shutterSpeed[2:] + shutterSpeed = float(shutterSpeed) + if shutterSpeed <= filterStartShutterSpeed: + shutterSpeedOK= True + if shutterSpeedOK is False: + return(False) + + if startShutterSpeed == "" and endShutterSpeed != "": + shutterSpeedOK = False + filterEndShutterSpeed = endShutterSpeed[2:] + filterEndShutterSpeed = float(filterEndShutterSpeed) + # check each photo and set flag to true if at least one photo passes test + # reject if all photos fail this test + for p in sighting["photos"]: + shutterSpeed = p["shutterSpeed"] + if shutterSpeed != "": + shutterSpeed = shutterSpeed[2:] + shutterSpeed = float(shutterSpeed) + if shutterSpeed >= filterEndShutterSpeed: + shutterSpeedOK= True + if shutterSpeedOK is False: + return(False) + + if endShutterSpeed != "" and startShutterSpeed != "": + shutterSpeedOK= False + filterStartShutterSpeed = startShutterSpeed[2:] + filterStartShutterSpeed = float(filterStartShutterSpeed) + filterEndShutterSpeed = endShutterSpeed[2:] + filterEndShutterSpeed = float(filterEndShutterSpeed) + for p in sighting["photos"]: + # convert the string to an integer + shutterSpeed = p["shutterSpeed"] + if shutterSpeed != "": + shutterSpeed = shutterSpeed[2:] + shutterSpeed = float(shutterSpeed) + if shutterSpeed <= filterStartShutterSpeed and shutterSpeed >= filterEndShutterSpeed: + shutterSpeedOK = True + if shutterSpeedOK is False: + return(False) + + if startAperture != "" and endAperture == "": + apertureOK = False + filterStartAperture = float(startAperture) + # check each photo and set flag to true if at least one photo passes test + # reject if all photos fail this test + for p in sighting["photos"]: + aperture = p["aperture"] + if aperture != "": + aperture = float(aperture) + if aperture >= filterStartAperture: + apertureOK= True + if apertureOK is False: + return(False) + + if startAperture == "" and endAperture != "": + apertureOK = False + filterEndAperture = float(endAperture) + # check each photo and set flag to true if at least one photo passes test + # reject if all photos fail this test + for p in sighting["photos"]: + aperture = p["aperture"] + if aperture != "": + aperture = float(aperture) + if aperture >= filterEndAperture: + apertureOK= True + if apertureOK is False: + return(False) + + if endAperture != "" and startAperture != "": + apertureOK= False + filterStartAperture = float(startAperture) + filterEndAperture = float(endAperture) + for p in sighting["photos"]: + # convert the string to an integer + aperture = p["aperture"] + if aperture != "": + aperture = float(aperture) + if aperture >= filterStartAperture and aperture <= filterEndAperture: + apertureOK = True + if apertureOK is False: + return(False) + + if startIso != "" and endIso == "": + isoOK = False + filterStartIso = int(startIso) + # check each photo and set flag to true if at least one photo passes test + # reject if all photos fail this test + for p in sighting["photos"]: + iso = p["iso"] + if iso != "": + iso = int(iso) + if iso >= filterStartIso: + isoOK= True + if isoOK is False: + return(False) + + if startIso == "" and endIso != "": + isoOK = False + filterEndIso = int(endIso) + # check each photo and set flag to true if at least one photo passes test + # reject if all photos fail this test + for p in sighting["photos"]: + iso = p["iso"] + if iso != "": + iso = int(iso) + if iso <= filterEndIso: + isoOK= True + if isoOK is False: + return(False) + + if endIso != "" and startIso != "": + isoOK= False + filterStartIso = int(startIso) + filterEndIso = int(endIso) + for p in sighting["photos"]: + # convert the string to an integer + iso = p["iso"] + if iso != "": + iso = int(iso) + if iso >= filterStartIso and iso <= filterEndIso: + isoOK = True + if isoOK is False: + return(False) + + if startFocalLength != "" and endFocalLength == "": + focalLengthOK = False + filterStartFocalLength = startFocalLength.split(" mm")[0] + filterStartFocalLength = int(filterStartFocalLength) + # check each photo and set flag to true if at least one photo passes test + # reject if all photos fail this test + for p in sighting["photos"]: + focalLength = p["focalLength"] + if focalLength != "": + focalLength = focalLength.split(" mm")[0] + focalLength = int(focalLength) + if focalLength >= filterStartFocalLength: + focalLengthOK= True + if focalLengthOK is False: + return(False) + + if startFocalLength == "" and endFocalLength != "": + focalLengthOK = False + filterEndFocalLength = endFocalLength.split(" mm")[0] + filterEndFocalLength = int(filterEndFocalLength) + # check each photo and set flag to true if at least one photo passes test + # reject if all photos fail this test + for p in sighting["photos"]: + focalLength = p["focalLength"] + if focalLength != "": + focalLength = focalLength.split(" mm")[0] + focalLength = int(focalLength) + if focalLength <= filterEndFocalLength: + focalLengthOK= True + if focalLengthOK is False: + return(False) + + if endFocalLength != "" and startFocalLength != "": + focalLengthOK= False + filterStartFocalLength = startFocalLength.split(" mm")[0] + filterStartFocalLength = int(filterStartFocalLength) + filterEndFocalLength = endFocalLength.split(" mm")[0] + filterEndFocalLength = int(filterEndFocalLength) + for p in sighting["photos"]: + # convert the string to an integer + focalLength = p["focalLength"] + if focalLength != "": + focalLength = focalLength.split(" mm")[0] + focalLength = int(focalLength) + if focalLength >= filterStartFocalLength and focalLength <= filterEndFocalLength: + focalLengthOK = True + if focalLengthOK is False: + return(False) + + if startRating != "" and endRating == "": + ratingOK = False + filterStartRating = int(startRating) + # check each photo and set flag to true if at least one photo passes test + # reject if all photos fail this test + for p in sighting["photos"]: + rating = p["rating"] + if rating != "": + rating = int(rating) + if rating >= filterStartRating: + ratingOK= True + if ratingOK is False: + return(False) + + if startRating == "" and endRating != "": + ratingOK = False + filterEndRating = int(endRating) + # check each photo and set flag to true if at least one photo passes test + # reject if all photos fail this test + for p in sighting["photos"]: + rating = p["rating"] + if rating != "": + rating = int(rating) + if rating >= filterEndRating: + ratingOK= True + if ratingOK is False: + return(False) + + if endRating != "" and startRating != "": + ratingOK= False + filterStartRating = int(startRating) + filterEndRating = int(endRating) + for p in sighting["photos"]: + # convert the string to an integer + rating = p["rating"] + if rating != "": + rating = int(rating) + if rating >= filterStartRating and rating <= filterEndRating: + ratingOK = True + if ratingOK is False: + return(False) + # if we've arrived here, the sighting passes the filter. return(True) - def GetDates(self, filter, filteredSightingList=[]): + def GetDates(self, filter, filteredSightingList=[]): dateList = set() needToCheckFilter = False @@ -811,7 +2231,7 @@ def GetDates(self, filter, filteredSightingList=[]): # for each sighting, test date if necessary. Append new dates to return list. for s in filteredSightingList: if needToCheckFilter is True: - if self.TestSighting(s, filter) is True: + if self.TestSighting(s, filter) is True: dateList.add(s["date"]) else: dateList.add(s["date"]) @@ -823,6 +2243,29 @@ def GetDates(self, filter, filteredSightingList=[]): return(dateList) + def GetStartTimes(self, filter, filteredSightingList=[]): + timeList = set() + needToCheckFilter = False + + # set filteredSightingList to master list if no filteredSightingList specified + if filteredSightingList == []: + filteredSightingList = self.GetMinimalFilteredSightingsList(filter) + needToCheckFilter = True + + # for each sighting, test time if necessary. Append new times to return list. + for s in filteredSightingList: + if needToCheckFilter is True: + if self.TestSighting(s, filter) is True: + timeList.add(s["time"]) + else: + timeList.add(s["time"]) + + # convert the set to a list and sort it. + timeList = list(timeList) + timeList.sort() + + return(timeList) + def ClearDatabase(self): self.eBirdFileOpenFlag = False @@ -839,23 +2282,40 @@ def ClearDatabase(self): self.yearDict = defaultdict() self.monthDict = defaultdict() self.dateDict = defaultdict() + self.regionDict = defaultdict() self.countryDict = defaultdict() self.stateDict = defaultdict() self.countyDict = defaultdict() self.locationDict = defaultdict() self.checklistDict = defaultdict() + + def ClearPhotoSettings(self): + + self.cameraList = [] + self.lensList = [] + self.shutterSpeedList = [] + self.apertureList = [] + self.focalLengthList=[] + self.isoList = [] + + # remove photo data from sightings + for s in self.sightingList: + if "photos" in s.keys(): + del s["photos"] + + self.photoDataFileOpenFlag = False - def GetChecklists(self, filter): + def GetChecklists(self, filter): returnList = [] checklistIDs = set() # speed retreaval by choosing minimal set of sightings to search minimalSightingList = self.GetMinimalFilteredSightingsList(filter) - + # gather the IDs of checklists that match the filter for s in minimalSightingList: - if self.TestSighting(s, filter) is True: + if self.TestSighting(s, filter) is True: checklistIDs .add(s["checklistID"]) # get all the sightings that match these checklistIDs @@ -870,29 +2330,29 @@ def GetChecklists(self, filter): # append species common name to list, so we can count the species checklistSpecies.append(sighting["commonName"]) - # count the species, discarding superflous subspecies, spuhs and slashes when necessary + # count the species, discarding superfluous subspecies, spuhs and slashes when necessary speciesCount = self.CountSpecies(checklistSpecies) - # compile data for checklist (id, state (which incudes country prefix), county, location, date, time, speciesCount) - checklistData= [ - sighting["checklistID"], + # compile data for checklist (id, state (which includes country prefix), county, location, date, time, speciesCount) + checklistData = [ + sighting["checklistID"], sighting["state"], - sighting["county"], - sighting["location"], - sighting["date"], - sighting["time"], - speciesCount + sighting["county"], + sighting["location"], + sighting["date"], + sighting["time"], + speciesCount, + sighting["duration"] ] returnList.append(checklistData) # sort by country, county, location, date, time - returnList= sorted(returnList, key=lambda x: (x[1], x[2], x[3], x[4], x[5])) + returnList = sorted(returnList, key=lambda x: (x[1], x[2], x[3], x[4], x[5])) return(returnList) - - def GetFindResults(self, searchString, checkedBoxes): + def GetFindResults(self, searchString, checkedBoxes): foundSet = set() @@ -900,47 +2360,46 @@ def GetFindResults(self, searchString, checkedBoxes): for c in checkedBoxes: if c == "chkCommonName": if searchString.lower() in s["commonName"].lower(): - foundSet.add(("Common Name", s["checklistID"], s["location"], s["date"], s["commonName"])) + foundSet.add(("Common Name", s["checklistID"], s["location"], s["date"], s["commonName"])) if c == "chkScientificName": if searchString.lower() in s["scientificName"].lower(): - foundSet.add(("Scientific Name", s["checklistID"], s["location"], s["date"], s["scientificName"])) + foundSet.add(("Scientific Name", s["checklistID"], s["location"], s["date"], s["scientificName"])) if c == "chkCountryName": if searchString.lower() in self.GetCountryName(s["country"]).lower(): - foundSet.add(("Country", s["checklistID"], s["location"], s["date"], self.GetCountryName(s[5][0:2]))) + foundSet.add(("Country", s["checklistID"], s["location"], s["date"], self.GetCountryName(s[5][0:2]))) if c == "chkStateName": if searchString.lower() in self.GetStateName(s["state"]).lower(): - foundSet.add(("State", s["checklistID"], s["location"], s["date"], self.GetStateName(s["state"]))) + foundSet.add(("State", s["checklistID"], s["location"], s["date"], self.GetStateName(s["state"]))) if c == "chkCountyName": if searchString.lower() in s["county"].lower(): - foundSet.add(("County", s["checklistID"], s["location"], s["date"], s["county"])) + foundSet.add(("County", s["checklistID"], s["location"], s["date"], s["county"])) if c == "chkLocationName": if searchString.lower() in s["location"].lower(): - foundSet.add(("Location", s["checklistID"], s["location"], s["date"], s["location"])) + foundSet.add(("Location", s["checklistID"], s["location"], s["date"], s["location"])) if c == "chkSpeciesComments": if searchString.lower() in s["speciesComments"].lower(): - foundSet.add(("Species Comments", s["checklistID"], s["location"], s["date"], s["speciesComments"])) + foundSet.add(("Species Comments", s["checklistID"], s["location"], s["date"], s["speciesComments"])) if c == "chkChecklistComments": if searchString.lower() in s["checklistComments"].lower(): - foundSet.add(("Checklist Comments", s["checklistID"], s["location"], s["date"], s["checklistComments"])) + foundSet.add(("Checklist Comments", s["checklistID"], s["location"], s["date"], s["checklistComments"])) foundList = list(foundSet) foundList.sort() return(foundList) - def GetLastDayOfMonth(self, month): + def GetLastDayOfMonth(self, month): # find last day of the specified month - if month in ["01", "1", "03", "3", "05", "5", "07", "7", "08", "8", "10", "12"]: + if month in ["01", "1", "03", "3", "05", "5", "07", "7", "08", "8", "10", "12"]: lastDayOfThisMonth = "31" - if month in ["04", "4", "06", "6", "09", "9", "11"]: + if month in ["04", "4", "06", "6", "09", "9", "11"]: lastDayOfThisMonth = "30" - if month in ["02", "2"]: + if month in ["02", "2"]: lastDayOfThisMonth = "29" return(lastDayOfThisMonth) - - def GetLocationCoordinates(self, location): + def GetLocationCoordinates(self, location): coordinates = [] s = self.locationDict[location][0] coordinates.append(s["latitude"]) @@ -948,8 +2407,7 @@ def GetLocationCoordinates(self, location): return(coordinates) - - def GetLocations(self, filter, queryType="OnlyLocations", filteredSightingList=[]): + def GetLocations(self, filter, queryType="OnlyLocations", filteredSightingList=[]): # queryType specifies which data fields to return in the returnList # "OnlyLocations" returns just the location names # "Checklist"returns locationName, count, checklistID, and time @@ -980,7 +2438,7 @@ def GetLocations(self, filter, queryType="OnlyLocations", filteredSightingList= if s["commonName"] != speciesName and speciesName != s["subspeciesName"]: break - if self.TestSighting(s, filter) is True: + if self.TestSighting(s, filter) is True: sightingFound = True @@ -988,10 +2446,10 @@ def GetLocations(self, filter, queryType="OnlyLocations", filteredSightingList= thisLocationList = s["location"] if queryType == "Checklist": - thisLocationList = [s["location"], s["count"], s["checklistID"], s["latitude"]] + thisLocationList = [s["location"], s["count"], s["checklistID"], s["latitude"]] if queryType == "LocationHierarchy": - thisLocationList = [s["state"], s["county"], s["location"]] + thisLocationList = [s["state"], s["county"], s["location"]] # if we're getting first and last dates, too, we need to # store all dates in a dictionary keyed by location @@ -1018,16 +2476,15 @@ def GetLocations(self, filter, queryType="OnlyLocations", filteredSightingList= tempDateList = tempDateDict[k] tempDateList.sort() tempFirstDate = tempDateList[0] - tempLastDate = tempDateList[len(tempDateList)-1] - thisLocationList = [k, tempFirstDate, tempLastDate] + tempLastDate = tempDateList[len(tempDateList) - 1] + thisLocationList = [k, tempFirstDate, tempLastDate] returnList.append(thisLocationList) # sort the list and return it returnList.sort() return(returnList) - - def GetNewCountrySpecies(self, filter, filteredSightingList, sightingListForSpeciesSubset, speciesList): + def GetNewCountrySpecies(self, filter, filteredSightingList, sightingListForSpeciesSubset, speciesList): countries = set() countrySpecies = [] @@ -1035,7 +2492,7 @@ def GetNewCountrySpecies(self, filter, filteredSightingList, sightingListForS # loop through sightingListForSpeciesSubset to gather relevant country names for s in sightingListForSpeciesSubset: - if self.TestSighting(s, tempFilter) is True: + if self.TestSighting(s, tempFilter) is True: countries.add(s["country"]) countries = list(countries) countries.sort() @@ -1047,7 +2504,7 @@ def GetNewCountrySpecies(self, filter, filteredSightingList, sightingListForS tempFilter.setLocationType("Country") tempFilter.setLocationName(country) - speciesWithFirstLastDates = self.GetSpeciesWithData(tempFilter, filteredSightingList) + speciesWithFirstLastDates = self.GetSpeciesWithData(tempFilter, filteredSightingList) # use dictionary created when data file was first loaded to get all sightings from # the selected country @@ -1071,12 +2528,11 @@ def GetNewCountrySpecies(self, filter, filteredSightingList, sightingListForS tempSpeciesSightings = tempSpeciesDict[species[0]] tempSpeciesSightings = sorted(tempSpeciesSightings, key=lambda x: (x["date"])) if species[1] <= tempSpeciesSightings[0]["date"]: - countrySpecies.append([country, species[0]]) + countrySpecies.append([country, species[0]]) return(countrySpecies) - - def GetNewCountySpecies(self, filter, filteredSightingList, sightingListForSpeciesSubset, speciesList): + def GetNewCountySpecies(self, filter, filteredSightingList, sightingListForSpeciesSubset, speciesList): # find which years are in the filtered sightingsfor the species in question counties = set() @@ -1086,7 +2542,7 @@ def GetNewCountySpecies(self, filter, filteredSightingList, sightingListForSp # loop through speciesWithFirstLastDates to gather county names for sightings subset for s in sightingListForSpeciesSubset: county = s["county"] - if self.TestSighting(s, tempFilter) is True: + if self.TestSighting(s, tempFilter) is True: if county != "": counties.add(county) counties = list(counties) @@ -1098,7 +2554,7 @@ def GetNewCountySpecies(self, filter, filteredSightingList, sightingListForSp # get list of species with first/last dates for filter in selected county tempFilter.setLocationType("County") tempFilter.setLocationName(county) - speciesWithFirstLastDates = self.GetSpeciesWithData(tempFilter, filteredSightingList) + speciesWithFirstLastDates = self.GetSpeciesWithData(tempFilter, filteredSightingList) thisCountySightings = self.countyDict[county] @@ -1118,19 +2574,18 @@ def GetNewCountySpecies(self, filter, filteredSightingList, sightingListForSp tempSpeciesSightings = tempSpeciesDict[species[0]] tempSpeciesSightings = sorted(tempSpeciesSightings, key=lambda x: (x["date"])) if species[1] <= tempSpeciesSightings[0]["date"]: - countySpecies.append([county, species[0]]) + countySpecies.append([county, species[0]]) return(countySpecies) - - def GetNewLifeSpecies(self, filter, filteredSightingList, sightingListForSpeciesSubset): + def GetNewLifeSpecies(self, filter, filteredSightingList, sightingListForSpeciesSubset): # find which years are in the filtered sightingsfor the species in question # DON'T use a SET here, because we want to keep the species taxonomic order lifeSpecies = [] # get list of species with first/last dates for filter - speciesWithFirstLastDates = self.GetSpeciesWithData(filter, filteredSightingList) + speciesWithFirstLastDates = self.GetSpeciesWithData(filter, filteredSightingList) for species in speciesWithFirstLastDates: tempSpeciesSightings = self.speciesDict[species[0]] @@ -1140,8 +2595,7 @@ def GetNewLifeSpecies(self, filter, filteredSightingList, sightingListForSpec return(lifeSpecies) - - def GetNewLocationSpecies(self, filter, filteredSightingList, sightingListForSpeciesSubset, speciesList): + def GetNewLocationSpecies(self, filter, filteredSightingList, sightingListForSpeciesSubset, speciesList): # find which years are in the filtered sightingsfor the species in question locations = set() @@ -1149,7 +2603,7 @@ def GetNewLocationSpecies(self, filter, filteredSightingList, sightingListFor tempFilter = deepcopy(filter) for s in sightingListForSpeciesSubset: - if self.TestSighting(s, filter) is True: + if self.TestSighting(s, filter) is True: locations.add(s["location"]) locations = list(locations) locations.sort() @@ -1160,13 +2614,13 @@ def GetNewLocationSpecies(self, filter, filteredSightingList, sightingListFor tempFilter.setLocationType("Location") tempFilter.setLocationName(location) - speciesWithFirstLastDates = self.GetSpeciesWithData(tempFilter, filteredSightingList) + speciesWithFirstLastDates = self.GetSpeciesWithData(tempFilter, filteredSightingList) thisLocationSightings = self.locationDict[location] tempSpeciesDict = {} for tms in thisLocationSightings: - commonName= tms["commonName"] + commonName = tms["commonName"] if commonName not in tempSpeciesDict.keys(): tempSpeciesDict[commonName] = [tms] else: @@ -1176,19 +2630,18 @@ def GetNewLocationSpecies(self, filter, filteredSightingList, sightingListFor tempSpeciesSightings = tempSpeciesDict[species[0]] tempSpeciesSightings = sorted(tempSpeciesSightings, key=lambda x: (x["date"])) if species[1] <= tempSpeciesSightings[0]["date"]: - locationSpecies.append([location, species[0]]) + locationSpecies.append([location, species[0]]) return(locationSpecies) - - def GetNewMonthSpecies(self, filter, filteredSightingList, sightingListForSpeciesSubset): + def GetNewMonthSpecies(self, filter, filteredSightingList, sightingListForSpeciesSubset): # find which months are in the filtered sightingsfor the species in question months = set() monthSpecies = [] for s in sightingListForSpeciesSubset: - if self.TestSighting(s, filter) is True: + if self.TestSighting(s, filter) is True: months.add(s["date"][5:7]) months = list(months) months.sort() @@ -1205,7 +2658,7 @@ def GetNewMonthSpecies(self, filter, filteredSightingList, sightingListForSpe tempFilter.setEndSeasonalDay(lastDayOfThisMonth) - speciesWithFirstLastDates = self.GetSpeciesWithData(tempFilter, filteredSightingList) + speciesWithFirstLastDates = self.GetSpeciesWithData(tempFilter, filteredSightingList) thisMonthSightings = self.monthDict[month] @@ -1221,14 +2674,13 @@ def GetNewMonthSpecies(self, filter, filteredSightingList, sightingListForSpe tempSpeciesSightings = tempSpeciesDict[species[0]] tempSpeciesSightings = sorted(tempSpeciesSightings, key=lambda x: (x["date"])) if species[1] <= tempSpeciesSightings[0]["date"]: - monthRange = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] - monthName = monthRange[int(month)-1] - monthSpecies.append([monthName, species[0]]) + monthRange = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] + monthName = monthRange[int(month) - 1] + monthSpecies.append([monthName, species[0]]) return(monthSpecies) - - def GetNewStateSpecies(self, filter, filteredSightingList, sightingListForSpeciesSubset, speciesList): + def GetNewStateSpecies(self, filter, filteredSightingList, sightingListForSpeciesSubset, speciesList): # find which years are in the filtered sightingsfor the species in question states = set() @@ -1236,7 +2688,7 @@ def GetNewStateSpecies(self, filter, filteredSightingList, sightingListForSpe tempFilter = deepcopy(filter) for s in sightingListForSpeciesSubset: - if self.TestSighting(s, tempFilter) is True: + if self.TestSighting(s, tempFilter) is True: states.add(s["state"]) states = list(states) states.sort() @@ -1247,7 +2699,7 @@ def GetNewStateSpecies(self, filter, filteredSightingList, sightingListForSpe tempFilter.setLocationType("State") tempFilter.setLocationName(state) - speciesWithDates = self.GetSpeciesWithData(tempFilter, filteredSightingList) + speciesWithDates = self.GetSpeciesWithData(tempFilter, filteredSightingList) thisStateSightings = self.stateDict[state] tempSpeciesDict = {} @@ -1262,12 +2714,11 @@ def GetNewStateSpecies(self, filter, filteredSightingList, sightingListForSpe tempSpeciesSightings = tempSpeciesDict[species[0]] tempSpeciesSightings = sorted(tempSpeciesSightings, key=lambda x: (x["date"])) if species[1] <= tempSpeciesSightings[0]["date"]: - stateSpecies.append([state, species[0]]) + stateSpecies.append([state, species[0]]) return(stateSpecies) - - def GetNewYearSpecies(self, filter, filteredSightingList, sightingListForSpeciesSubset): + def GetNewYearSpecies(self, filter, filteredSightingList, sightingListForSpeciesSubset): # find which years are in the filtered sightingsfor the species in question years = set() @@ -1275,7 +2726,7 @@ def GetNewYearSpecies(self, filter, filteredSightingList, sightingListForSpec thisFilter = deepcopy(filter) for s in sightingListForSpeciesSubset: - if self.TestSighting(s, filter) is True: + if self.TestSighting(s, filter) is True: years.add(s["date"][0:4]) years = list(years) years.sort() @@ -1288,7 +2739,7 @@ def GetNewYearSpecies(self, filter, filteredSightingList, sightingListForSpec endDate = year + "-12-31" thisFilter.setStartDate(startDate) thisFilter.setEndDate(endDate) - speciesWithDates = self.GetSpeciesWithData(thisFilter, filteredSightingList) + speciesWithDates = self.GetSpeciesWithData(thisFilter, filteredSightingList) tempSpeciesDict = {} for tms in thisYearSightings: @@ -1302,91 +2753,244 @@ def GetNewYearSpecies(self, filter, filteredSightingList, sightingListForSpec tempSpeciesSightings = tempSpeciesDict[species[0]] tempSpeciesSightings = sorted(tempSpeciesSightings, key=lambda x: (x["date"])) if species[1] <= tempSpeciesSightings[0]["date"]: - yearSpecies.append([year, species[0]]) + yearSpecies.append([year, species[0]]) return(yearSpecies) - - def GetFamilyName(self, species): + + def GetQuickEntryCode(self, species): + + thisSpecies = deepcopy(species) + + if " (" in thisSpecies: + thisSpecies = thisSpecies.split(" (")[0] + + if " x " in thisSpecies: + return("Not applicable to hybrids") + + cleanedSpeciesName = thisSpecies.replace('-',' ') + wordList = cleanedSpeciesName.split(" ") - filteredSightingList = self.speciesDict[species] + quickEntryCode = "" - familyName = filteredSightingList[0]["family"] + if len(wordList) == 1: + quickEntryCode = wordList[0][0:4] - return(familyName) + if len(wordList) == 2: + quickEntryCode = wordList[0][0:2] + wordList[1][0:2] + + if len(wordList) == 3: + quickEntryCode = wordList[0][0:1] + wordList[1][0:1] + wordList[2][0:2] + if len(wordList) == 4: + quickEntryCode = wordList[0][0:1] + wordList[1][0:1] + wordList[2][0:1] + wordList[3][0:1] + + return(quickEntryCode) - def GetOrderName(self, species): - filteredSightingList = self.speciesDict[species] + def GetBBLCode(self, species): - orderName = filteredSightingList[0]["order"] + thisScientificName = self.GetScientificName(species) + + if thisScientificName in self.bblCodeDict.keys(): + bblCode = self.bblCodeDict[thisScientificName] + else: + bblCode = "" - return(orderName) + return(bblCode) + + def GetFamilyName(self, species): + + speciesDetails = self.speciesDict[species] + + familyName = speciesDetails[0]["family"] + + return(familyName) + + + def GetOrderName(self, species): + + speciesDetails = self.speciesDict[species] + + orderName = speciesDetails[0]["order"] + + return(orderName) + - def GetScientificName(self, species): + def GetScientificName(self, species): - filteredSightingList = self.speciesDict[species] + speciesDetails = self.speciesDict[species] - scientificName = filteredSightingList[0]["scientificName"] + scientificName = speciesDetails[0]["scientificName"] return(scientificName) - def GetCountryCode(self, longName): + def GetRegionCode(self, longName): + + if longName == "**All Regions**": + return("**All Regions**") + + else: + return(self.regionCodeDict[longName]) + + + def GetRegionName(self, code): + + return(self.regionNameDict[code]) + + + def GetCountryCode(self, longName): if longName == "**All Countries**": return("**All Countries**") if self.countryStateCodeFileFound is True: for l in self.masterLocationList: - if l[4] == longName: - return(l[0]) + if l["countryName"] == longName: + return(l["countryCode"]) else: - return(longName) - + return(longName) + - def GetStateCode(self, longName): + def GetStateCode(self, longName): if longName == "**All States**": return("**All States**") if self.countryStateCodeFileFound is True: for l in self.masterLocationList: - if l[5] == longName: - return(l[1]) + if l["stateName"] == longName: + return(l["stateCode"]) else: return(longName) + - - def GetCountryName(self, shortCode): + def GetCountryName(self, shortCode): if shortCode == "**All Countries**": return("**All Countries**") if self.countryStateCodeFileFound is True: for l in self.masterLocationList: - if l[0] == shortCode: - return(l[4]) + if l["countryCode"] == shortCode: + return(l["countryName"]) else: return(shortCode) - def GetMonthName(self, monthNumberString): + def GetMonthName(self, monthNumberString): monthName = self.monthNameDict[monthNumberString] return(monthName) - - - def GetStateName(self, shortCode): + + + def GetStateName(self, shortCode): if shortCode == "**All States**": return("**All States**") if self.countryStateCodeFileFound is True: for l in self.masterLocationList: - if l[1] == shortCode: - return(l[5]) + if l["stateCode"] == shortCode: + return(l["stateName"]) else: return(shortCode) + + + def dumpDatabaseToFile(self): + + # routine used only in debugging + f = open("yearbird_Db_Dump.txt", "w+") + for s in self.sightingList: + f.write(str(s)) + f.write("\n") + f.close() + + + def setStartupFolder(self, startupFolder): + self.startupFolder = startupFolder + + + def getStartupFolder(self): + return(self.startupFolder) + + + def setPhotoDataFile(self, photoDataFile): + self.photoDataFile = photoDataFile + + + def getPhotoDataFile(self): + return(self.photoDataFile) + + + def addPhotoDataToDb(self, photoData): + + if photoData["camera"] not in self.cameraList: + if photoData["camera"] != "": + self.cameraList.append(photoData["camera"]) + self.cameraList.sort() + + if photoData["lens"] not in self.lensList: + if photoData["lens"] != "": + self.lensList.append(photoData["lens"]) + self.lensList.sort() + + if photoData["shutterSpeed"] not in self.shutterSpeedList: + if photoData["shutterSpeed"] != "": + self.shutterSpeedList.append(photoData["shutterSpeed"]) + self.shutterSpeedList = natsorted(self.shutterSpeedList, key=lambda y: y.lower(), reverse=True) + + if photoData["aperture"] not in self.apertureList: + if photoData["aperture"] != "": + self.apertureList.append(photoData["aperture"]) + self.apertureList= natsorted(self.apertureList, key=lambda y: y.lower()) + + if photoData["focalLength"] not in self.focalLengthList: + if photoData["focalLength"] != "": + self.focalLengthList.append(photoData["focalLength"]) + self.focalLengthList = natsorted(self.focalLengthList, key=lambda y: y.lower()) + + if photoData["iso"] not in self.isoList: + if photoData["iso"] != "": + self.isoList.append(photoData["iso"]) + self.isoList = natsorted(self.isoList, key=lambda y: y.lower()) + + + def readPreferences(self): + + # try to open the preferences file + + if os.path.isfile("yearbirdPreferences.txt"): + + with open("yearbirdPreferences.txt", "r") as settingsFile: + + for line in settingsFile: + + if "startupFolder=" in line: + startupFolder = line[14:].rstrip(" \n\r") + startupFolder = startupFolder.lstrip(" ") + self.startupFolder= startupFolder + + if "photoDataFile=" in line: + photoDataFile = line[14:].rstrip(" \n\r") + photoDataFile = photoDataFile.lstrip(" ") + self.photoDataFile = photoDataFile + + settingsFile.close() + + + def writePreferences(self): + + f = open("yearbirdPreferences.txt", "w") + + f.write("startupFolder=" + self.startupFolder) + f.write("\n") + + f.write("photoDataFile=" + self.photoDataFile) + f.write("\n") + + f.close() + + diff --git a/code_DateTotals.py b/code_DateTotals.py index c44a37d..8bfe76a 100644 --- a/code_DateTotals.py +++ b/code_DateTotals.py @@ -41,6 +41,7 @@ class DateTotals(QMdiSubWindow, form_DateTotals.Ui_frmDateTotals): def __init__(self): super(self.__class__, self).__init__() self.setupUi(self) + self.setAttribute(Qt.WA_DeleteOnClose,True) self.mdiParent = "" self.tblYearTotals.itemDoubleClicked.connect(self.YearTableClicked) self.tblMonthTotals.itemDoubleClicked.connect(self.MonthTableClicked) @@ -342,9 +343,9 @@ def FillDateTotals(self, filter): for sighting in minimalSightingList: - # Consider only full species, not slash or spuh entries + # Consider only full species, not slash or spuh or hybrid entries commonName = sighting["commonName"] - if ("/" not in commonName) and ("sp." not in commonName): + if ("/" not in commonName) and ("sp." not in commonName) and " x " not in commonName: if self.mdiParent.db.TestSighting(sighting, filter) is True: dbYears.add(sighting["date"][0:4]) @@ -416,7 +417,7 @@ def FillDateTotals(self, filter): for s in monthDict[month]: monthSpecies.add(s["commonName"]) monthChecklists.add(s["checklistID"]) - monthArray.append([len(monthSpecies), month, len(monthChecklists)]) + monthArray.append([len(monthSpecies), month, len(monthChecklists) - 1 ]) monthArray.sort(reverse=True) R = 0 for month in monthArray: diff --git a/code_Families.py b/code_Families.py index 381382c..d8db420 100644 --- a/code_Families.py +++ b/code_Families.py @@ -8,13 +8,9 @@ # import basic Python libraries import random -from copy import ( - deepcopy -) +from copy import deepcopy -from math import ( - floor -) +from math import floor # import the Qt components we'll use # do this so later we won't have to clutter our code with references to parent Qt classes @@ -22,12 +18,14 @@ from PyQt5.QtGui import ( QCursor, QColor, - QFont + QFont, + QPen ) from PyQt5.QtCore import ( Qt, - pyqtSignal + pyqtSignal, + QTimer ) from PyQt5.QtWidgets import ( @@ -36,7 +34,8 @@ QHeaderView, QMdiSubWindow, QGraphicsScene, - QGraphicsEllipseItem + QGraphicsEllipseItem, + QMenu ) @@ -45,23 +44,93 @@ class Families(QMdiSubWindow, form_Families.Ui_frmFamilies): # create "resized" as a signal that the window can emit # we respond to this signal with the form's resizeMe method below resized = pyqtSignal() + + class MyEllipse(QGraphicsEllipseItem): + + family = "" + parent= "" + + def mouseDoubleClickEvent(self, event): + # Do your stuff here. + self.parent.CreateFamilyList(self.family) + + def contextMenuEvent(self, event): + + menu = QMenu() + menu.setStyleSheet("color:black; background-color: white;") + + actionSetFilterToFamily = menu.addAction("Set filter to " + self.family) + + action = menu.exec(event.screenPos()) + + if action == actionSetFilterToFamily: + self.parent.setFamilyFilter(self.family) + def __init__(self): super(self.__class__, self).__init__() self.setupUi(self) + self.setAttribute(Qt.WA_DeleteOnClose,True) self.mdiParent = "" + + self.resized.connect(self.resizeMe) + self.lstFamilies.currentRowChanged.connect(self.FillSpecies) self.lstFamilies.itemDoubleClicked.connect(self.ClickedLstFamilies) - self.lstSpecies.itemDoubleClicked.connect(self.ClickedLstSpecies) + self.lstSpecies.itemDoubleClicked.connect(self.ClickedLstSpecies) + self.tblPieChartLegend.doubleClicked.connect(self.clickedPieChartLegend) + + self.lstFamilies.addAction(self.actionSetFilterToFamily) + self.actionSetFilterToFamily.triggered.connect(self.lstFamiliesSetFilterToFamily) + + self.lstSpecies.addAction(self.actionSetFilterToSpecies) + self.actionSetFilterToSpecies.triggered.connect(self.lstSpeciesSetFilterToSpecies) + + self.tblPieChartLegend.addAction(self.actionSetFamilyFilterToPieChartLegendFamily) + self.actionSetFamilyFilterToPieChartLegendFamily.triggered.connect(self.tblPieChartLegendSetFilterToFamily) + self.lstFamilies.setSpacing(2) self.lstSpecies.setSpacing(2) - self.resized.connect(self.resizeMe) - self.tabFamilies.setCurrentIndex(0) + + self.tabFamilies.setCurrentIndex(1) + self.filter = code_Filter.Filter() self.filteredSpeciesList = [] self.filteredSpeciesListWithFamilies = [] self.familiesList = [] - + + + def lstFamiliesSetFilterToFamily(self): + + family = self.lstFamilies.currentItem().text() + self.setFamilyFilter(family) + + + def tblPieChartLegendSetFilterToFamily(self): + + currentRow = self.tblPieChartLegend.currentRow() + family = self.tblPieChartLegend.item(currentRow, 1).text() + self.setFamilyFilter(family) + + + def lstSpeciesSetFilterToSpecies(self): + + commonName = self.lstSpecies.currentItem().text() + self.mdiParent.setSpeciesFilter(commonName) + + + def setFamilyFilter(self, family): + + self.mdiParent.setFamilyFilter(family) + + + def clickedPieChartLegend(self): + + currentRow = self.tblPieChartLegend.currentRow() + + family = self.tblPieChartLegend.item(currentRow, 1).text() + self.CreateFamilyList(family) + def ClickedLstSpecies(self): species = self.lstSpecies.currentItem().text() @@ -115,7 +184,7 @@ def FillSpecies(self): self.lstSpecies.setSpacing(2) count = self.mdiParent.db.CountSpecies(familySpecies) - self.lblSpecies.setText("Species for selected family (" + str(count) + ")") + self.lblSpecies.setText("Species for selected family: " + str(count)) def html(self): @@ -194,22 +263,22 @@ def html(self): def FillFamilies(self, filter): + self.filter = deepcopy(filter) self.familiesList = self.mdiParent.db.GetFamilies(self.filter) self.filteredSpeciesList = self.mdiParent.db.GetSpecies(self.filter) cleanedFilteredSpeciesList = [] for s in self.filteredSpeciesList: - if ("sp." not in s) and ("/" not in s): + if "sp." not in s and "/" not in s and " x " not in s: cleanedFilteredSpeciesList.append(s) self.filteredSpeciesList = cleanedFilteredSpeciesList - self.lblFamilies.setText("Families (" + str(len(self.familiesList )) + "):") + self.lblFamilies.setText("Families: " + str(len(self.familiesList ))) self.lstFamilies.addItems(self.familiesList ) if len(self.familiesList ) > 0: self.lstFamilies.setCurrentRow(0) self.FillSpecies() - self.FillPieChart() self.lstFamilies.setSpacing(2) else: @@ -220,22 +289,27 @@ def FillFamilies(self, filter): self.setWindowTitle("Families: "+ self.lblLocation.text() + ": " + self.lblDateRange.text()) + QApplication.processEvents() + # report success back to MainWindow return(True) def FillPieChart(self): + self.tblPieChartLegend.clear() - scene = QGraphicsScene() + QApplication.processEvents() + self.tblPieChartLegend.setColumnCount(3) self.tblPieChartLegend.setRowCount(len(self.familiesList)) + self.tblPieChartLegend.horizontalHeader().setVisible(False) header = self.tblPieChartLegend.horizontalHeader() header.setSectionResizeMode(1, QHeaderView.Stretch) self.tblPieChartLegend.setShowGrid(False) - total = 0 - colours = [] + + colors = [] familiesCount = [] set_angle = 0 R = 0 @@ -249,48 +323,70 @@ def FillPieChart(self): # randomly create three color values (rgb) for rgb in range(3): number.append(random.randrange(0, 255)) - colours.append(QColor(number[0],number[1],number[2])) + colors.append(QColor(number[0],number[1],number[2])) - if self.gvPieChart.width() < self.gvPieChart.height(): - shorterSide = self.gvPieChart.width() - else: - shorterSide = self.gvPieChart.height() - - pieChartRadius = floor(.9 * shorterSide) + scene = QGraphicsScene() + for family in familiesCount: # create the angle of each wedge according to its perecent of 360 angle = round(family/total*16*360) # set size of circle and create wedge - ellipse = QGraphicsEllipseItem(0, 0, pieChartRadius, pieChartRadius) + ellipse = self.MyEllipse(0, 0, 100, 100) + # set the tooltip for the ellipse wedge + ellipse.setToolTip(self.familiesList[R] + ": " + str(family)) # set center of circle, like an axle ellipse.setPos(0,0) # rotate through the wedge ellipse.setStartAngle(set_angle) ellipse.setSpanAngle(angle) # assign color - ellipse.setBrush(colours[R]) + ellipse.setBrush(colors[R]) + # turn off pen so we won't have ellipse borders + + pen = QPen() + pen.setWidthF(.1) + ellipse.setPen(pen) + # create set_angle for next time around set_angle = angle + set_angle # add the actual wedge to the scene object scene.addItem(ellipse) + + # save meta data to ellipse for future use (e.g., clicks) + ellipse.family = self.familiesList[R] + ellipse.parent = self + # add entry to legend table and set proper color colorItem = QTableWidgetItem() - colorItem.setBackground(QColor(colours[R])) + colorItem.setBackground(QColor(colors[R])) + familyNameItem = QTableWidgetItem() familyNameItem.setData(Qt.DisplayRole, self.familiesList[R]) + familyCountItem = QTableWidgetItem() familyCountItem.setData(Qt.DisplayRole, family) + self.tblPieChartLegend.setItem(R, 0, colorItem) self.tblPieChartLegend.setItem(R, 1, familyNameItem) self.tblPieChartLegend.setItem(R, 2, familyCountItem) R = R + 1 - + self.gvPieChart.setScene(scene) + + self.fitPieChart() + + return(True) + + + def fitPieChart(self): + self.gvPieChart.setSceneRect(0, 0, 100, 100) + self.gvPieChart.fitInView(self.gvPieChart.sceneRect(), Qt.KeepAspectRatio) + def resizeEvent(self, event): - #routine to handle events on objects, like clicks, lost focus, gained forcus, etc. + #routine to handle events on objects, like clicks, lost focus, gained focus, etc. self.resized.emit() return super(self.__class__, self).resizeEvent(event) @@ -300,7 +396,7 @@ def resizeMe(self): windowWidth = self.frameGeometry().width() windowHeight = self.frameGeometry().height() self.scrollArea.setGeometry(5, 27, windowWidth -10 , windowHeight-35) - self.FillPieChart() + self.fitPieChart() def scaleMe(self): diff --git a/code_Filter.py b/code_Filter.py index 2d88943..78d1d05 100644 --- a/code_Filter.py +++ b/code_Filter.py @@ -1,18 +1,36 @@ class Filter(): - locationType = "" # str choices are Country, County, State, Location, or "" - locationName = "" # str name of region or location or "" - startDate = "" # str format yyyy-mm-dd or "" - endDate = "" # str format yyyy-mm-dd or "" - startSeasonalMonth = "" # str format mm - startSeasonalDay = "" # str format dd - endSeasonalMonth = "" # str format mm - endSeasonalDay = "" # str format dd - checklistID = "" # str number taken from main sightings CSV file - speciesName = "" # str species name - speciesList = [] # list of species names - family = "" #str family name - time = "" #str format HH:MM in 24-hour format - order = "" #str order name + locationType = "" # str choices are Region, Country, County, State, Location, or "" + locationName = "" # str name of region or location or "" + startDate = "" # str format yyyy-mm-dd or "" + endDate = "" # str format yyyy-mm-dd or "" + startSeasonalMonth = "" # str format mm + startSeasonalDay = "" # str format dd + endSeasonalMonth = "" # str format mm + endSeasonalDay = "" # str format dd + checklistID = "" # str number taken from main sightings CSV file + speciesName = "" # str species name + scientificName = "" + speciesList = [] # list of species names + family = "" #str family name + time = "" #str format HH:MM in 24-hour format + order = "" + commonNameSearch = "" #str commonNameSearch string + + sightingHasPhoto = "" + speciesHasPhoto = "" + validPhotoSpecies = [] # list of species names that are valid according to the photo species filter setting + camera = "" + lens = "" + startShutterSpeed = "" + endShutterSpeed = "" + startAperture = "" + endAperture = "" + startFocalLength = "" + endFocalLength = "" + startIso = "" + endIso = "" + startRating = "" + endRating = "" def setLocationType(self, locationType): @@ -95,6 +113,14 @@ def getSpeciesName(self): return(self.speciesName) + def setScientificName(self, scientficName): + self.scientificName = scientficName + + + def getScientificName(self): + return(self.scientificName) + + def setSpeciesList(self, speciesList): self.speciesList = speciesList @@ -107,6 +133,10 @@ def setFamily(self, family): self.family = family + def getFamily(self): + return(self.family) + + def getOrder(self): return(self.order) @@ -115,8 +145,12 @@ def setOrder(self, order): self.order = order - def getFamily(self): - return(self.family) + def getCommonNameSearch(self): + return(self.commonNameSearch) + + + def setCommonNameSearch(self, commonNameSearch): + self.commonNameSearch = commonNameSearch def setTime(self, time): @@ -125,14 +159,131 @@ def setTime(self, time): def getTime(self): return(self.time) + + + def setStartRating(self, startRating): + self.startRating = startRating + + + def getStartRating(self): + return(self.startRating) + + + def setEndRating(self, endRating): + self.endRating = endRating + + + def getEndRating(self): + return(self.endRating) + + + def getSightingHasPhoto(self): + return(self.sightingHasPhoto) + + + def setSpeciesHasPhoto(self, speciesHasPhoto): + self.speciesHasPhoto = speciesHasPhoto + + + def getSpeciesHasPhoto(self): + return(self.speciesHasPhoto) + + + def setValidPhotoSpecies(self, validPhotoSpecies): + self.validPhotoSpecies = validPhotoSpecies + + + def getValidPhotoSpecies(self): + return(self.validPhotoSpecies) + + + def setCamera(self, camera): + self.camera = camera + + + def getCamera(self): + return(self.camera) + + + def setLens(self, lens): + self.lens = lens + + + def getLens(self): + return(self.lens) + + + def setStartShutterSpeed(self, startShutterSpeed): + self.startShutterSpeed = startShutterSpeed + + + def getStartShutterSpeed(self): + return(self.startShutterSpeed) + + + def setEndShutterSpeed(self, endShutterSpeed): + self.endShutterSpeed = endShutterSpeed + def getEndShutterSpeed(self): + return(self.endShutterSpeed) + + + def setStartAperture(self, startAperture): + self.startAperture = startAperture + + + def getStartAperture(self): + return(self.startAperture) + + + def setEndAperture(self, endAperture): + self.endAperture = endAperture + + + def getEndAperture(self): + return(self.endAperture) + + + def setStartFocalLength(self, startFocalLength): + self.startFocalLength = startFocalLength + + + def getStartFocalLength(self): + return(self.startFocalLength) + + + def setEndFocalLength(self, endFocalLength): + self.endFocalLength = endFocalLength + + + def getEndFocalLength(self): + return(self.endFocalLength) + + + def setStartIso(self, startIso): + self.startIso = startIso + + + def getStartIso(self): + return(self.startIso) + + + def setEndIso(self, endIso): + self.endIso = endIso + + + def getEndIso(self): + return(self.endIso) + + def debugAll(self): returnString = ( "locationType: " + self.locationType + ' ' + "locationName: " + self.locationName + " " + "startDate: " + self.startDate + " " + "endDate: " + self.endDate + " " + + "time: " + self.time + " " + "startSeasonalMonth: " + self.startSeasonalMonth + " " + "startSeasonalDay: " + self.startSeasonalDay + " " + "endSeasonalMonth: " + self.endSeasonalMonth + " " + diff --git a/code_Find.py b/code_Find.py index 2227c72..acdff05 100644 --- a/code_Find.py +++ b/code_Find.py @@ -25,6 +25,7 @@ class Find(QMdiSubWindow, form_Find.Ui_frmFind): def __init__(self): super(self.__class__, self).__init__() self.setupUi(self) + self.setAttribute(Qt.WA_DeleteOnClose,True) self.mdiParent = "" self.resized.connect(self.resizeMe) self.btnFind.clicked.connect(self.CreateFindResults) diff --git a/code_Individual.py b/code_Individual.py index 8e7e4ee..2155ac0 100644 --- a/code_Individual.py +++ b/code_Individual.py @@ -4,6 +4,8 @@ import code_Lists import code_Location import code_Web +import code_Photos +import code_Stylesheet # import the Qt components we'll use # do this so later we won't have to clutter our code with references to parent Qt classes @@ -25,12 +27,11 @@ QTableWidgetItem, QHeaderView, QMdiSubWindow, - QTreeWidgetItem + QTreeWidgetItem, + QPushButton ) -from math import ( - floor -) +from math import floor class Individual(QMdiSubWindow, form_Individual.Ui_frmIndividual): @@ -43,6 +44,7 @@ class Individual(QMdiSubWindow, form_Individual.Ui_frmIndividual): def __init__(self): super(self.__class__, self).__init__() self.setupUi(self) + self.setAttribute(Qt.WA_DeleteOnClose,True) self.mdiParent = "" self.resized.connect(self.resizeMe) self.trLocations.currentItemChanged.connect(self.FillDates) @@ -52,7 +54,6 @@ def __init__(self): self.lstDates.itemDoubleClicked.connect(lambda: self.CreateSpeciesList(self.lstDates)) self.tblYearLocations.itemDoubleClicked.connect(lambda: self.CreateSpeciesList(self.tblYearLocations)) self.tblMonthLocations.itemDoubleClicked.connect(lambda: self.CreateSpeciesList(self.tblMonthLocations)) - self.buttonMacaulay.clicked.connect(self.CreateWebPageForPhotos) self.buttonWikipedia.clicked.connect(self.CreateWebPageForWikipedia) self.buttonAllAboutBirds.clicked.connect(self.CreateWebPageForAllAboutBirds) self.buttonAudubon.clicked.connect(self.CreateWebPageForAudubon) @@ -62,15 +63,30 @@ def __init__(self): def FillIndividual(self, Species): + self.setWindowTitle(Species) self.lblCommonName.setText(Species) - self.lblCommonName.setStyleSheet('QLabel {color: blue;}') + + red = code_Stylesheet.speciesColor.red() + green = code_Stylesheet.speciesColor.green() + blue = code_Stylesheet.speciesColor.blue() + self.lblCommonName.setStyleSheet('QLabel {font-weight: bold; color: rgb(' + str(red) + ',' + str(green) + ',' + str(blue) + ');}') self.lblScientificName.setText(self.mdiParent.db.GetScientificName(Species)) orderAndFamilyText = self.mdiParent.db.GetOrderName(Species) + # check if taxonomy data has been loaded. If so, add a semi-colon and the family name if orderAndFamilyText != "": orderAndFamilyText = orderAndFamilyText + "; " + self.mdiParent.db.GetFamilyName(Species) self.lblOrderName.setText(orderAndFamilyText) + + # if available, add BBL banding code + bblCode = self.mdiParent.db.GetBBLCode(Species) + if bblCode != "": + self.lblSpeciesCode.setText("IBP Banding Code: " + bblCode) + else: + # add species Quick Entry Code code used by eBird + self.lblSpeciesCode.setText("eBird Entry Code: " + self.mdiParent.db.GetQuickEntryCode(Species).upper()) + # find list of dates for species, to find oldest and newest filter = code_Filter.Filter() filter.setSpeciesName(Species) @@ -181,9 +197,7 @@ def FillIndividual(self, Species): locationCount = locationCount + len(theseLocations) - # Fill Year Tree widget - theseYears = [] - + # Fill Year Tree widget theseYears = set() for d in dateList: theseYears.add(d[0:4]) @@ -282,7 +296,19 @@ def FillIndividual(self, Species): if locationCount == 1: self.lblLocations.setText("Location (1)") else: - self.lblLocations.setText("Locations (" + str(locationCount) + ")") + self.lblLocations.setText("Locations (" + str(locationCount) + ")") + + # add a photo button if the db holds photos of this species + filter = code_Filter.Filter() + filter.setSpeciesName(Species) + + photoSightings = self.mdiParent.db.GetSightingsWithPhotos(filter) + + if len(photoSightings) > 0: + btnPhotos = QPushButton() + btnPhotos.setText("Photos") + btnPhotos.clicked.connect(self.createPhotos) + self.verticalLayout_10.addWidget(btnPhotos) self.scaleMe() self.resizeMe() @@ -567,30 +593,24 @@ def CreateWebPageForAudubon(self): sub.LoadWebPage(url) self.parent().parent().addSubWindow(sub) self.mdiParent.PositionChildWindow(sub, self) - sub.show() + sub.show() - def CreateWebPageForPhotos(self): + def createPhotos(self): - speciesScientificName = self.lblScientificName.text() speciesCommonName = self.lblCommonName.text() - - sub = code_Web.Web() + + sub = code_Photos.Photos() sub.mdiParent = self.mdiParent - sub.title = "Macaulay Library Photos: " + speciesCommonName - url = ("https://search.macaulaylibrary.org/catalog?searchField=species&q=" - + speciesScientificName.split(" ")[0] - +"+" - + speciesScientificName.split(" ")[1] - ) - - if speciesScientificName.count(" ") == 2: - url = url + "%20" + speciesScientificName.split(" ")[2] - - sub.LoadWebPage(url) + + filter = code_Filter.Filter() + filter.setSpeciesName(speciesCommonName) + sub.FillPhotos(filter) + self.parent().parent().addSubWindow(sub) self.mdiParent.PositionChildWindow(sub, self) - sub.show() + + sub.show() def CreateWebPageForWikipedia(self): @@ -796,12 +816,11 @@ def scaleMe(self): textHeight = metrics.boundingRect("2222-22-22").height() textWidth= metrics.boundingRect("2222-22-22").width() - self.buttonMacaulay.resize(self.buttonMacaulay.x(), textHeight) - self.buttonWikipedia.resize(self.buttonMacaulay.x(), textHeight) - self.buttonAudubon.resize(self.buttonMacaulay.x(), textHeight) - self.buttonAllAboutBirds.resize(self.buttonMacaulay.x(), textHeight) - self.buttonChecklists.resize(self.buttonMacaulay.x(), textHeight) - self.buttonMap.resize(self.buttonMacaulay.x(), textHeight) + self.buttonWikipedia.resize(self.buttonWikipedia.x(), textHeight) + self.buttonAudubon.resize(self.buttonWikipedia.x(), textHeight) + self.buttonAllAboutBirds.resize(self.buttonWikipedia.x(), textHeight) + self.buttonChecklists.resize(self.buttonWikipedia.x(), textHeight) + self.buttonMap.resize(self.buttonWikipedia.x(), textHeight) root = self.trLocations.invisibleRootItem() for i in range(root.childCount()): diff --git a/code_Lists.py b/code_Lists.py index 18fd301..186886f 100644 --- a/code_Lists.py +++ b/code_Lists.py @@ -4,6 +4,7 @@ import code_Location import code_Individual import code_FloatDelegate +import code_Stylesheet # import basic Python libraries from copy import deepcopy @@ -30,7 +31,6 @@ ) - class Lists(QMdiSubWindow, form_Lists.Ui_frmSpeciesList): # create "resized" as a signal that the window can emit @@ -41,17 +41,12 @@ class Lists(QMdiSubWindow, form_Lists.Ui_frmSpeciesList): def __init__(self): super(self.__class__, self).__init__() self.setupUi(self) - self.mdiParent = "" + + self.setAttribute(Qt.WA_DeleteOnClose,True) + self.tblList.doubleClicked.connect(self.tblListClicked) self.btnShowLocation.clicked.connect(self.CreateLocation) self.txtFind.textChanged.connect(self.ChangedFindText) - self.resized.connect(self.resizeMe) - self.currentSpeciesList = [] - self.btnShowLocation.setVisible(False) - self.lblDetails.setVisible(False) - self.filter = () - self.listType = "" - self.actionSetDateFilter.triggered.connect(self.setDateFilter) self.actionSetFirstDateFilter.triggered.connect(self.setFirstDateFilter) self.actionSetLastDateFilter.triggered.connect(self.setLastDateFilter) @@ -60,6 +55,16 @@ def __init__(self): self.actionSetStateFilter.triggered.connect(self.setStateFilter) self.actionSetCountyFilter.triggered.connect(self.setCountyFilter) self.actionSetLocationFilter.triggered.connect(self.setLocationFilter) + self.tblList.horizontalHeader().sortIndicatorChanged.connect(self.tblList.resizeRowsToContents) + self.resized.connect(self.resizeMe) + + self.btnShowLocation.setVisible(False) + self.lblDetails.setVisible(False) + + self.mdiParent = "" + self.currentSpeciesList = [] + self.filter = () + self.listType = "" def resizeEvent(self, event): @@ -78,6 +83,7 @@ def resizeMe(self): def setCountyFilter(self): + if self.listType in ["Checklists"]: if self.listType == "Checklists": countyName= self.tblList.item(self.tblList.currentRow(), 2).text() @@ -91,7 +97,8 @@ def setCountryFilter(self): self.mdiParent.setCountryFilter(countryName) - def setDateFilter(self): + def setDateFilter(self): + if self.listType in ["Checklists", "Single Checklist"]: if self.listType == "Checklists": date = self.tblList.item(self.tblList.currentRow(), 4).text() @@ -101,6 +108,7 @@ def setDateFilter(self): def setFirstDateFilter(self): + if self.listType in ["Species", "Locations"]: if self.listType == "Species": date = self.tblList.item(self.tblList.currentRow(), 2).text() @@ -120,6 +128,7 @@ def setLastDateFilter(self): def setLocationFilter(self): + if self.listType in ["Locations", "Single Checklist", "Checklists"]: if self.listType == "Locations": locationName= self.tblList.item(self.tblList.currentRow(), 0).text() @@ -131,12 +140,14 @@ def setLocationFilter(self): def setSpeciesFilter(self): + if self.listType in ["Species", "Single Checklist"]: speciesName = self.tblList.item(self.tblList.currentRow(), 1).text() self.mdiParent.setSpeciesFilter(speciesName) def setStateFilter(self): + if self.listType in ["Checklists"]: if self.listType == "Checklists": stateName= self.tblList.item(self.tblList.currentRow(), 1).text() @@ -495,9 +506,9 @@ def FillSpecies(self, filter): font.setBold(True) if filter.getLocationType() == "Location": - self.btnShowLocation.setVisible(True) + self.btnShowLocation.setVisible(True) - # set up tblList column headers and widths + # set up tblList column headers and widths self.tblList.setShowGrid(False) header = self.tblList.horizontalHeader() header.setVisible(True) @@ -506,13 +517,13 @@ def FillSpecies(self, filter): if filter.getChecklistID() == "": thisWindowList = self.mdiParent.db.GetSpeciesWithData(filter, [], "Subspecies") - thisCleanedWindowList = [] - - # clean out spuh and slash entries - for s in range(len(thisWindowList)): - if not("sp." in thisWindowList[s][0] or "/" in thisWindowList[s][0]): - thisCleanedWindowList.append(thisWindowList[s]) - thisWindowList = thisCleanedWindowList +# thisCleanedWindowList = [] +# +# # clean out spuh and slash entries +# for s in range(len(thisWindowList)): +# if not("sp." in thisWindowList[s][0] or "/" in thisWindowList[s][0]): +# thisCleanedWindowList.append(thisWindowList[s]) +# thisWindowList = thisCleanedWindowList if len(thisWindowList) == 0: return(False) @@ -545,7 +556,12 @@ def FillSpecies(self, filter): self.tblList.setItem(R, 1, speciesItem) self.tblList.item(R, 1).setFont(font) - self.tblList.item(R, 1).setForeground(Qt.blue) + + # set the species to gray if it's not a true species + if " x " in species[0] or "sp." in species[0] or "/" in species[0]: + self.tblList.item(R, 1).setForeground(Qt.gray) + else: + self.tblList.item(R, 1).setForeground(code_Stylesheet.speciesColor) self.tblList.setItem(R, 2, firstItem) self.tblList.setItem(R, 3, lastItem) @@ -597,9 +613,14 @@ def FillSpecies(self, filter): self.tblList.setItem(R, 0, taxItem) self.tblList.setItem(R, 1, speciesItem) self.tblList.item(R, 1).setFont(font) - self.tblList.item(R, 1).setForeground(Qt.blue) self.tblList.setItem(R, 2, countItem) self.tblList.setItem(R, 3, commentItem) + + # set the species to gray if it's not a true species + if " x " in s["commonName"] or "sp." in s["commonName"] or "/" in s["commonName"]: + self.tblList.item(R, 1).setForeground(Qt.gray) + else: + self.tblList.item(R, 1).setForeground(code_Stylesheet.speciesColor) self.currentSpeciesList.append(s["commonName"]) @@ -624,18 +645,18 @@ def FillSpecies(self, filter): distance = thisWindowList[0]["distance"] observerCount = thisWindowList[0]["observers"] - if time != "": + if time != ""and time is not None: time = time + ", " - if duration != "0": + if duration != "0" and duration is not None: duration = duration + " min, " else: duration = "" - if distance != "": + if distance != "" and distance is not None: distance = distance + " km, " - if observerCount != "": + if observerCount != "" and observerCount is not None: observerCount = observerCount + " obs, " if "Traveling" in protocol: @@ -665,9 +686,9 @@ def FillSpecies(self, filter): self.lblSpecies.setText( "Species: " + str(speciesCount) + - " plus " + + " + " + str(self.tblList.rowCount() - speciesCount) + - " other taxa" + " taxa" ) self.mdiParent.SetChildDetailsLabels(self, filter) @@ -677,6 +698,8 @@ def FillSpecies(self, filter): location = filter.getLocationName() if location != "": + if filter.getLocationType() == "Region": + location = self.mdiParent.db.GetRegionName(location) if filter.getLocationType() == "Country": location = self.mdiParent.db.GetCountryName(location) if filter.getLocationType() == "State": diff --git a/code_Location.py b/code_Location.py index 134511f..5ac8c12 100644 --- a/code_Location.py +++ b/code_Location.py @@ -6,6 +6,7 @@ import code_Lists import code_MapHtml import code_Individual +import code_Stylesheet from collections import defaultdict # import the Qt components we'll use @@ -42,6 +43,7 @@ from base64 import ( b64encode ) +import code_Stylesheet class Location(QMdiSubWindow, form_Location.Ui_frmLocation): @@ -51,6 +53,7 @@ class Location(QMdiSubWindow, form_Location.Ui_frmLocation): def __init__(self): super(self.__class__, self).__init__() self.setupUi(self) + self.setAttribute(Qt.WA_DeleteOnClose,True) self.mdiParent = "" self.resized.connect(self.resizeMe) self.tblDates.currentItemChanged.connect(self.FillSpeciesForDate) @@ -231,12 +234,11 @@ def FillSpecies(self): tempFilter = code_Filter.Filter() tempFilter.setLocationType("Location") tempFilter.setLocationName(location) - speciesList = [] # get species data from db thisWindowList = self.mdiParent.db.GetSpeciesWithData(tempFilter) - # set up tblSpecies column headers and widths + # set up tblSpecies column headers and widths self.tblSpecies.setColumnCount(6) self.tblSpecies.setRowCount(len(thisWindowList)+1) self.tblSpecies.horizontalHeader().setVisible(True) @@ -244,9 +246,14 @@ def FillSpecies(self): header = self.tblSpecies.horizontalHeader() header.setSectionResizeMode(1, QHeaderView.Stretch) self.tblSpecies.setShowGrid(False) + + font = QFont() + font.setBold(True) + count = 0 + nonSpeciesTaxaCount = 0 # add species and dates to table row by row - R = 1 + R = 0 for species in thisWindowList: taxItem = QTableWidgetItem() taxItem.setData(Qt.DisplayRole, R) @@ -268,13 +275,23 @@ def FillSpecies(self): self.tblSpecies.setItem(R, 4, checklistsItem) self.tblSpecies.setItem(R, 5, percentageItem) - speciesList.append(species[0]) - R = R + 1 + self.tblSpecies.item(R, 1).setFont(font) - self.tblSpecies.removeRow(0) - - count = self.mdiParent.db.CountSpecies(speciesList) - self.lblSpecies.setText("Species (" + str(count) + "):") + # set the species to gray if it's not a true species + if " x " in species[0] or "sp." in species[0] or "/" in species[0]: + self.tblSpecies.item(R, 1).setForeground(Qt.gray) + nonSpeciesTaxaCount += 1 + else: + self.tblSpecies.item(R, 1).setForeground(code_Stylesheet.speciesColor) + count += 1 + + R += 1 + + labelText = "Species: " + str(count) + if nonSpeciesTaxaCount > 0: + labelText = labelText + " + " + str(nonSpeciesTaxaCount) + " taxa" + + self.lblSpecies.setText(labelText) def SetDate(self, date): @@ -302,13 +319,35 @@ def FillSpeciesForDate(self): species = self.mdiParent.db.GetSpecies(tempFilter) - self.lstSpecies.addItems(species) - self.lstSpecies.setCurrentRow(0) - self.lstSpecies.setSpacing(2) + font = QFont() + font.setBold(True) - count = self.mdiParent.db.CountSpecies(species) + count = 0 + nonSpeciesTaxaCount = 0 - self.lblSpeciesSeen.setText("Species for selected date (" + str(count) + ")") + R = 0 + for s in species: + + self.lstSpecies.addItem(s) + self.lstSpecies.item(R).setFont(font) + # set the species to gray if it's not a true species + if " x " in s or "sp." in s or "/" in s: + self.lstSpecies.item(R).setForeground(Qt.gray) + nonSpeciesTaxaCount += 1 + else: + self.lstSpecies.item(R).setForeground(code_Stylesheet.speciesColor) + count += 1 + + R += 1 + + labelText = "Species: " + str(count) + if nonSpeciesTaxaCount > 0: + labelText = labelText + " + " + str(nonSpeciesTaxaCount) + " taxa" + + self.lstSpecies.setCurrentRow(0) + self.lstSpecies.setSpacing(2) + + self.lblSpeciesSeen.setText(labelText) def html(self): diff --git a/code_LocationTotals.py b/code_LocationTotals.py index 72b062c..ce07b66 100644 --- a/code_LocationTotals.py +++ b/code_LocationTotals.py @@ -1,5 +1,9 @@ # import GUI form for this class import form_LocationTotals +# import pandas +# import folium +# import json +# from branca.colormap import LinearColormap # import classes from other project files import code_Filter @@ -42,8 +46,10 @@ class LocationTotals(QMdiSubWindow, form_LocationTotals.Ui_frmLocationTotals): def __init__(self): super(self.__class__, self).__init__() self.setupUi(self) + self.setAttribute(Qt.WA_DeleteOnClose,True) self.mdiParent = "" self.resized.connect(self.resizeMe) + self.tblRegionTotals.itemDoubleClicked.connect(lambda: self.CreateListForLocation("Region")) self.tblCountryTotals.itemDoubleClicked.connect(lambda: self.CreateListForLocation("Country")) self.tblStateTotals.itemDoubleClicked.connect(lambda: self.CreateListForLocation("State")) self.tblCountyTotals.itemDoubleClicked.connect(lambda: self.CreateListForLocation("County")) @@ -64,7 +70,9 @@ def __init__(self): def CreateListForLocation(self, locationType): tempFilter = deepcopy(self.filter) - + + if locationType == "Region": + locationName = self.mdiParent.db.GetRegionCode(self.tblRegionTotals.item(self.tblRegionTotals.currentRow(), 1).text()) if locationType == "Country": locationName = self.mdiParent.db.GetCountryCode(self.tblCountryTotals.item(self.tblCountryTotals.currentRow(), 1).text()) if locationType == "State": @@ -142,6 +150,50 @@ def html(self): "" ) + html = html + ( + "
Rank | " + + "Region | " + + "Species | " + + "Checklists | " + + "
---|---|---|---|
" + + self.tblRegionTotals.item(r, 0).text() + + " | " + + "" + + self.tblRegionTotals.item(r, 1).text() + + " | " + + "" + + self.tblRegionTotals.item(r, 2).text() + + " | " + + "" + + self.tblRegionTotals.item(r, 3).text() + + " | " + + "