Skip to content

Commit

Permalink
refactor: ♻️ refactor structure, comments and cleanup about page
Browse files Browse the repository at this point in the history
  • Loading branch information
HannesOberreiter authored Mar 7, 2024
1 parent 3642e51 commit 807d4a4
Show file tree
Hide file tree
Showing 11 changed files with 732 additions and 545 deletions.
34 changes: 24 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,39 @@
# GBIF - Extinct

Project to keep track for the last seen observations in each country for species taxa on GBIF. This project was made to help working with the GBIF data, as you cannot easily filter for latest observation of multiple taxa on GBIF.
## A Tool for Exploring Global Biodiversity Information Facility (GBIF) Data with Enhanced Filtering Capabilities and Potential Applications in Uncovering "Forgotten Taxa"

## Taxa
GBIF-Extinct introduces a tool that facilitates the exploration of the Global Biodiversity Information Facility (GBIF) data with unique filtering functionalities not readily available on the official GBIF website. The homepage enables users to search for the latest observation of specific taxa across different countries, offering insights into spatiotemporal distribution patterns. Users can refine their search by applying filters based on taxon name, taxonomic rank, and country.

To migrate taxa into our database, we use the backbone taxonomy from GBIF, see [hosted-datasets.gbif.org/datasets/backbone/README.html](https://hosted-datasets.gbif.org/datasets/backbone/README.html) for details. To fill the database the `Taxon.tsv` and the `simple.txt` ([github.com/gbif/.../backbone-ddl.sql](https://github.com/gbif/checklistbank/blob/master/checklistbank-mybatis-service/src/main/resources/backbone-ddl.sql)).
This user-friendly interface provides several advantages:

Running the mutate script will fill the database with the latest backbone taxonomy from GBIF, set synonyms and delete possible taxa which are synonyms for non species rank taxa.
- Identification of "Forgotten Taxa": By focusing on the latest observation, the homepage can potentially highlight taxa that have not been recently recorded (potentially "forgotten taxa") within specific regions. This can guide researchers and conservationists towards understudied species and areas, promoting targeted biodiversity research and conservation efforts.
- Enhanced Filtering Capabilities: The homepage's ability to filter by taxonomic rank allows users to explore data at different levels of the taxonomic hierarchy, catering to a broader range of research interests.
- Accessibility and User-Friendliness: The readily accessible homepage interface, with its intuitive filtering options, empowers researchers, students, and citizen scientists with a convenient tool to explore and analyze GBIF data efficiently.

```bash
go run ./scripts/mutate/mutate.go
```
Overall, it offers a valuable contribution to the field of biodiversity research by providing a user-friendly and versatile platform for exploring GBIF data, potentially leading to the identification of "forgotten taxa" and promoting a deeper understanding of global biodiversity patterns.

## Development

For ease of development we use [cosmtrek/air](https://github.com/cosmtrek/air) to automatically reload the server when changes are made. See the [config](.air.toml) file for the configuration.
The project is open-source and contributions are welcome [github.com/HannesOberreiter/gbif-extinct](https://github.com/HannesOberreiter/gbif-extinct). The project is written in Go and uses Echo as a web framework. HTMX and Tailwind CSS and templ are used for the frontend. The database is DuckDB as it can be deployed as binary inside the go application.

### Pre-requisites

To get development running you will need [Go](https://golang.org/doc/install) the standalone version of [Tailwind CSS Standalone CLI](https://tailwindcss.com/blog/standalone-cli) and [templ](https://templ.guide/).

## CI/CD
### Localhost

For ease of development we use [cosmtrek/air](https://github.com/cosmtrek/air) to automatically reload the server when changes are made. See the [config](.air.toml) file for the configuration.

### Taxa Data

To migrate taxa into our database, we use the backbone taxonomy from GBIF, see [hosted-datasets.gbif.org/datasets/backbone/README.html](https://hosted-datasets.gbif.org/datasets/backbone/README.html) for details. To fill the database the `Taxon.tsv` and the `simple.txt` ([github.com/gbif/.../backbone-ddl.sql](https://github.com/gbif/checklistbank/blob/master/checklistbank-mybatis-service/src/main/resources/backbone-ddl.sql)).

Running the mutate script will fill the database with the latest backbone taxonomy from GBIF, set synonyms and delete possible taxa which are synonyms for non species rank taxa.

```bash
go run ./scripts/mutate/mutate.go
```

### Docker

For ease of deployment, a GitHub action is used to generate the web-sever as a docker container [hub.docker.com/r/hannesoberreiter/gbif-extinct](https://hub.docker.com/r/hannesoberreiter/gbif-extinct).
GitHub action is used to generate the web-sever as a docker container [hub.docker.com/r/hannesoberreiter/gbif-extinct](https://hub.docker.com/r/hannesoberreiter/gbif-extinct).
127 changes: 100 additions & 27 deletions components/components.templ
Original file line number Diff line number Diff line change
@@ -1,14 +1,90 @@
package components

import (
"math"
"strconv"
"os"
"log/slog"
"fmt"

"github.com/HannesOberreiter/gbif-extinct/pkg/queries"
"github.com/HannesOberreiter/gbif-extinct/pkg/gbif"
"github.com/HannesOberreiter/gbif-extinct/internal"
"github.com/HannesOberreiter/gbif-extinct/pkg/pagination"

"golang.org/x/text/message"
)
"github.com/gomarkdown/markdown"
"github.com/gomarkdown/markdown/html"
"github.com/gomarkdown/markdown/parser")

var printer = message.NewPrinter(message.MatchLanguage("en"))

type TableRows []internal.TableRow
type Pages struct {
CURRENT int
NEXT int
PREVIOUS int
LAST int
}

type PagesString struct {
CURRENT string
NEXT string
PREVIOUS string
LAST string
}

func CalculatePages(counts queries.Counts, q queries.Query) PagesString {
var response Pages
response.CURRENT = 1

page, err := strconv.Atoi(q.PAGE)
if err == nil {
response.CURRENT = page
}


response.LAST = int(math.Ceil(float64(counts.TaxaCount) / float64(queries.PageLimit)))
if response.CURRENT == response.LAST {
response.NEXT = response.LAST
response.PREVIOUS = response.LAST - 1
} else if response.CURRENT == 1 {
response.NEXT = response.CURRENT + 1
response.PREVIOUS = response.CURRENT
} else {
response.NEXT = response.CURRENT + 1
response.PREVIOUS = response.CURRENT - 1
}

return PagesString{
CURRENT: strconv.Itoa(response.CURRENT),
NEXT: strconv.Itoa(response.NEXT),
PREVIOUS: strconv.Itoa(response.PREVIOUS),
LAST: strconv.Itoa(response.LAST),
}
}


type TableRows []queries.TableRow

var _aboutPage string;

func RenderAbout() {
md, err := os.ReadFile("README.md")
if err != nil {
slog.Error("Error reading README.md", err)
_aboutPage = "Error reading README.md"
}

extensions := parser.CommonExtensions | parser.AutoHeadingIDs | parser.NoEmptyLineBeforeBlock
p := parser.NewWithExtensions(extensions)
doc := p.Parse(md)

htmlFlags := html.CommonFlags | html.HrefTargetBlank
opts := html.RendererOptions{Flags: htmlFlags}
renderer := html.NewRenderer(opts)
res := markdown.Render(doc, renderer)
_aboutPage = string(res)
slog.Info("About page rendered")
}

// Intercept helper function to call main.js setSortingFields to update the form and submit it
script setSortingFields(orderBy string, orderDir string) {
Expand All @@ -22,9 +98,9 @@ script baseUrl() {


// Utility to create a table header with sorting
templ TableTh(columnName string, orderBy string, payload internal.Payload) {
if payload.ORDER_BY != nil && *payload.ORDER_BY == orderBy {
if payload.ORDER_DIR != nil && *payload.ORDER_DIR == "asc" {
templ TableTh(columnName string, orderBy string, q queries.Query) {
if q.ORDER_BY == orderBy {
if q.ORDER_DIR == "asc" {
<th class="text-left font-black cursor-pointer" onclick={ setSortingFields(orderBy, "desc") }> { columnName } ↓</th>
} else {
<th class="text-left font-black cursor-pointer" onclick={ setSortingFields(orderBy, "asc") }> { columnName } ↑</th>
Expand Down Expand Up @@ -95,7 +171,7 @@ templ Filter(){
</form>
}

templ Pagination(pages pagination.PagesString){
templ Pagination(pages PagesString){
<div class="flex flex-row">
<div>
<button class="uppercase tracking-wide hover:font-bold pr-sm" hx-get="/table" hx-swap="outerHTML" hx-target="#tableContainer" hx-include="#filterForm" hx-vals={ `{"page":1}` }>
Expand Down Expand Up @@ -125,7 +201,7 @@ templ Pagination(pages pagination.PagesString){



templ Table(rows TableRows, payload internal.Payload, counts internal.Counts, pages pagination.PagesString) {
templ Table(rows TableRows, q queries.Query, counts queries.Counts, pages PagesString) {
<div id="tableContainer" class="w-full">
<small>
<span>Taxa: { printer.Sprintln(counts.TaxaCount) }</span>
Expand All @@ -135,11 +211,11 @@ templ Table(rows TableRows, payload internal.Payload, counts internal.Counts, pa
<table class="w-full caption-bottom text-nowrap">
<thead class="">
<tr>
@TableTh("Scientific Name", "name", payload)
@TableTh("Scientific Name", "name", q)
<th class="text-left">Country</th>
@TableTh("Latest Observation", "date", payload)
@TableTh("Latest Observation", "date", q)
<th class="text-left">~Years</th>
@TableTh("Last Fetched", "fetch", payload)
@TableTh("Last Fetched", "fetch", q)
<th class="text-left">Synonym</th>
<th class="text-left">Taxa</th>
</tr>
Expand Down Expand Up @@ -198,15 +274,15 @@ templ Table(rows TableRows, payload internal.Payload, counts internal.Counts, pa
}

// Index landing page with table
templ PageTable(rows TableRows, payload internal.Payload, counts internal.Counts, pages pagination.PagesString){
templ PageTable(rows TableRows, q queries.Query, counts queries.Counts, pages PagesString){
@Page() {
<div class="p-4">
<section>
@Filter()
</section>
<hr class="mt-1 mb-1" />
<main>
@Table(rows, payload, counts, pages)
@Table(rows, q, counts, pages)
</main>
</div>
}
Expand All @@ -215,21 +291,18 @@ templ PageTable(rows TableRows, payload internal.Payload, counts internal.Counts
// About page
templ PageAbout(){
@Page() {
<div class="p-4">
About

lorem isopsum

## Table Columns

- **Scientific Name**: The scientific name of the species
- **Country**: The 2-letter ISO country code
- **Latest Observation**: The date of the latest observation, if only a year/month is given the first month / day of the month is used. Additionally ranges are ignored and only the first date is used
- **~Years**: The difference in years between the latest observation and the current date, calculated without leap years
- **Last Fetched**: The date when the data was last fetched from GBIF


<div class="container mx-auto px-4">
@templ.Raw(_aboutPage)

<hr class="mt-1 mb-1" />
<h3>Current Server Setup</h3>
<ul>
<li>Cron Interval: { fmt.Sprint(internal.Config.CronJobIntervalSec) } seconds</li>
<li>Taxa per Cron: { gbif.SampleRows }</li>
<li>User Agent Prefix: { internal.Config.UserAgentPrefix }</li>
</ul>
</div>

}

}
Expand Down
48 changes: 0 additions & 48 deletions internal/env.go

This file was deleted.

Loading

0 comments on commit 807d4a4

Please sign in to comment.