diff --git a/lib/www/client/source/src/views/Plan.vue b/lib/www/client/source/src/views/Plan.vue
index eefa837..60e2e9b 100644
--- a/lib/www/client/source/src/views/Plan.vue
+++ b/lib/www/client/source/src/views/Plan.vue
@@ -119,7 +119,11 @@
>
- {{srssIcon(item)}}
+
+ {{srssIcon(item)}}
+ /
+ {{wxIcon(item)}}
+
@@ -422,6 +426,123 @@ export default {
plannerConfig: null,
shiftAll: false, // Shift all sequences checkbox
+ // Weather API
+ wxData: null,
+ weathercode: {
+ 0: {
+ description: "Clear sky",
+ icon: "mdi-weather-sunny"
+ },
+ 1: {
+ description: "Mainly clear",
+ icon: "mdi-weather-sunny"
+ },
+ 2: {
+ description: "Partly cloudy",
+ icon: "mdi-weather-partly-cloudy"
+ },
+ 3: {
+ description: "Overcast",
+ icon: "mdi-weather-cloudy"
+ },
+ 45: {
+ description: "Fog",
+ icon: "mde-weather-fog"
+ },
+ 48: {
+ description: "Depositing rime fog",
+ icon: "mdi-weather-fog"
+ },
+ 51: {
+ description: "Light drizzle",
+ icon: "mdi-weather-partly-rainy"
+ },
+ 53: {
+ description: "Moderate drizzle",
+ icon: "mdi-weather-partly-rainy"
+ },
+ 55: {
+ description: "Dense drizzle",
+ icon: "mdi-weather-rainy"
+ },
+ 56: {
+ description: "Light freezing drizzle",
+ icon: "mdi-weather-partly-snowy-rainy"
+ },
+ 57: {
+ description: "Freezing drizzle",
+ icon: "mdi-weather-partly-snowy-rainy"
+ },
+ 61: {
+ description: "Light rain",
+ icon: "mdi-weather-rainy"
+ },
+ 63: {
+ description: "Moderate rain",
+ icon: "mdi-weather-rainy"
+ },
+ 65: {
+ description: "Heavy rain",
+ icon: "mdi-weather-pouring"
+ },
+ 66: {
+ description: "Light freezing rain",
+ icon: "mdi-loading"
+ },
+ 67: {
+ description: "Freezing rain",
+ icon: "mdi-loading"
+ },
+ 71: {
+ description: "Light snow",
+ icon: "mdi-loading"
+ },
+ 73: {
+ description: "Moderate snow",
+ icon: "mdi-loading"
+ },
+ 75: {
+ description: "Heavy snow",
+ icon: "mdi-loading"
+ },
+ 77: {
+ description: "Snow grains",
+ icon: "mdi-loading"
+ },
+ 80: {
+ description: "Light rain showers",
+ icon: "mdi-loading"
+ },
+ 81: {
+ description: "Moderate rain showers",
+ icon: "mdi-loading"
+ },
+ 82: {
+ description: "Violent rain showers",
+ icon: "mdi-loading"
+ },
+ 85: {
+ description: "Light snow showers",
+ icon: "mdi-loading"
+ },
+ 86: {
+ description: "Snow showers",
+ icon: "mdi-loading"
+ },
+ 95: {
+ description: "Thunderstorm",
+ icon: "mdi-loading"
+ },
+ 96: {
+ description: "Hailstorm",
+ icon: "mdi-loading"
+ },
+ 99: {
+ description: "Heavy hailstorm",
+ icon: "mdi-loading"
+ },
+ },
+
// Context menu stuff
contextMenuShow: false,
contextMenuX: 0,
@@ -630,6 +751,113 @@ export default {
return text.join("\n");
},
+ wxInfo (line) {
+
+ function atm(key) {
+ return line.meta?.wx?.atmospheric?.hourly[key];
+ }
+
+ function mar(key) {
+ return line.meta?.wx?.marine?.hourly[key];
+ }
+
+ const code = atm("weathercode");
+
+ const description = this.weathercode[code]?.description ?? `WMO code ${code}`;
+ const wind_speed = Math.round(atm("windspeed_10m"));
+ const wind_direction = String(Math.round(atm("winddirection_10m"))).padStart(3, "0");
+ const pressure = Math.round(atm("surface_pressure"));
+ const temperature = Math.round(atm("temperature_2m"));
+ const humidity = atm("relativehumidity_2m");
+ const precipitation = atm("precipitation");
+ const precipitation_probability = atm("precipitation_probability");
+ const precipitation_str = precipitation_probability
+ ? `\nPrecipitation ${precipitation} mm (prob. ${precipitation_probability}%)`
+ : ""
+
+ const wave_height = mar("wave_height").toFixed(1);
+ const wave_direction = mar("wave_direction");
+ const wave_period = mar("wave_period");
+
+ return `${description}\n${temperature}° C\n${pressure} hPa\nWind ${wind_speed} kt ${wind_direction}°\nRelative humidity ${humidity}%${precipitation_str}\nWaves ${wave_height} m ${wave_direction}° @ ${wave_period} s`;
+ },
+
+ wxIcon (line) {
+ const code = line.meta?.wx?.atmospheric?.hourly?.weathercode;
+
+ return this.weathercode[code]?.icon ?? "mdi-help";
+
+ },
+
+ async wxQuery (line) {
+ function midpoint(line) {
+ // WARNING Fails if across the antimeridian
+ const longitude = (line.geometry.coordinates[0][0] + line.geometry.coordinates[1][0])/2;
+ const latitude = (line.geometry.coordinates[0][1] + line.geometry.coordinates[1][1])/2;
+ return [ longitude, latitude ];
+ }
+
+ function extract (fcst) {
+ const τ = (line.ts0.valueOf() + line.ts1.valueOf()) / 2000;
+ const [idx, ε] = fcst?.hourly?.time?.reduce( (acc, cur, idx) => {
+ const δ = Math.abs(cur - τ);
+ const retval = acc
+ ? acc[1] < δ
+ ? acc
+ : [ idx, δ ]
+ : [ idx, δ ];
+
+ return retval;
+ });
+
+ if (idx) {
+ const hourly = {};
+ for (let key in fcst?.hourly) {
+ fcst.hourly[key] = fcst.hourly[key][idx];
+ }
+ }
+
+ return fcst;
+ }
+
+ async function fetch_atmospheric (opts) {
+ const { longitude, latitude, dt0, dt1 } = opts;
+
+ const url = `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&hourly=temperature_2m,relativehumidity_2m,precipitation_probability,precipitation,weathercode,pressure_msl,surface_pressure,windspeed_10m,winddirection_10m&daily=uv_index_max&windspeed_unit=kn&timeformat=unixtime&timezone=GMT&start_date=${dt0}&end_date=${dt1}&format=json`;
+ const init = {};
+ const res = await fetch (url, init);
+ if (res?.ok) {
+ const data = await res.json();
+
+ return extract(data);
+ }
+ }
+
+ async function fetch_marine (opts) {
+ const { longitude, latitude, dt0, dt1 } = opts;
+ const url = `https://marine-api.open-meteo.com/v1/marine?latitude=${latitude}&longitude=${longitude}&hourly=wave_height,wave_direction,wave_period&timeformat=unixtime&timezone=GMT&start_date=${dt0}&end_date=${dt1}&format=json`;
+
+ const init = {};
+ const res = await fetch (url, init);
+ if (res?.ok) {
+ const data = await res.json();
+
+ return extract(data);
+ }
+ }
+
+ if (line) {
+ const [ longitude, latitude ] = midpoint(line);
+ const dt0 = line.ts0.toISOString().substr(0, 10);
+ const dt1 = line.ts1.toISOString().substr(0, 10);
+
+ return {
+ atmospheric: await fetch_atmospheric({longitude, latitude, dt0, dt1}),
+ marine: await fetch_marine({longitude, latitude, dt0, dt1})
+ };
+ }
+ },
+
lagAfter (item) {
const pos = this.items.indexOf(item)+1;
if (pos != 0) {
@@ -723,6 +951,9 @@ export default {
for (const item of this.items) {
item.ts0 = new Date(item.ts0);
item.ts1 = new Date(item.ts1);
+ this.wxQuery(item).then( (wx) => {
+ item.meta = {...item.meta, wx};
+ });
}
},