---
title: "Journal de Vol - FlightLog Pro"
format:
html:
toc: false
---
```{r setup, include=FALSE}
knitr::opts_chunk$set(echo = FALSE, warning = FALSE, message = FALSE)
library(jsonlite)
library(DT)
library(knitr)
# Chargement des données
db_file <- "data/processed/flights.json"
if (file.exists(db_file)) {
db <- fromJSON(db_file, simplifyVector = FALSE)
has_data <- length(db$flights) > 0
} else {
has_data <- FALSE
db <- list(flights = list(), global_stats = list())
}
```
## 🪂 Mon journal de vol
Le carnet de mes vols en parapente, généré automatiquement à partir des fichiers IGC de mon vario : statistiques, traces et profils d'altitude de chaque vol.
```{r conditional-stats, results='asis'}
if (has_data) {
cat("## 📊 Statistiques générales\n\n")
} else {
cat("## 🚀 Premiers pas\n\n")
cat("Aucun vol n'a encore été traité. Importez votre premier vol Ultrabip avec `source('scripts/journal_manager.R')` !\n\n")
}
```
```{r stats-display, results='asis'}
if (has_data && !is.null(db$global_stats)) {
source("scripts/flightlog_theme.R")
stats <- db$global_stats
# Formatage du temps de vol total en heures et minutes
total_hours <- stats$total_airtime_hours %||% 0
hours <- floor(total_hours)
minutes <- floor((total_hours - hours) * 60)
formatted_airtime <- sprintf("%dh %02dm", hours, minutes)
cat(fl_stat_tiles(
data.frame(
icon = c(
"🛩️", "⏱️", "📏",
"🏆", "⛰️", "🪂", "📊", "📊",
"📈", "🚀", "📊", "🪁", "📊", "⬆️", "🛰️"
),
label = c(
"Vols", "Temps de vol", "Distance totale",
"Vol le plus long", "Altitude max", "Hauteur-sol record",
"Durée moyenne", "Distance moyenne",
"Cumul gain altitude", "Vitesse max", "Vitesse moyenne",
"Meilleure finesse", "Finesse moyenne", "Taux montée max",
"Vols avec alt. GPS"
),
value = c(
as.character(stats$total_flights %||% 0),
formatted_airtime,
as.character(stats$total_distance_km %||% 0),
as.character(stats$longest_flight_km %||% 0),
as.character(stats$highest_altitude_m %||% 0),
as.character(stats$highest_agl_m %||% 0),
as.character(round(stats$avg_flight_duration_min %||% 0)),
as.character(round(stats$avg_flight_distance_km %||% 0, 2)),
as.character(stats$total_elevation_gain_m %||% 0),
as.character(stats$max_speed_kmh %||% 0),
as.character(round(stats$avg_speed_kmh %||% 0, 1)),
paste0(stats$best_glide_ratio %||% "N/A", ":1"),
paste0(round(stats$avg_glide_ratio %||% 0, 1), ":1"),
as.character(stats$max_climb_rate %||% 0),
paste(stats$flights_with_gps_altitude %||% 0, "/", stats$total_flights %||% 0)
),
unit = c(
"", "", "km",
"km", "m", "m", "min", "km",
"m", "km/h", "km/h", "", "", "m/s", ""
),
stringsAsFactors = FALSE
),
hero = 3
))
}
```
```{r hike-stats, results='asis'}
# Statistiques Hike & Fly si disponibles
if (has_data && !is.null(db$global_stats) && (db$global_stats$total_hike_flights %||% 0) > 0) {
cat("## 🥾 Statistiques Hike & Fly\n\n")
cat(fl_stat_tiles(data.frame(
icon = c("🥾", "⏱️", "📈"),
label = c("Vols hike & fly", "Temps de marche", "Gain altitude à pied"),
value = c(
as.character(db$global_stats$total_hike_flights),
as.character(db$global_stats$total_hike_hours %||% 0),
as.character(db$global_stats$total_elevation_gain_hiking %||% 0)
),
unit = c("vols", "h", "m"),
stringsAsFactors = FALSE
)))
}
```
## 📍 Sites favoris
::: {.grid}
::: {.g-col-6}
**🛫 Décollages**
```{r fav-takeoff, results='asis'}
if (has_data && !is.null(db$global_stats$favorite_takeoff_sites)) {
sites <- db$global_stats$favorite_takeoff_sites
if (length(sites) > 0) {
for (i in 1:length(sites)) {
site_name <- names(sites)[i]
if (site_name != "") {
cat(sprintf("- **%s** (%d vols)\n", site_name, sites[[i]]))
}
}
} else {
cat("_Pas encore de données._")
}
} else {
cat("_Pas encore de données._")
}
```
:::
::: {.g-col-6}
**🛬 Atterrissages**
```{r fav-landing, results='asis'}
if (has_data && !is.null(db$global_stats$favorite_landing_sites)) {
sites <- db$global_stats$favorite_landing_sites
if (length(sites) > 0) {
for (i in 1:length(sites)) {
site_name <- names(sites)[i]
if (site_name != "") {
cat(sprintf("- **%s** (%d vols)\n", site_name, sites[[i]]))
}
}
} else {
cat("_Pas encore de données._")
}
} else {
cat("_Pas encore de données._")
}
```
:::
:::
## 📈 Fréquence de vol
```{r flight-frequency}
#| fig-width: 9
#| fig-height: 4
#| fig-dpi: 150
#| fig-alt: "Histogramme du nombre de vols par mois depuis le premier vol, avec une droite de tendance linéaire indiquant l'évolution du rythme de vol dans le temps."
if (has_data && length(db$flights) > 1) {
library(ggplot2)
# Même palette et même thème que les graphiques des vols
source("scripts/flightlog_theme.R")
flight_dates <- as.Date(sapply(db$flights, function(f) f$date %||% NA))
flight_dates <- flight_dates[!is.na(flight_dates)]
month_start <- function(d) as.Date(format(d, "%Y-%m-01"))
flight_months <- month_start(flight_dates)
# Séquence complète des mois (y compris ceux sans vol) pour une régression
# non biaisée par les mois omis
all_months <- seq(min(flight_months), max(flight_months), by = "month")
monthly_df <- data.frame(
month = all_months,
n = as.integer(table(factor(flight_months, levels = as.character(all_months))))
)
monthly_df$month_num <- as.numeric(monthly_df$month)
trend <- lm(n ~ month_num, data = monthly_df)
slope_per_year <- round(coef(trend)[["month_num"]] * 365.25, 1)
trend_label <- if (abs(slope_per_year) < 0.1) {
"stable"
} else if (slope_per_year > 0) {
sprintf("+%.1f vols/an", slope_per_year)
} else {
sprintf("%.1f vols/an", slope_per_year)
}
ggplot(monthly_df, aes(x = month, y = n)) +
geom_col(fill = fl_colors$flight, width = 22) +
geom_smooth(
method = "lm", se = FALSE, colour = fl_colors$accent,
linetype = "dashed", linewidth = 0.7
) +
labs(
title = "Fréquence de vol dans le temps",
subtitle = paste("Tendance linéaire :", trend_label),
x = NULL, y = "Vols / mois",
caption = "Barres : nombre de vols par mois · pointillé : tendance linéaire sur toute la période"
) +
scale_x_date(date_breaks = "6 months", date_labels = "%b %Y") +
scale_y_continuous(
breaks = scales::pretty_breaks(),
expand = expansion(mult = c(0, 0.08))
) +
theme_flightlog() +
theme(axis.text.x = element_text(angle = 45, hjust = 1))
}
```
## 🗓️ Derniers vols
```{r recent-flights}
#| results: asis
if (has_data && length(db$flights) > 0) {
# Tri des vols par date (les plus récents en premier)
flights_sorted <- db$flights[order(sapply(db$flights, function(f) f$date %||% ""), decreasing = TRUE)]
# Affichage des 5 derniers vols
recent_flights <- head(flights_sorted, 5)
for (flight in recent_flights) {
# Formatage de la durée en minutes et secondes
duration_min <- floor(flight$duration_minutes %||% 0)
duration_sec <- floor(((flight$duration_minutes %||% 0) - duration_min) * 60)
formatted_duration <- sprintf("%dm %02ds", duration_min, duration_sec)
cat(sprintf(
"- **[%s - %s](posts/%s/%s.qmd)** - %s, %.2f km, +%s m\n",
flight$date_display %||% flight$date %||% "Date inconnue",
flight$takeoff_time %||% "Heure inconnue",
flight$id %||% "",
flight$id %||% "",
formatted_duration,
flight$total_distance_km %||% 0,
flight$elevation_gain_total %||% "?"
))
}
}
```
---
*Carnet de vol personnel — détails techniques sur la page [À propos](about.qmd).*