Added greeness index from phenocam.
This commit is contained in:
parent
415af89c7d
commit
31dc536c3a
3 changed files with 155 additions and 8 deletions
|
|
@ -1,7 +1,10 @@
|
||||||
|
import csv
|
||||||
|
import json
|
||||||
import requests
|
import requests
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||||
|
from io import StringIO
|
||||||
|
|
||||||
PHENOCAM_API = "https://phenocam.nau.edu/api"
|
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")
|
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)")
|
||||||
|
|
||||||
|
|
|
||||||
9
run.py
9
run.py
|
|
@ -8,7 +8,7 @@ from ndvi import (
|
||||||
)
|
)
|
||||||
from download_s2 import download_s2
|
from download_s2 import download_s2
|
||||||
from download_s3 import download_s3
|
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
|
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_s2(season, site_position, site_name)
|
||||||
# download_s3(season, site_position, site_name)
|
# download_s3(season, site_position, site_name)
|
||||||
# download_phenocam(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}")
|
# print(f"Generating NDVI for raw data: {site_name}, {season}")
|
||||||
# generate_ndvi_raw(season, site_position, site_name)
|
# 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)
|
#run_efast(season, site_position, site_name)
|
||||||
|
|
||||||
#print(f"Post-processing data: {site_name}, {season}")
|
#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}")
|
#print(f"Generating NDVI for final outputs: {site_name}, {season}")
|
||||||
generate_ndvi_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)
|
#create_ndvi_timeseries_post_process(season, site_position, site_name)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error: {e}")
|
print(f"Error: {e}")
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
.phenocam-date { font-size: 11px; margin-top: 5px; color: #999; }
|
.phenocam-date { font-size: 11px; margin-top: 5px; color: #999; }
|
||||||
.phenocam-image { width: 100%; height: 200px; object-fit: contain; border: 1px solid #ccc; }
|
.phenocam-image { width: 100%; height: 200px; object-fit: contain; border: 1px solid #ccc; }
|
||||||
.sitemap { height: 200px; border: 1px solid #ccc; margin-top: 32px; }
|
.sitemap { height: 200px; border: 1px solid #ccc; margin-top: 32px; }
|
||||||
|
#greennesstimeseries { margin-top: 0; }
|
||||||
#dateSlider { width: 100%; }
|
#dateSlider { width: 100%; }
|
||||||
#dateDisplay { text-align: center; margin: 10px 0; font-size: 18px; }
|
#dateDisplay { text-align: center; margin: 10px 0; font-size: 18px; }
|
||||||
.maps { display: flex; gap: 20px; }
|
.maps { display: flex; gap: 20px; }
|
||||||
|
|
@ -42,12 +43,14 @@
|
||||||
<div class="header-col site-info">
|
<div class="header-col site-info">
|
||||||
<h1 id="siteName">Innsbruck</h1>
|
<h1 id="siteName">Innsbruck</h1>
|
||||||
<h2 id="season">2024</h2>
|
<h2 id="season">2024</h2>
|
||||||
</div>
|
|
||||||
<div class="header-col">
|
|
||||||
<div class="phenocam-label">PhenoCam</div>
|
<div class="phenocam-label">PhenoCam</div>
|
||||||
<div id="phenocamdate" class="phenocam-date"></div>
|
<div id="phenocamdate" class="phenocam-date"></div>
|
||||||
<img id="phenocamimage" class="phenocam-image" alt="PhenoCam">
|
<img id="phenocamimage" class="phenocam-image" alt="PhenoCam">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="header-col">
|
||||||
|
<div class="timeseries-label">Greenness Index Timeseries</div>
|
||||||
|
<canvas id="greennesstimeseries" class="timeseries"></canvas>
|
||||||
|
</div>
|
||||||
<div class="header-col">
|
<div class="header-col">
|
||||||
<div id="sitemap" class="sitemap"></div>
|
<div id="sitemap" class="sitemap"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -116,6 +119,7 @@
|
||||||
const markers = { s2: null, fusion: null, s3: null };
|
const markers = { s2: null, fusion: null, s3: null };
|
||||||
const ndviMarkers = { s2: null, fusion: null, s3: null };
|
const ndviMarkers = { s2: null, fusion: null, s3: null };
|
||||||
let timeseries = { s2: [], fusion: [], s3: [] };
|
let timeseries = { s2: [], fusion: [], s3: [] };
|
||||||
|
let greennessTimeseries = [];
|
||||||
|
|
||||||
// Add site marker to all maps
|
// Add site marker to all maps
|
||||||
for (const source of ["s2", "fusion", "s3"]) {
|
for (const source of ["s2", "fusion", "s3"]) {
|
||||||
|
|
@ -140,13 +144,84 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
async function loadTimeseries() {
|
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/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/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 };
|
timeseries = { s2, fusion, s3 };
|
||||||
|
greennessTimeseries = greenness;
|
||||||
drawTimeseries();
|
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() {
|
function drawTimeseries() {
|
||||||
|
|
@ -430,6 +505,7 @@
|
||||||
params.set("date", date);
|
params.set("date", date);
|
||||||
history.replaceState({}, "", `?${params}`);
|
history.replaceState({}, "", `?${params}`);
|
||||||
drawTimeseries();
|
drawTimeseries();
|
||||||
|
drawGreennessTimeseries();
|
||||||
for (const source of ["s2", "fusion", "s3"]) {
|
for (const source of ["s2", "fusion", "s3"]) {
|
||||||
const filename = await findFile(date, source);
|
const filename = await findFile(date, source);
|
||||||
if (filename) {
|
if (filename) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue