diff --git a/Portfolio.Application/DependencyInjection.cs b/Portfolio.Application/DependencyInjection.cs index 373fb00..a6d8751 100644 --- a/Portfolio.Application/DependencyInjection.cs +++ b/Portfolio.Application/DependencyInjection.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.DependencyInjection; using Portfolio.Application.Services.Articles; +using Portfolio.Application.Services.NWSWeatherService; using Portfolio.Application.Services.PokemonNatureService; using Portfolio.Application.Services.PokemonService; using Portfolio.Application.Services.PokemonSubskillService; @@ -19,6 +20,7 @@ namespace Portfolio.Application services.AddScoped(); services.AddScoped(); services.AddScoped(); + //services.AddScoped(); diff --git a/Portfolio.Application/Services/NWSWeatherService/INWSWeatherService.cs b/Portfolio.Application/Services/NWSWeatherService/INWSWeatherService.cs new file mode 100644 index 0000000..017ce0f --- /dev/null +++ b/Portfolio.Application/Services/NWSWeatherService/INWSWeatherService.cs @@ -0,0 +1,17 @@ +using Portfolio.Domain.Features.TemperatureDay; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Portfolio.Application.Services.NWSWeatherService +{ + public interface INWSWeatherService + { + Task GetNearestStationAsync(double latitude, double longitude); + Task GetDailyAverageTempAsync(string stationId, DateTime date); + Task> GetTemperatureDataAsync(double latitude, double longitude, int year); + + } +} diff --git a/Portfolio.Application/Services/NWSWeatherService/NWSWeatherService.cs b/Portfolio.Application/Services/NWSWeatherService/NWSWeatherService.cs new file mode 100644 index 0000000..990d3cc --- /dev/null +++ b/Portfolio.Application/Services/NWSWeatherService/NWSWeatherService.cs @@ -0,0 +1,123 @@ +using Portfolio.Domain.Features.TemperatureDay; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace Portfolio.Application.Services.NWSWeatherService +{ + public class NWSWeatherService : INWSWeatherService + { + private readonly HttpClient _httpClient; + + public NWSWeatherService(HttpClient httpClient) + { + _httpClient = httpClient; + _httpClient.DefaultRequestHeaders.Add("User-Agent", "kira.jiroux@gmail.com"); // Replace with your email ideally + } + + public async Task GetNearestStationAsync(double latitude, double longitude) + { + var url = $"https://api.weather.gov/points/{latitude},{longitude}"; + var response = await _httpClient.GetAsync(url); + response.EnsureSuccessStatusCode(); + + using var stream = await response.Content.ReadAsStreamAsync(); + var json = await JsonDocument.ParseAsync(stream); + + var stationUrl = json.RootElement + .GetProperty("properties") + .GetProperty("observationStations") + .GetString(); + + if (string.IsNullOrEmpty(stationUrl)) + throw new Exception("Could not find station info."); + + var stationListResponse = await _httpClient.GetAsync(stationUrl); + stationListResponse.EnsureSuccessStatusCode(); + + using var stationStream = await stationListResponse.Content.ReadAsStreamAsync(); + var stationJson = await JsonDocument.ParseAsync(stationStream); + + var firstStation = stationJson.RootElement + .GetProperty("features")[0] + .GetProperty("properties") + .GetProperty("stationIdentifier") + .GetString(); + + return firstStation; + } + + public async Task GetDailyAverageTempAsync(string stationId, DateTime date) + { + var start = date.ToString("yyyy-MM-dd") + "T00:00:00Z"; + var end = date.ToString("yyyy-MM-dd") + "T23:59:59Z"; + + var url = $"https://api.weather.gov/stations/{stationId}/observations?start={start}&end={end}"; + var response = await _httpClient.GetAsync(url); + response.EnsureSuccessStatusCode(); + + using var stream = await response.Content.ReadAsStreamAsync(); + var json = await JsonDocument.ParseAsync(stream); + + var temps = new List(); + + foreach (var feature in json.RootElement.GetProperty("features").EnumerateArray()) + { + if (feature.TryGetProperty("properties", out var props) && + props.TryGetProperty("temperature", out var tempObj) && + tempObj.TryGetProperty("value", out var valueProp) && + valueProp.ValueKind == JsonValueKind.Number) + { + var celsius = valueProp.GetDouble(); + if (!double.IsNaN(celsius)) + { + var fahrenheit = (celsius * 9.0 / 5.0) + 32.0; + temps.Add(fahrenheit); + } + } + } + + if (temps.Count == 0) + return null; + + return temps.Average(); + } + + public async Task> GetTemperatureDataAsync(double latitude, double longitude, int year) + { + var stationId = await GetNearestStationAsync(latitude, longitude); + + var result = new List(); + + var startDate = new DateTime(year, 1, 1); + var endDate = new DateTime(year, 12, 31); + + for (var date = startDate; date <= endDate; date = date.AddDays(1)) + { + Console.WriteLine($"Fetching {date:yyyy-MM-dd}..."); + var avgTemp = await GetDailyAverageTempAsync(stationId, date); + + if (avgTemp.HasValue) + { + result.Add(new TemperatureDay + { + Date = date, + AvgTemp = Math.Round(avgTemp.Value, 1) + }); + } + else + { + Console.WriteLine($"No data for {date:yyyy-MM-dd}"); + } + + await Task.Delay(600); // small delay to be nice to NWS + } + + return result; + } + } +} diff --git a/Portfolio.Domain/Features/TemperatureDay/TemperatureDay.cs b/Portfolio.Domain/Features/TemperatureDay/TemperatureDay.cs new file mode 100644 index 0000000..0dafdd2 --- /dev/null +++ b/Portfolio.Domain/Features/TemperatureDay/TemperatureDay.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Portfolio.Domain.Features.TemperatureDay +{ + public class TemperatureDay + { + public DateTime Date { get; set; } + public double AvgTemp { get; set; } + } +} diff --git a/Portfolio.Domain/Features/TemperatureRange/TemperatureRange.cs b/Portfolio.Domain/Features/TemperatureRange/TemperatureRange.cs new file mode 100644 index 0000000..d5b1309 --- /dev/null +++ b/Portfolio.Domain/Features/TemperatureRange/TemperatureRange.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Portfolio.Domain.Features.TemperatureRange +{ + public class TemperatureRange + { + public double Min { get; set; } + public double Max { get; set; } + public string Color { get; set; } = "#ffffff"; + } +} diff --git a/Portfolio.Infrastructure/DependencyInjection.cs b/Portfolio.Infrastructure/DependencyInjection.cs index 4bf1479..b8afdd7 100644 --- a/Portfolio.Infrastructure/DependencyInjection.cs +++ b/Portfolio.Infrastructure/DependencyInjection.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Portfolio.Application.Services.Articles; +using Portfolio.Application.Services.NWSWeatherService; using Portfolio.Application.Services.PokemonService; using Portfolio.Domain.Features.Pokemon; using Portfolio.Domain.Features.Pokemon_Natures; @@ -25,6 +26,7 @@ namespace Portfolio.Infrastructure services.AddScoped(); services.AddScoped(); services.AddScoped(); + //services.AddScoped(); return services; } diff --git a/Portfolio.WebUI.Server/Components/Component/Crochet Components/TemperatureBlanketVisualizer.razor b/Portfolio.WebUI.Server/Components/Component/Crochet Components/TemperatureBlanketVisualizer.razor new file mode 100644 index 0000000..16c07ee --- /dev/null +++ b/Portfolio.WebUI.Server/Components/Component/Crochet Components/TemperatureBlanketVisualizer.razor @@ -0,0 +1,29 @@ +@attribute [StreamRendering] +@rendermode InteractiveServer + +
+ @if (TemperatureDays is null || TemperatureRanges is null) + { + + } + else + { + +
+

Temperature Blanket Reviewer

+ +
+ @foreach (var day in TemperatureDays) + { + var color = GetColorForTemp(day.AvgTemp); +
+
+ } +
+ + +
+ } +
+ diff --git a/Portfolio.WebUI.Server/Components/Component/Crochet Components/TemperatureBlanketVisualizer.razor.cs b/Portfolio.WebUI.Server/Components/Component/Crochet Components/TemperatureBlanketVisualizer.razor.cs new file mode 100644 index 0000000..ef8bdeb --- /dev/null +++ b/Portfolio.WebUI.Server/Components/Component/Crochet Components/TemperatureBlanketVisualizer.razor.cs @@ -0,0 +1,46 @@ +using Microsoft.AspNetCore.Components; +using Portfolio.Domain.Features.TemperatureDay; +using Portfolio.Domain.Features.TemperatureRange; + +namespace Portfolio.WebUI.Server.Components.Component.Crochet_Components +{ + public partial class TemperatureBlanketVisualizer : ComponentBase + { + [Parameter] public List TemperatureDays { get; set; } + + public List TemperatureRanges { get; set; } = new(); + + protected override void OnInitialized() + { + TemperatureRanges = new() + { + new() { Min = 0, Max = 21, Color = "#ffffff" }, + new() { Min = 21, Max = 28, Color = "#ffc0cb" }, + new() { Min = 28, Max = 35, Color = "#dda0dd" }, + new() { Min = 35, Max = 42, Color = "#add8e6" }, + new() { Min = 42, Max = 49, Color = "#00008b" }, + new() { Min = 49, Max = 56, Color = "#006400" }, + new() { Min = 56, Max = 63, Color = "#90ee90" }, + new() { Min = 63, Max = 70, Color = "#ffff00" }, + new() { Min = 70, Max = 77, Color = "#ffa500" }, + new() { Min = 77, Max = 84, Color = "#ff0000" }, + new() { Min = 84, Max = 100, Color = "#000000" } + }; + } + + private string GetColorForTemp(double temp) + { + var range = TemperatureRanges.FirstOrDefault(r => temp >= r.Min && temp < r.Max); + return range?.Color ?? "#888888"; + } + + private void HandleRangesChanged(List updatedRanges) + { + TemperatureRanges = updatedRanges; + StateHasChanged(); + } + + + + } +} diff --git a/Portfolio.WebUI.Server/Components/Component/Crochet Components/TemperatureRangeEditor.razor b/Portfolio.WebUI.Server/Components/Component/Crochet Components/TemperatureRangeEditor.razor new file mode 100644 index 0000000..1151cb1 --- /dev/null +++ b/Portfolio.WebUI.Server/Components/Component/Crochet Components/TemperatureRangeEditor.razor @@ -0,0 +1,34 @@ +@using Microsoft.AspNetCore.Components.Web + + +@attribute [StreamRendering] +@rendermode InteractiveServer + +
+ +
+ + + @for (int i = 0; i < TempRanges.Count; i++) + { + var left = i == 0 ? 0 : TempRanges[i - 1].Max; + var leftPercent = (left / 100.0) * 100; + +
+
+ } + + +
+ @for (int i = 0; i < TempRanges.Count; i++) + { + var localIndex = i; + + } +
+
\ No newline at end of file diff --git a/Portfolio.WebUI.Server/Components/Component/Crochet Components/TemperatureRangeEditor.razor.cs b/Portfolio.WebUI.Server/Components/Component/Crochet Components/TemperatureRangeEditor.razor.cs new file mode 100644 index 0000000..7ac559e --- /dev/null +++ b/Portfolio.WebUI.Server/Components/Component/Crochet Components/TemperatureRangeEditor.razor.cs @@ -0,0 +1,88 @@ +using Microsoft.AspNetCore.Components.Web; +using Microsoft.AspNetCore.Components; +using Microsoft.JSInterop; +using Portfolio.Domain.Features.TemperatureRange; + +namespace Portfolio.WebUI.Server.Components.Component.Crochet_Components +{ + public partial class TemperatureRangeEditor : ComponentBase + { + [Parameter] public List TempRanges { get; set; } + + [Parameter] public EventCallback> OnRangesChanged { get; set; } + + [Inject] private IJSRuntime JS { get; set; } + + private double windowWidth = 1000; + private int? draggingIndex = null; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + windowWidth = await JS.InvokeAsync("eval", "window.innerWidth"); + StateHasChanged(); + } + } + + private void OnMouseDown(int index, MouseEventArgs e) + { + draggingIndex = index; + } + + private async void OnMouseUp() + { + if (draggingIndex != null) + { + draggingIndex = null; + await OnRangesChanged.InvokeAsync(TempRanges); + } + } + + private void OnMouseMove(MouseEventArgs e) + { + if (draggingIndex is not int index || index <= 0 || index >= TempRanges.Count) + return; + + var percent = (e.ClientX / windowWidth) * 100; + percent = Math.Clamp(percent, 0, 100); + + var min = TempRanges[index - 1].Min; + var max = TempRanges[index].Max; + + if (percent <= min + 1 || percent >= max - 1) + return; + + TempRanges[index - 1].Max = percent; + TempRanges[index].Min = percent; + } + + private void HandleColorChange(ChangeEventArgs e, int index) + { + if (index < 0 || index >= TempRanges.Count) + { + Console.WriteLine($"Color change requested for out-of-bounds index: {index}"); + return; + } + + var color = e?.Value?.ToString() ?? "#000000"; + OnColorChanged(index, color); + } + + private async void OnColorChanged(int index, string newColor) + { + //Console.WriteLine($"Color change at index {index} to {newColor}"); + + if (index >= 0 && index < TempRanges.Count) + { + TempRanges[index].Color = newColor; + Console.WriteLine($"Updated color: {TempRanges[index].Color}"); + await OnRangesChanged.InvokeAsync(TempRanges); + } + else + { + Console.WriteLine($"Invalid index: {index} (Count: {TempRanges.Count})"); + } + } + } +} diff --git a/Portfolio.WebUI.Server/Components/Layout/Sidebar.razor b/Portfolio.WebUI.Server/Components/Layout/Sidebar.razor index 60320d4..85bcdf5 100644 --- a/Portfolio.WebUI.Server/Components/Layout/Sidebar.razor +++ b/Portfolio.WebUI.Server/Components/Layout/Sidebar.razor @@ -26,6 +26,14 @@ Articles +
  • + + + + + Crochet + +
  • diff --git a/Portfolio.WebUI.Server/Components/Pages/Crochet Pages/CrochetHome.razor b/Portfolio.WebUI.Server/Components/Pages/Crochet Pages/CrochetHome.razor new file mode 100644 index 0000000..5c2216f --- /dev/null +++ b/Portfolio.WebUI.Server/Components/Pages/Crochet Pages/CrochetHome.razor @@ -0,0 +1,9 @@ +@page "/temperature-blanket" + +@attribute [StreamRendering] +@rendermode InteractiveServer + +

    Crochet

    + + + diff --git a/Portfolio.WebUI.Server/Components/Pages/Crochet Pages/CrochetHome.razor.cs b/Portfolio.WebUI.Server/Components/Pages/Crochet Pages/CrochetHome.razor.cs new file mode 100644 index 0000000..57a29a2 --- /dev/null +++ b/Portfolio.WebUI.Server/Components/Pages/Crochet Pages/CrochetHome.razor.cs @@ -0,0 +1,28 @@ +using Microsoft.AspNetCore.Components; +using Portfolio.Domain.Features.TemperatureDay; +using Portfolio.Domain.Features.TemperatureRange; +using Portfolio.WebUI.Server.Components.Component.Crochet_Components; +using static Portfolio.WebUI.Server.Components.Component.Crochet_Components.TemperatureRangeEditor; + +namespace Portfolio.WebUI.Server.Components.Pages.Crochet_Pages +{ + public partial class CrochetHome + { + public List temperatureDays { get; set; } + + protected override async Task OnInitializedAsync() + { + // Placeholder for loading temperature data + // Replace with actual API call + temperatureDays = Enumerable.Range(0, 365).Select(i => new TemperatureDay + { + Date = new DateTime(DateTime.Now.Year - 1, 1, 1).AddDays(i), + AvgTemp = Random.Shared.Next(10, 95) + }).ToList(); + + + + } + + } +} diff --git a/Portfolio.WebUI.Server/Components/Pages/Crochet Pages/TemperatureDataImporter.razor b/Portfolio.WebUI.Server/Components/Pages/Crochet Pages/TemperatureDataImporter.razor new file mode 100644 index 0000000..8a001d4 --- /dev/null +++ b/Portfolio.WebUI.Server/Components/Pages/Crochet Pages/TemperatureDataImporter.razor @@ -0,0 +1,24 @@ +@* @page "/import-temperature-data" + +@inject NWSWeatherService WeatherService +@inject IWebHostEnvironment Env + +

    Import Temperature Data

    + +@if (IsImporting) +{ +

    Importing temperatures... Please wait...

    +} +else +{ + +} + +@if (!string.IsNullOrEmpty(Message)) +{ +

    @Message

    +} + + + + *@ \ No newline at end of file diff --git a/Portfolio.WebUI.Server/Components/Pages/Crochet Pages/TemperatureDataImporter.razor.cs b/Portfolio.WebUI.Server/Components/Pages/Crochet Pages/TemperatureDataImporter.razor.cs new file mode 100644 index 0000000..e7b8af9 --- /dev/null +++ b/Portfolio.WebUI.Server/Components/Pages/Crochet Pages/TemperatureDataImporter.razor.cs @@ -0,0 +1,69 @@ +using Portfolio.Application.Services.NWSWeatherService; +using Portfolio.Domain.Features.TemperatureDay; +using System.Text.Json; + +namespace Portfolio.WebUI.Server.Components.Pages.Crochet_Pages +{ + public partial class TemperatureDataImporter + { + private readonly NWSWeatherService _weatherService; + private readonly string _storagePath = "Data/temperature_data_2024.json"; // changeable if needed + + public bool IsImporting = true; + + public TemperatureDataImporter(NWSWeatherService weatherService) + { + _weatherService = weatherService; + } + + public async Task> ImportAndSaveYearAsync(int year, double latitude, double longitude) + { + var days = new List(); + + var stationId = await _weatherService.GetNearestStationAsync(latitude, longitude); + if (stationId == null) + { + throw new Exception("Failed to find nearest station."); + } + + var startDate = new DateTime(year, 1, 1); + var endDate = new DateTime(year, 12, 31); + + for (var date = startDate; date <= endDate; date = date.AddDays(1)) + { + var avgTemp = await _weatherService.GetDailyAverageTempAsync(stationId, date); + if (avgTemp.HasValue) + { + days.Add(new TemperatureDay { Date = date, AvgTemp = avgTemp.Value }); + } + + await Task.Delay(600); // Respectful API use + } + + await SaveToFileAsync(days); + + return days; + } + + private async Task SaveToFileAsync(List days) + { + Directory.CreateDirectory(Path.GetDirectoryName(_storagePath)!); + + var json = JsonSerializer.Serialize(days, new JsonSerializerOptions { WriteIndented = true }); + await File.WriteAllTextAsync(_storagePath, json); + + Console.WriteLine($"✅ Saved {days.Count} days to {_storagePath}"); + } + + public async Task> LoadSavedDataAsync() + { + if (!File.Exists(_storagePath)) + throw new FileNotFoundException($"No saved temperature data found at {_storagePath}"); + + var json = await File.ReadAllTextAsync(_storagePath); + var days = JsonSerializer.Deserialize>(json); + + return days ?? new List(); + } + } +} \ No newline at end of file diff --git a/Portfolio.WebUI.Server/Components/_Imports.razor b/Portfolio.WebUI.Server/Components/_Imports.razor index 68b22b0..1b603f7 100644 --- a/Portfolio.WebUI.Server/Components/_Imports.razor +++ b/Portfolio.WebUI.Server/Components/_Imports.razor @@ -10,11 +10,14 @@ @using Portfolio.WebUI.Server.Components @using Portfolio.WebUI.Server.Components.Component @using Portfolio.WebUI.Server.Components.Component.Pokemon_Components +@using Portfolio.WebUI.Server.Components.Component.Crochet_Components @using Portfolio.Domain.Features.Articles @using Portfolio.Domain.Features.Pokemon @using Portfolio.Domain.Features.Pokemon_Natures @using Portfolio.Domain.Features.Pokemon_Subskills +@using Portfolio.Domain.Features.TemperatureDay @using Portfolio.Application.Services.Articles @using Portfolio.Application.Services.PokemonService @using Portfolio.Application.Services.PokemonNatureService -@using Portfolio.Application.Services.PokemonSubskillService \ No newline at end of file +@using Portfolio.Application.Services.PokemonSubskillService +@using Portfolio.Application.Services.NWSWeatherService \ No newline at end of file