diff --git a/download_phenocam.py b/download_phenocam.py index 2e21733..f352539 100644 --- a/download_phenocam.py +++ b/download_phenocam.py @@ -1,7 +1,10 @@ +import csv +import json import requests from pathlib import Path from datetime import datetime from concurrent.futures import ThreadPoolExecutor, as_completed +from io import StringIO PHENOCAM_API = "https://phenocam.nau.edu/api" @@ -144,3 +147,70 @@ def download_phenocam(season, site_position, site_name, date_range=None): print("[PhenoCam] Completed") + +def download_phenocam_greenness(season, site_position, site_name, date_range=None): + """Fetch greenness-index time series from PhenoCam API.""" + datetime_range = date_range or f"{season}-01-01/{season}-12-31" + output_file = Path(f"data/{site_name}/{season}/raw/phenocam/timeseries.json") + output_file.parent.mkdir(parents=True, exist_ok=True) + + start_date, end_date = datetime_range.split("/") + start_dt = datetime.strptime(start_date, "%Y-%m-%d") + end_dt = datetime.strptime(end_date, "%Y-%m-%d") + + print(f"[PhenoCam-GI] Fetching greenness-index time series: {site_name}, {season}") + + # Get ROIs for site (paginate through results) + try: + url = f"{PHENOCAM_API}/roilists/" + params = {"site": site_name} + rois = [] + while url: + r = requests.get(url, params=params, timeout=30) + r.raise_for_status() + data = r.json() + rois.extend([roi for roi in data.get("results", []) if roi["site"] == site_name]) + url = data.get("next") + params = None + if len(rois) > 0: + break + if not rois: + print(f"[PhenoCam-GI] No ROIs found for site '{site_name}'") + return + csv_url = rois[0].get("one_day_summary") + if not csv_url: + print(f"[PhenoCam-GI] No CSV data URL found for ROI") + return + except requests.exceptions.RequestException as e: + print(f"[PhenoCam-GI] Error fetching ROIs: {e}") + return + + # Fetch CSV data + try: + csv_r = requests.get(csv_url, timeout=30) + csv_r.raise_for_status() + lines = [l for l in csv_r.text.split('\n') if l and not l.startswith('#')] + reader = csv.DictReader(lines) + timeseries = [] + for row in reader: + try: + date_str = row.get("date") + if not date_str: + continue + date = datetime.strptime(date_str, "%Y-%m-%d") + if start_dt <= date <= end_dt: + gcc = row.get("gcc_mean") + if gcc and gcc != "NA": + timeseries.append({"date": date.isoformat(), "greenness_index": float(gcc)}) + except (ValueError, KeyError): + continue + except requests.exceptions.RequestException as e: + print(f"[PhenoCam-GI] Error fetching CSV: {e}") + return + + timeseries.sort(key=lambda x: x["date"]) + with open(output_file, "w") as f: + json.dump(timeseries, f, indent=2) + + print(f"[PhenoCam-GI] Saved: {output_file} ({len(timeseries)} entries)") + diff --git a/run.py b/run.py index 37bf7ac..d4b74d8 100644 --- a/run.py +++ b/run.py @@ -8,7 +8,7 @@ from ndvi import ( ) from download_s2 import download_s2 from download_s3 import download_s3 -from download_phenocam import download_phenocam +from download_phenocam import download_phenocam, download_phenocam_greenness from clouds import detect_clouds @@ -18,6 +18,7 @@ def run_pipeline(season, site_position, site_name): # download_s2(season, site_position, site_name) # download_s3(season, site_position, site_name) # download_phenocam(season, site_position, site_name) + download_phenocam_greenness(season, site_position, site_name) # print(f"Generating NDVI for raw data: {site_name}, {season}") # generate_ndvi_raw(season, site_position, site_name) @@ -34,10 +35,10 @@ def run_pipeline(season, site_position, site_name): #run_efast(season, site_position, site_name) #print(f"Post-processing data: {site_name}, {season}") - process_cropped(season, site_position, site_name) + #process_cropped(season, site_position, site_name) #print(f"Generating NDVI for final outputs: {site_name}, {season}") - generate_ndvi_post_process(season, site_position, site_name) - create_ndvi_timeseries_post_process(season, site_position, site_name) + #generate_ndvi_post_process(season, site_position, site_name) + #create_ndvi_timeseries_post_process(season, site_position, site_name) except Exception as e: print(f"Error: {e}") diff --git a/webapp/index.html b/webapp/index.html index 396e1c9..49574f0 100644 --- a/webapp/index.html +++ b/webapp/index.html @@ -18,6 +18,7 @@ .phenocam-date { font-size: 11px; margin-top: 5px; color: #999; } .phenocam-image { width: 100%; height: 200px; object-fit: contain; border: 1px solid #ccc; } .sitemap { height: 200px; border: 1px solid #ccc; margin-top: 32px; } + #greennesstimeseries { margin-top: 0; } #dateSlider { width: 100%; } #dateDisplay { text-align: center; margin: 10px 0; font-size: 18px; } .maps { display: flex; gap: 20px; } @@ -42,12 +43,14 @@

Innsbruck

2024

-
-
PhenoCam
PhenoCam
+
+
Greenness Index Timeseries
+ +
@@ -116,6 +119,7 @@ const markers = { s2: null, fusion: null, s3: null }; const ndviMarkers = { s2: null, fusion: null, s3: null }; let timeseries = { s2: [], fusion: [], s3: [] }; + let greennessTimeseries = []; // Add site marker to all maps for (const source of ["s2", "fusion", "s3"]) { @@ -140,13 +144,84 @@ }); async function loadTimeseries() { - const [s2, fusion, s3] = await Promise.all([ + const [s2, fusion, s3, greenness] = await Promise.all([ fetch("../data/innsbruck/2024/processed/ndvi/s2/timeseries.json").then(r => r.json()), fetch("../data/innsbruck/2024/processed/ndvi/fusion/timeseries.json").then(r => r.json()).catch(() => []), - fetch("../data/innsbruck/2024/processed/ndvi/s3/timeseries.json").then(r => r.json()) + fetch("../data/innsbruck/2024/processed/ndvi/s3/timeseries.json").then(r => r.json()), + fetch("../data/innsbruck/2024/raw/phenocam/timeseries.json").then(r => r.json()).catch(() => []) ]); timeseries = { s2, fusion, s3 }; + greennessTimeseries = greenness; drawTimeseries(); + drawGreennessTimeseries(); + } + + function drawGreennessTimeseries() { + const canvas = document.getElementById("greennesstimeseries"); + const ctx = canvas.getContext("2d"); + canvas.width = canvas.offsetWidth; + canvas.height = 120; + const w = canvas.width, h = canvas.height; + const pad = 30; + const plotW = w - pad * 2, plotH = h - pad * 2; + + ctx.clearRect(0, 0, w, h); + const data = greennessTimeseries.filter(t => t.date && t.greenness_index !== null && t.greenness_index !== undefined); + if (!data.length) return; + + const dates = data.map(t => new Date(t.date)); + const minDate = new Date(Math.min(...dates)); + const maxDate = new Date(Math.max(...dates)); + const dateRange = maxDate - minDate || 1; + + const values = data.map(t => t.greenness_index); + const minVal = Math.min(...values); + const maxVal = Math.max(...values); + const valRange = maxVal - minVal || 1; + + const x = (d) => pad + ((new Date(d) - minDate) / dateRange) * plotW; + const y = (v) => pad + plotH - ((v - minVal) / valRange) * plotH; + + ctx.strokeStyle = "#ccc"; + ctx.beginPath(); + ctx.moveTo(pad, pad); + ctx.lineTo(pad, pad + plotH); + ctx.lineTo(pad + plotW, pad + plotH); + ctx.stroke(); + + ctx.fillStyle = "#000"; + ctx.font = "9px sans-serif"; + ctx.fillText(minVal.toFixed(3), 2, pad + plotH + 10); + ctx.fillText(maxVal.toFixed(3), 2, pad + 3); + + ctx.strokeStyle = "#00aa00"; + ctx.beginPath(); + let first = true; + for (const t of data) { + const px = x(t.date), py = y(t.greenness_index); + if (first) { ctx.moveTo(px, py); first = false; } + else ctx.lineTo(px, py); + } + ctx.stroke(); + + const currentDate = dateFromDays(parseInt(slider.value)); + const xPos = x(currentDate); + ctx.strokeStyle = "#f00"; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.moveTo(xPos, pad); + ctx.lineTo(xPos, pad + plotH); + ctx.stroke(); + + const closest = data.reduce((c, t) => + Math.abs(new Date(t.date) - new Date(currentDate)) < Math.abs(new Date(c.date) - new Date(currentDate)) ? t : c + ); + if (closest && closest.greenness_index !== null) { + const yPos = y(closest.greenness_index); + ctx.fillStyle = "#f00"; + ctx.font = "bold 10px sans-serif"; + ctx.fillText(closest.greenness_index.toFixed(3), xPos + 5, yPos - 5); + } } function drawTimeseries() { @@ -430,6 +505,7 @@ params.set("date", date); history.replaceState({}, "", `?${params}`); drawTimeseries(); + drawGreennessTimeseries(); for (const source of ["s2", "fusion", "s3"]) { const filename = await findFile(date, source); if (filename) {