forked from fbee3157/Sakura-Time
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathforecast.js
More file actions
167 lines (145 loc) · 5.6 KB
/
Copy pathforecast.js
File metadata and controls
167 lines (145 loc) · 5.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
const express = require('express');
const router = express.Router();
// DTS (Dormancy Temperature Sum) model - standard phenological model
// Based on Aono & Kazui (2008) and subsequent research using 1200+ years of Kyoto data
/**
* Calculate bloom forecast using the DTS (Dormancy Temperature Sum) model
* @param {number} lat - latitude
* @param {number} alt - altitude in meters
* @param {Array} dailyTemps - array of {date, tMax, tMin, tMean}
*/
function calculateBloomForecast(lat, alt, dailyTemps = []) {
// Temperature base constants (Somei Yoshino calibrated)
const CHILLING_BASE = 7.2; // °C - chilling requirement base temp
const FORCING_BASE = 4.7; // °C - heat accumulation base temp
const REQUIRED_CHILLING = 1400; // chilling hours needed
const REQUIRED_FORCING = 400; // growing degree days needed
let chillingAcc = 0;
let forcingAcc = 0;
let chillingDone = false;
let firstBloomDate = null;
let fullBloomDate = null;
// Simulate from Oct 1 using provided or synthetic temps
const temps = dailyTemps.length > 0 ? dailyTemps : generateSyntheticTemps(lat, alt);
for (const day of temps) {
const tMean = day.tMean ?? (day.tMax + day.tMin) / 2;
if (!chillingDone) {
if (tMean < CHILLING_BASE) {
chillingAcc += (CHILLING_BASE - tMean) * 24; // hours equivalent
}
if (chillingAcc >= REQUIRED_CHILLING) chillingDone = true;
} else {
if (tMean > FORCING_BASE) {
forcingAcc += tMean - FORCING_BASE;
}
if (forcingAcc >= REQUIRED_FORCING && !firstBloomDate) {
firstBloomDate = day.date;
fullBloomDate = addDays(day.date, 7);
break;
}
}
}
const confidence = calculateConfidence(lat, alt, chillingAcc, forcingAcc);
return {
firstBloomDate,
fullBloomDate,
confidence,
chillingAccumulated: Math.round(chillingAcc),
forcingAccumulated: Math.round(forcingAcc),
model: 'DTS-Enhanced'
};
}
function generateSyntheticTemps(lat, alt) {
const temps = [];
const altCorrection = alt * 0.006; // lapse rate
const latCorrection = (lat - 35) * 0.5;
// Generate Oct 1 → Apr 30
const start = new Date('2025-10-01');
for (let i = 0; i < 212; i++) {
const date = addDays(start, i);
const dayOfYear = getDayOfYear(date);
// Simplified sinusoidal temperature model
const baseTemp = 15 - altCorrection - latCorrection;
const seasonal = -15 * Math.cos((dayOfYear / 365) * 2 * Math.PI);
const noise = (Math.random() - 0.5) * 4;
const tMean = baseTemp + seasonal + noise;
temps.push({ date, tMean, tMax: tMean + 5, tMin: tMean - 5 });
}
return temps;
}
function calculateConfidence(lat, alt, chillingAcc, forcingAcc) {
let conf = 0.75;
if (chillingAcc > 1200) conf += 0.05;
if (forcingAcc > 300) conf += 0.05;
if (lat > 40) conf -= 0.05; // Higher uncertainty at high latitudes
if (alt > 500) conf -= 0.05;
return Math.min(0.95, Math.max(0.5, conf));
}
function addDays(date, days) {
const d = new Date(date);
d.setDate(d.getDate() + days);
return d;
}
function getDayOfYear(date) {
const start = new Date(date.getFullYear(), 0, 0);
return Math.floor((date - start) / 86400000);
}
// ── Routes ───────────────────────────────────────────────────────────────────
// Get national forecast summary (like 桜前線 sakura front)
router.get('/front', (_req, res) => {
const now = new Date();
const currentMonth = now.getMonth() + 1;
// Simulate the sakura front moving northward
const front = {
year: now.getFullYear(),
updatedAt: now,
stages: [
{ region: 'Okinawa', status: 'ended', avgFirstBloom: '01-20', lat: 26.5 },
{ region: 'Kyushu', status: currentMonth >= 4 ? 'ended' : 'full_bloom', avgFirstBloom: '03-20', lat: 33.0 },
{ region: 'Shikoku', status: 'full_bloom', avgFirstBloom: '03-25', lat: 33.8 },
{ region: 'Kinki', status: 'blooming', avgFirstBloom: '03-27', lat: 34.7 },
{ region: 'Chubu', status: 'opening', avgFirstBloom: '04-05', lat: 35.5 },
{ region: 'Kanto', status: 'full_bloom', avgFirstBloom: '03-28', lat: 36.0 },
{ region: 'Tohoku', status: 'budding', avgFirstBloom: '04-15', lat: 38.5 },
{ region: 'Hokkaido', status: 'not_yet', avgFirstBloom: '05-01', lat: 43.0 }
]
};
res.json({ success: true, data: front });
});
// Get probability forecast for a spot
router.get('/spot/:id', (req, res) => {
const spotId = req.params.id;
const now = new Date();
// Generate 30-day probability timeline
const probabilities = [];
for (let i = -5; i < 25; i++) {
const d = addDays(now, i);
const gaussian = (x, mu, sigma) => Math.exp(-0.5 * Math.pow((x - mu) / sigma, 2));
const prob = gaussian(i, 3, 4) * 0.9;
probabilities.push({
date: d.toISOString().split('T')[0],
probability: Math.max(0, Math.min(1, prob)),
expectedBloomPercent: Math.max(0, Math.min(100, prob * 110))
});
}
res.json({
success: true,
data: {
spotId,
model: 'DTS-Enhanced v2.1',
generatedAt: now,
firstBloomDate: addDays(now, 2),
fullBloomDate: addDays(now, 9),
confidence: 0.84,
probabilities
}
});
});
// Run forecast calculation for coordinates
router.post('/calculate', (req, res) => {
const { lat, lng, altitude = 0, dailyTemps = [] } = req.body;
if (!lat || !lng) return res.status(400).json({ success: false, error: 'lat and lng required' });
const result = calculateBloomForecast(lat, altitude, dailyTemps);
res.json({ success: true, data: result });
});
module.exports = router;