File size: 14,970 Bytes
1bd792d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8fb1882
4ecd995
8fb1882
fd791ba
 
 
8fb1882
 
fd791ba
 
63aab61
1bd792d
fd791ba
 
8fb1882
fd791ba
 
 
 
 
 
 
 
 
8fb1882
fd791ba
de870a3
fd791ba
 
707ea7b
fd791ba
 
 
707ea7b
fd791ba
 
 
 
 
 
707ea7b
fd791ba
 
 
 
 
 
 
 
 
 
707ea7b
fd791ba
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
707ea7b
fd791ba
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
707ea7b
fd791ba
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
707ea7b
fd791ba
707ea7b
fd791ba
 
 
 
707ea7b
 
 
fd791ba
 
 
 
 
 
 
 
 
 
 
 
707ea7b
fd791ba
 
 
 
 
 
 
 
 
 
 
 
 
8fb1882
fd791ba
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8fb1882
fd791ba
 
 
 
 
 
 
 
53b06d1
fd791ba
 
 
 
 
 
 
 
 
8fb1882
707ea7b
fd791ba
 
 
 
 
8fb1882
fd791ba
8fb1882
fd791ba
8fb1882
707ea7b
fd791ba
 
 
 
 
8fb1882
fd791ba
8fb1882
fd791ba
8fb1882
fd791ba
 
 
 
 
 
8fb1882
fd791ba
8fb1882
fd791ba
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8fb1882
fd791ba
 
8fb1882
fd791ba
 
 
 
8fb1882
fd791ba
 
8fb1882
fd791ba
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8fb1882
 
fd791ba
 
8fb1882
 
fd791ba
 
 
 
 
 
 
 
 
4e60581
8fb1882
fd791ba
 
 
 
 
 
 
 
 
 
 
 
 
4e60581
fd791ba
 
4e60581
fd791ba
 
 
4e60581
fd791ba
 
 
 
8fb1882
 
707ea7b
 
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
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
import os
import subprocess
import sys

# Function to install system dependencies
def install_system_dependencies():
    try:
        # Install apt dependencies
        os.system('apt-get update && apt-get install -y libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 libcups2 libatspi2.0-0 libxcomposite1 libxdamage1')
        return True
    except Exception as e:
        print(f"Error installing system dependencies: {e}")
        return False

# Function to install Python packages
def install_python_packages():
    try:
        subprocess.check_call([sys.executable, "-m", "pip", "install", "playwright", "windrose"])
        subprocess.check_call([sys.executable, "-m", "playwright", "install", "chromium"])
        subprocess.check_call([sys.executable, "-m", "playwright", "install-deps"])
        return True
    except Exception as e:
        print(f"Error installing Python packages: {e}")
        return False

# Install all required dependencies
print("Installing system dependencies...")
if not install_system_dependencies():
    print("Warning: Failed to install system dependencies")

print("Installing Python packages...")
if not install_python_packages():
    print("Warning: Failed to install Python packages")

# Now import the required packages
import gradio as gr
import pandas as pd
import numpy as np
import re
from playwright.sync_api import sync_playwright
import time
import matplotlib.pyplot as plt
from matplotlib.gridspec import GridSpec
from windrose import WindroseAxes
from datetime import datetime


# Install Playwright browsers on startup
def install_playwright_browsers():
    try:
        if not os.path.exists('/home/user/.cache/ms-playwright'):
            print("Installing Playwright browsers...")
            subprocess.run(
                [sys.executable, "-m", "playwright", "install", "chromium"],
                check=True,
                capture_output=True,
                text=True
            )
            print("Playwright browsers installed successfully")
    except Exception as e:
        print(f"Error installing browsers: {e}")

# Install browsers when the module loads
install_playwright_browsers()

def scrape_weather_data(site_id, hours=720):
    """Scrape weather data from weather.gov timeseries"""
    url = f"https://www.weather.gov/wrh/timeseries?site={site_id}&hours={hours}&units=english&chart=on&headers=on&obs=tabular&hourly=false&pview=full&font=12&plot="
    
    try:
        with sync_playwright() as p:
            browser = p.chromium.launch(
                headless=True,
                args=['--no-sandbox', '--disable-dev-shm-usage']
            )
            
            context = browser.new_context(
                user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
            )
            
            page = context.new_page()
            response = page.goto(url)
            print(f"Response status: {response.status}")
            
            page.wait_for_selector('table', timeout=30000)
            time.sleep(5)
            
            print("Extracting data...")
            content = page.evaluate('''() => {
                const getTextContent = () => {
                    const rows = [];
                    const tables = document.getElementsByTagName('table');
                    for (const table of tables) {
                        if (table.textContent.includes('Date/Time')) {
                            const headerRow = Array.from(table.querySelectorAll('th'))
                                .map(th => th.textContent.trim());
                            
                            const dataRows = Array.from(table.querySelectorAll('tbody tr'))
                                .map(row => Array.from(row.querySelectorAll('td'))
                                    .map(td => td.textContent.trim()));
                            
                            return {headers: headerRow, rows: dataRows};
                        }
                    }
                    return null;
                };
                
                return getTextContent();
            }''')
            
            print(f"Found {len(content['rows'] if content else [])} rows of data")
            browser.close()
            return content
            
    except Exception as e:
        print(f"Error scraping data: {str(e)}")
        raise e

def parse_date(date_str):
    """Parse date string to datetime"""
    try:
        current_year = datetime.now().year
        return pd.to_datetime(f"{date_str}, {current_year}", format="%b %d, %I:%M %p, %Y")
    except:
        return pd.NaT

def parse_weather_data(data):
    """Parse the weather data into a pandas DataFrame"""
    if not data or 'rows' not in data:
        raise ValueError("No valid weather data found")
        
    df = pd.DataFrame(data['rows'])
    
    columns = ['datetime', 'temp', 'dew_point', 'humidity', 'wind_chill', 
              'wind_dir', 'wind_speed', 'snow_depth', 'snowfall_3hr', 
              'snowfall_6hr', 'snowfall_24hr', 'swe']
    
    df = df.iloc[:, :12]
    df.columns = columns
    
    numeric_cols = ['temp', 'dew_point', 'humidity', 'wind_chill', 'snow_depth',
                   'snowfall_3hr', 'snowfall_6hr', 'snowfall_24hr', 'swe']
    for col in numeric_cols:
        df[col] = pd.to_numeric(df[col], errors='coerce')
    
    def parse_wind(x):
        if pd.isna(x): return np.nan, np.nan
        match = re.search(r'(\d+)G(\d+)', str(x))
        if match:
            return float(match.group(1)), float(match.group(2))
        try:
            return float(x), np.nan
        except:
            return np.nan, np.nan
    
    wind_data = df['wind_speed'].apply(parse_wind)
    df['wind_speed'] = wind_data.apply(lambda x: x[0])
    df['wind_gust'] = wind_data.apply(lambda x: x[1])
    
    def parse_direction(direction):
        direction_map = {
            'N': 0, 'NNE': 22.5, 'NE': 45, 'ENE': 67.5,
            'E': 90, 'ESE': 112.5, 'SE': 135, 'SSE': 157.5,
            'S': 180, 'SSW': 202.5, 'SW': 225, 'WSW': 247.5,
            'W': 270, 'WNW': 292.5, 'NW': 315, 'NNW': 337.5
        }
        return direction_map.get(direction, np.nan)
    
    df['wind_dir_deg'] = df['wind_dir'].apply(parse_direction)
    
    df['datetime'] = df['datetime'].apply(parse_date)
    df['date'] = df['datetime'].dt.date
    
    return df

def process_daily_snow(group):
    """Sum up ONLY the 3-hour snowfall amounts for each day period"""
    # Sort by time to ensure proper sequence
    group = group.sort_values('datetime')
    
    # Initialize variables for tracking snow accumulation
    daily_total = 0
    last_valid_time = None
    last_amount = 0
    
    for _, row in group.iterrows():
        current_amount = row['snowfall_3hr'] if pd.notna(row['snowfall_3hr']) else 0
        
        # If this is a new reading (not overlapping with previous)
        if current_amount > 0:
            if last_valid_time is None or (row['datetime'] - last_valid_time).total_seconds() > 3600:
                daily_total += current_amount
                last_valid_time = row['datetime']
                last_amount = current_amount
            else:
                # For overlapping periods, only count the difference if it's higher
                if current_amount > last_amount:
                    daily_total += (current_amount - last_amount)
                    last_amount = current_amount
    
    return daily_total

def calculate_total_new_snow(df):
    """Calculate total new snow accumulation"""
    # Sort by datetime to ensure correct calculation
    df = df.sort_values('datetime')
    
    # Create a copy of the dataframe with ONLY datetime and 3-hour snowfall
    snow_df = df[['datetime', 'snowfall_3hr']].copy()
    
    # Create a day group that starts at 9 AM instead of midnight
    snow_df['day_group'] = snow_df['datetime'].apply(
        lambda x: x.date() if x.hour >= 9 else (x - pd.Timedelta(days=1)).date()
    )
    
    # Calculate daily snow totals
    daily_totals = snow_df.groupby('day_group').apply(process_daily_snow)
    
    return daily_totals.sum()

def create_wind_rose(df, ax):
    """Create a wind rose plot"""
    if not isinstance(ax, WindroseAxes):
        ax = WindroseAxes.from_ax(ax=ax)
    ax.bar(df['wind_dir_deg'].dropna(), df['wind_speed'].dropna(), 
           bins=np.arange(0, 40, 5), normed=True, opening=0.8, edgecolor='white')
    ax.set_legend(title='Wind Speed (mph)')
    ax.set_title('Wind Rose')

def create_plots(df):
    """Create all weather plots including SWE estimates"""
    # Create figure with adjusted height and spacing
    fig = plt.figure(figsize=(20, 24))
    
    # Calculate height ratios for different plots
    height_ratios = [1, 1, 1, 1, 1]  # Equal height for all plots
    gs = GridSpec(5, 1, figure=fig, height_ratios=height_ratios)
    gs.update(hspace=0.4)  # Increase vertical spacing between plots
    
    # Temperature plot
    ax1 = fig.add_subplot(gs[0])
    ax1.plot(df['datetime'], df['temp'], label='Temperature', color='red')
    ax1.plot(df['datetime'], df['wind_chill'], label='Wind Chill', color='blue')
    ax1.set_title('Temperature and Wind Chill Over Time', pad=20)
    ax1.set_xlabel('Date')
    ax1.set_ylabel('Temperature (°F)')
    ax1.legend()
    ax1.grid(True)
    ax1.tick_params(axis='x', rotation=45)
    
    # Wind speed plot
    ax2 = fig.add_subplot(gs[1])
    ax2.plot(df['datetime'], df['wind_speed'], label='Wind Speed', color='blue')
    ax2.plot(df['datetime'], df['wind_gust'], label='Wind Gust', color='orange')
    ax2.set_title('Wind Speed and Gusts Over Time', pad=20)
    ax2.set_xlabel('Date')
    ax2.set_ylabel('Wind Speed (mph)')
    ax2.legend()
    ax2.grid(True)
    ax2.tick_params(axis='x', rotation=45)
    
    # Snow depth plot
    ax3 = fig.add_subplot(gs[2])
    ax3.plot(df['datetime'], df['snow_depth'], color='blue', label='Snow Depth')
    ax3.set_title('Snow Depth Over Time', pad=20)
    ax3.set_xlabel('Date')
    ax3.set_ylabel('Snow Depth (inches)')
    ax3.grid(True)
    ax3.tick_params(axis='x', rotation=45)
    
    # Daily new snow bar plot
    ax4 = fig.add_subplot(gs[3])
    snow_df = df[['datetime', 'snowfall_3hr']].copy()
    snow_df['day_group'] = snow_df['datetime'].apply(
        lambda x: x.date() if x.hour >= 9 else (x - pd.Timedelta(days=1)).date()
    )
    daily_snow = snow_df.groupby('day_group').apply(process_daily_snow).reset_index()
    daily_snow.columns = ['date', 'new_snow']
    
    # Create the bar plot
    ax4.bar(daily_snow['date'], daily_snow['new_snow'], color='blue')
    ax4.set_title('Daily New Snow (Sum of 3-hour amounts, 9 AM Reset)', pad=20)
    ax4.set_xlabel('Date')
    ax4.set_ylabel('New Snow (inches)')
    ax4.tick_params(axis='x', rotation=45)
    ax4.grid(True, axis='y', linestyle='--', alpha=0.7)
    
    # Add value labels on top of each bar
    for i, v in enumerate(daily_snow['new_snow']):
        if v > 0:  # Only label bars with snow
            ax4.text(i, v, f'{v:.1f}"', ha='center', va='bottom')
    
    # SWE bar plot
    ax5 = fig.add_subplot(gs[4])
    daily_swe = df.groupby('date')['swe'].mean()
    ax5.bar(daily_swe.index, daily_swe.values, color='lightblue')
    ax5.set_title('Snow/Water Equivalent', pad=20)
    ax5.set_xlabel('Date')
    ax5.set_ylabel('SWE (inches)')
    ax5.tick_params(axis='x', rotation=45)
    
    # Adjust layout
    plt.subplots_adjust(top=0.95, bottom=0.05, left=0.1, right=0.95)
    
    # Create separate wind rose figure
    fig_rose = plt.figure(figsize=(10, 10))
    ax_rose = WindroseAxes.from_ax(fig=fig_rose)
    create_wind_rose(df, ax_rose)
    fig_rose.subplots_adjust(top=0.95, bottom=0.05, left=0.1, right=0.95)
    
    return fig, fig_rose

def analyze_weather_data(site_id, hours):
    """Analyze weather data and create visualizations"""
    try:
        print(f"Scraping data for {site_id}...")
        raw_data = scrape_weather_data(site_id, hours)
        if not raw_data:
            return "Error: Could not retrieve weather data.", None, None
        
        print("Parsing data...")    
        df = parse_weather_data(raw_data)
        
        # Calculate total new snow using the new method
        total_new_snow = calculate_total_new_snow(df)
        current_swe = df['swe'].iloc[0]  # Get most recent SWE measurement
        
        print("Calculating statistics...")
        stats = {
            'Temperature Range': f"{df['temp'].min():.1f}°F to {df['temp'].max():.1f}°F",
            'Average Temperature': f"{df['temp'].mean():.1f}°F",
            'Max Wind Speed': f"{df['wind_speed'].max():.1f} mph",
            'Max Wind Gust': f"{df['wind_gust'].max():.1f} mph",
            'Average Humidity': f"{df['humidity'].mean():.1f}%",
            'Current Snow Depth': f"{df['snow_depth'].iloc[0]:.1f} inches",
            'Total New Snow': f"{total_new_snow:.1f} inches",
            'Current Snow/Water Equivalent': f"{current_swe:.2f} inches"
        }
        
        html_output = "<div style='font-size: 16px; line-height: 1.5;'>"
        html_output += f"<p><strong>Weather Station:</strong> {site_id}</p>"
        html_output += f"<p><strong>Data Range:</strong> {df['datetime'].min().strftime('%Y-%m-%d %H:%M')} to {df['datetime'].max().strftime('%Y-%m-%d %H:%M')}</p>"
        for key, value in stats.items():
            html_output += f"<p><strong>{key}:</strong> {value}</p>"
        html_output += "</div>"
        
        print("Creating plots...")
        main_plots, wind_rose = create_plots(df)
        
        return html_output, main_plots, wind_rose
        
    except Exception as e:
        print(f"Error in analysis: {str(e)}")
        return f"Error analyzing data: {str(e)}", None, None

# Create Gradio interface
with gr.Blocks(title="Weather Station Data Analyzer") as demo:
    gr.Markdown("# Weather Station Data Analyzer")
    gr.Markdown("""
    Enter a weather station ID and number of hours to analyze.
    Example station IDs:
    - YCTIM (Yellowstone Club - Timber)
    - KBZN (Bozeman Airport)
    - KSLC (Salt Lake City)
    """)
    
    with gr.Row():
        site_id = gr.Textbox(
            label="Weather Station ID",
            value="YCTIM",
            placeholder="Enter station ID (e.g., YCTIM)"
        )
        hours = gr.Number(
            label="Hours of Data",
            value=720,
            minimum=1,
            maximum=1440
        )
    
    analyze_btn = gr.Button("Fetch and Analyze Weather Data")
    
    with gr.Row():
        stats_output = gr.HTML(label="Statistics")
    
    with gr.Row():
        weather_plots = gr.Plot(label="Weather Plots")
        wind_rose = gr.Plot(label="Wind Rose")
    
    analyze_btn.click(
        fn=analyze_weather_data,
        inputs=[site_id, hours],
        outputs=[stats_output, weather_plots, wind_rose]
    )

if __name__ == "__main__":
    demo.launch()