Add image generation
This commit is contained in:
@@ -12,7 +12,18 @@ public class HomeController(IMediator mediator) : ControllerBase
|
||||
[HttpGet(Name = "GetHome")]
|
||||
public async Task<ActionResult<WeatherInformation>> Get()
|
||||
{
|
||||
var result = await mediator.Send(new GetWeather.Command());
|
||||
return Ok(result);
|
||||
return Ok(await mediator.Send(new Weather.Command()));
|
||||
}
|
||||
|
||||
[HttpGet("default.png")]
|
||||
public async Task<IActionResult> GetImage()
|
||||
{
|
||||
return File(await mediator.Send(new ImageGeneration.Command()), "image/png");
|
||||
}
|
||||
|
||||
[HttpGet("departureboard")]
|
||||
public async Task<ActionResult<List<TimeTable>>> GetDepartureBoard()
|
||||
{
|
||||
return Ok(await mediator.Send(new DepartureBoard.Command()));
|
||||
}
|
||||
}
|
||||
@@ -126,4 +126,22 @@ public static class ContractExtensions
|
||||
};
|
||||
}
|
||||
|
||||
public static List<TimeTable>? ToContract(this TrafikLabsApiResponse response)
|
||||
{
|
||||
if (response?.Departure is null)
|
||||
return [];
|
||||
|
||||
return response.Departure.Select(dep => new TimeTable
|
||||
{
|
||||
LineNumber = dep.ProductAtStop?.DisplayNumber ?? dep.ProductAtStop?.Line,
|
||||
LineName = dep.ProductAtStop?.Name,
|
||||
TransportType = dep.ProductAtStop?.CatOutL,
|
||||
Operator = dep.ProductAtStop?.Operator,
|
||||
StopName = dep.Stop,
|
||||
DepartureTime = $"{dep.Date} {dep.Time}",
|
||||
Direction = dep.Direction,
|
||||
JourneyDetailRef = dep.JourneyDetailRef?.Ref,
|
||||
Notes = dep.Notes?.Note?.Select(n => n.Value).ToList() ?? []
|
||||
}).ToList();
|
||||
}
|
||||
}
|
||||
25
HomeApi/Handlers/DepartureBoard.cs
Normal file
25
HomeApi/Handlers/DepartureBoard.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using HomeApi.Integration;
|
||||
using HomeApi.Models;
|
||||
using MediatR;
|
||||
|
||||
namespace HomeApi.Handlers;
|
||||
|
||||
public static class DepartureBoard
|
||||
{
|
||||
public record Command : IRequest<List<TimeTable>>;
|
||||
|
||||
public class Handler : IRequestHandler<Command, List<TimeTable>>
|
||||
{
|
||||
private readonly IDepartureBoardService _departureBoardService;
|
||||
|
||||
public Handler(IDepartureBoardService departureBoardService)
|
||||
{
|
||||
_departureBoardService = departureBoardService;
|
||||
}
|
||||
|
||||
public async Task<List<TimeTable>> Handle(Command request, CancellationToken cancellationToken)
|
||||
{
|
||||
return await _departureBoardService.GetDepartureBoard() ?? new List<TimeTable>();
|
||||
}
|
||||
}
|
||||
}
|
||||
75
HomeApi/Handlers/ImageGeneration.cs
Normal file
75
HomeApi/Handlers/ImageGeneration.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
using System.Reflection;
|
||||
using HomeApi.Models;
|
||||
using HomeApi.Models.Configuration;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using Microsoft.Extensions.Options;
|
||||
using PuppeteerSharp;
|
||||
using RazorLight;
|
||||
|
||||
namespace HomeApi.Handlers;
|
||||
|
||||
public static class ImageGeneration
|
||||
{
|
||||
public record Command : IRequest<Stream>;
|
||||
|
||||
public class Handler : IRequestHandler<Command, Stream>
|
||||
{
|
||||
private readonly ILogger<Handler> _logger;
|
||||
private readonly IWebHostEnvironment _env;
|
||||
private readonly IMediator _mediator;
|
||||
public Handler(
|
||||
IOptions<ApiConfiguration> apiConfiguration,
|
||||
ILogger<Handler> logger, IWebHostEnvironment env, IMediator mediator)
|
||||
{
|
||||
_logger = logger;
|
||||
_env = env;
|
||||
_mediator = mediator;
|
||||
}
|
||||
|
||||
public async Task<Stream> Handle(Command request, CancellationToken cancellationToken)
|
||||
{
|
||||
var weather = await _mediator.Send(new Weather.Command(), cancellationToken);
|
||||
var departureBoard = await _mediator.Send(new DepartureBoard.Command(), cancellationToken);
|
||||
|
||||
var model = new Image
|
||||
{
|
||||
Weather = weather,
|
||||
TimeTable = departureBoard
|
||||
};
|
||||
|
||||
if(weather is null)
|
||||
throw new Exception("Weather data not found");
|
||||
|
||||
var engine = new RazorLightEngineBuilder().SetOperatingAssembly(Assembly.GetExecutingAssembly())
|
||||
.UseEmbeddedResourcesProject(typeof(ImageGeneration)).UseMemoryCachingProvider().Build();
|
||||
var path = Path.Combine(_env.WebRootPath, "index.cshtml");
|
||||
|
||||
var template = await File.ReadAllTextAsync(path);
|
||||
|
||||
var result = await engine.CompileRenderStringAsync("templateKey", template, model);
|
||||
|
||||
return await CreateImage(result);
|
||||
}
|
||||
|
||||
private static async Task<Stream> CreateImage(string htmlContent)
|
||||
{
|
||||
var browserFetcher = new BrowserFetcher();
|
||||
await browserFetcher.DownloadAsync();
|
||||
var browser = await Puppeteer.LaunchAsync(new LaunchOptions
|
||||
{
|
||||
Headless = true,
|
||||
Args = ["--disable-gpu"]
|
||||
});
|
||||
var page = await browser.NewPageAsync();
|
||||
await page.SetViewportAsync(new ViewPortOptions
|
||||
{
|
||||
Width = 800,
|
||||
Height = 480
|
||||
});
|
||||
await page.SetContentAsync(htmlContent, new NavigationOptions { WaitUntil = new[] { WaitUntilNavigation.Networkidle0 } });
|
||||
var stream = await page.ScreenshotStreamAsync(new ScreenshotOptions { Type = ScreenshotType.Png });
|
||||
return stream;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ using Microsoft.Extensions.Options;
|
||||
|
||||
namespace HomeApi.Handlers;
|
||||
|
||||
public static class GetWeather
|
||||
public static class Weather
|
||||
{
|
||||
public record Command : IRequest<WeatherInformation>;
|
||||
|
||||
@@ -5,12 +5,15 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
<PreserveCompilationContext>true</PreserveCompilationContext>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MediatR" Version="13.0.0" />
|
||||
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="11.1.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.7"/>
|
||||
<PackageReference Include="PuppeteerSharp" Version="20.2.0" />
|
||||
<PackageReference Include="RazorLight" Version="2.3.1" />
|
||||
<PackageReference Include="Refit.HttpClientFactory" Version="8.0.0" />
|
||||
<PackageReference Include="Scalar.AspNetCore" Version="2.5.6" />
|
||||
</ItemGroup>
|
||||
@@ -20,5 +23,4 @@
|
||||
<Link>.dockerignore</Link>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
30
HomeApi/Integration/Client/ResRobotClient.cs
Normal file
30
HomeApi/Integration/Client/ResRobotClient.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using HomeApi.Models.Response;
|
||||
using Refit;
|
||||
|
||||
namespace HomeApi.Integration.Client;
|
||||
|
||||
public interface IResRobotClient
|
||||
{
|
||||
[Get("/v2.1/departureBoard")]
|
||||
Task<TrafikLabsApiResponse> GetDepartureBoardAsync(
|
||||
[AliasAs("accessId")] string accessId,
|
||||
[AliasAs("id")] string stopId,
|
||||
[AliasAs("direction")] string direction = null,
|
||||
[AliasAs("date")] string date = null, // Format: YYYY-MM-DD
|
||||
[AliasAs("time")] string time = null, // Format: HH:MM
|
||||
[AliasAs("duration")] int? duration = null,
|
||||
[AliasAs("maxJourneys")] int? maxJourneys = null,
|
||||
[AliasAs("operators")] string operators = null, // Example: "275,287"
|
||||
[AliasAs("products")] int? products = null,
|
||||
[AliasAs("passlist")] int? passlist = 0,
|
||||
[AliasAs("lang")] string language = "sv",
|
||||
[AliasAs("format")] string format = "json"
|
||||
);
|
||||
|
||||
[Get("/v2.1/location.name")]
|
||||
Task<LocationNameResponse> GetLocationsByNameAsync(
|
||||
[AliasAs("input")] string input,
|
||||
[AliasAs("format")] string format = "json",
|
||||
[AliasAs("accessId")] string accessId = "YOUR_API_KEY"
|
||||
);
|
||||
}
|
||||
46
HomeApi/Integration/DepartureBoardService.cs
Normal file
46
HomeApi/Integration/DepartureBoardService.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using HomeApi.Extensions;
|
||||
using HomeApi.Integration.Client;
|
||||
using HomeApi.Models;
|
||||
using HomeApi.Models.Configuration;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace HomeApi.Integration;
|
||||
|
||||
public interface IDepartureBoardService
|
||||
{
|
||||
Task<List<TimeTable>?> GetDepartureBoard();
|
||||
}
|
||||
|
||||
public class DepartureBoardService(IResRobotClient departureBoardApi, IOptions<ApiConfiguration> options) : IDepartureBoardService
|
||||
{
|
||||
private readonly ApiConfiguration _apiConfig = options.Value;
|
||||
|
||||
public async Task<List<TimeTable>?> GetDepartureBoard()
|
||||
{
|
||||
var locationResponse = await departureBoardApi.GetLocationsByNameAsync(
|
||||
input: _apiConfig.DefaultStation,
|
||||
format: "json",
|
||||
accessId: _apiConfig.Keys.ResRobot
|
||||
);
|
||||
|
||||
var id = locationResponse.StopLocationOrCoordLocation.FirstOrDefault()?.StopLocation?.ExtId;
|
||||
|
||||
if (id == null)
|
||||
return null;
|
||||
|
||||
var result = await departureBoardApi.GetDepartureBoardAsync(
|
||||
accessId: _apiConfig.Keys.ResRobot,
|
||||
stopId: id,
|
||||
direction: null,
|
||||
date: DateTime.Now.ToString("yyyy-MM-dd"),
|
||||
time: DateTime.Now.ToString("HH:mm"),
|
||||
duration: 60,
|
||||
maxJourneys: 10,
|
||||
passlist: 1,
|
||||
language: "sv",
|
||||
format: "json"
|
||||
);
|
||||
|
||||
return result.ToContract();
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ public class ApiConfiguration
|
||||
public Keys Keys { get; set; } = new();
|
||||
public BaseUrls BaseUrls { get; set; } = new();
|
||||
public string DefaultCity { get; set; } = "Vega stockholms lan";
|
||||
public string DefaultStation { get; set; } = "Vega station";
|
||||
}
|
||||
|
||||
public class BaseUrls
|
||||
@@ -12,6 +13,7 @@ public class BaseUrls
|
||||
public string Weather { get; set; } = string.Empty;
|
||||
public string Nominatim { get; set; } = string.Empty;
|
||||
public string Aurora { get; set; } = string.Empty;
|
||||
public string ResRobot { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class Keys
|
||||
@@ -19,4 +21,5 @@ public class Keys
|
||||
public string Weather { get; set; } = string.Empty;
|
||||
public string Nominatim { get; set; } = string.Empty;
|
||||
public string Aurora { get; set; } = string.Empty;
|
||||
public string ResRobot { get; set; } = string.Empty;
|
||||
}
|
||||
7
HomeApi/Models/ImageGeneration.cs
Normal file
7
HomeApi/Models/ImageGeneration.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace HomeApi.Models;
|
||||
|
||||
public class Image
|
||||
{
|
||||
public WeatherInformation Weather { get; set; }
|
||||
public List<TimeTable> TimeTable { get; set; }
|
||||
}
|
||||
88
HomeApi/Models/Response/LocationNameResponse.cs
Normal file
88
HomeApi/Models/Response/LocationNameResponse.cs
Normal file
@@ -0,0 +1,88 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
public class LocationNameResponse
|
||||
{
|
||||
[JsonPropertyName("stopLocationOrCoordLocation")]
|
||||
public List<StopLocationOrCoordLocation> StopLocationOrCoordLocation { get; set; }
|
||||
|
||||
[JsonPropertyName("TechnicalMessages")]
|
||||
public TechnicalMessages TechnicalMessages { get; set; }
|
||||
|
||||
[JsonPropertyName("serverVersion")]
|
||||
public string ServerVersion { get; set; }
|
||||
|
||||
[JsonPropertyName("dialectVersion")]
|
||||
public string DialectVersion { get; set; }
|
||||
|
||||
[JsonPropertyName("requestId")]
|
||||
public string RequestId { get; set; }
|
||||
}
|
||||
|
||||
public class StopLocationOrCoordLocation
|
||||
{
|
||||
[JsonPropertyName("StopLocation")]
|
||||
public StopLocation StopLocation { get; set; }
|
||||
}
|
||||
|
||||
public class StopLocation
|
||||
{
|
||||
[JsonPropertyName("productAtStop")]
|
||||
public List<ProductAtStop> ProductAtStop { get; set; }
|
||||
|
||||
[JsonPropertyName("timezoneOffset")]
|
||||
public int TimezoneOffset { get; set; }
|
||||
|
||||
[JsonPropertyName("id")]
|
||||
public string Id { get; set; }
|
||||
|
||||
[JsonPropertyName("extId")]
|
||||
public string ExtId { get; set; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
[JsonPropertyName("lon")]
|
||||
public double Lon { get; set; }
|
||||
|
||||
[JsonPropertyName("lat")]
|
||||
public double Lat { get; set; }
|
||||
|
||||
[JsonPropertyName("weight")]
|
||||
public int Weight { get; set; }
|
||||
|
||||
[JsonPropertyName("products")]
|
||||
public int Products { get; set; }
|
||||
|
||||
[JsonPropertyName("minimumChangeDuration")]
|
||||
public string MinimumChangeDuration { get; set; }
|
||||
}
|
||||
|
||||
public class ProductAtStop
|
||||
{
|
||||
[JsonPropertyName("icon")]
|
||||
public Icon Icon { get; set; }
|
||||
|
||||
[JsonPropertyName("cls")]
|
||||
public string Cls { get; set; }
|
||||
}
|
||||
|
||||
public class Icon
|
||||
{
|
||||
[JsonPropertyName("res")]
|
||||
public string Res { get; set; }
|
||||
}
|
||||
|
||||
public class TechnicalMessages
|
||||
{
|
||||
[JsonPropertyName("TechnicalMessage")]
|
||||
public List<TechnicalMessage> TechnicalMessage { get; set; }
|
||||
}
|
||||
|
||||
public class TechnicalMessage
|
||||
{
|
||||
[JsonPropertyName("value")]
|
||||
public string Value { get; set; }
|
||||
|
||||
[JsonPropertyName("key")]
|
||||
public string Key { get; set; }
|
||||
}
|
||||
84
HomeApi/Models/Response/TrafikLabsApiResponse.cs
Normal file
84
HomeApi/Models/Response/TrafikLabsApiResponse.cs
Normal file
@@ -0,0 +1,84 @@
|
||||
namespace HomeApi.Models.Response;
|
||||
public class TrafikLabsApiResponse
|
||||
{
|
||||
public List<Departure> Departure { get; set; }
|
||||
}
|
||||
|
||||
public class Departure
|
||||
{
|
||||
public JourneyDetailRef JourneyDetailRef { get; set; }
|
||||
public string JourneyStatus { get; set; }
|
||||
public ProductDetail ProductAtStop { get; set; }
|
||||
public List<ProductDetail> Product { get; set; }
|
||||
public Notes Notes { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Type { get; set; }
|
||||
public string Stop { get; set; }
|
||||
public string Stopid { get; set; }
|
||||
public string StopExtId { get; set; }
|
||||
public double Lon { get; set; }
|
||||
public double Lat { get; set; }
|
||||
public string Time { get; set; }
|
||||
public string Date { get; set; }
|
||||
public bool Reachable { get; set; }
|
||||
public string Direction { get; set; }
|
||||
public string DirectionFlag { get; set; }
|
||||
}
|
||||
|
||||
public class JourneyDetailRef
|
||||
{
|
||||
public string Ref { get; set; }
|
||||
}
|
||||
|
||||
public class ProductDetail
|
||||
{
|
||||
public Icon Icon { get; set; }
|
||||
public OperatorInfo OperatorInfo { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string InternalName { get; set; }
|
||||
public string DisplayNumber { get; set; }
|
||||
public string Num { get; set; }
|
||||
public string Line { get; set; }
|
||||
public string LineId { get; set; }
|
||||
public string CatOut { get; set; }
|
||||
public string CatIn { get; set; }
|
||||
public string CatCode { get; set; }
|
||||
public string Cls { get; set; }
|
||||
public string CatOutS { get; set; }
|
||||
public string CatOutL { get; set; }
|
||||
public string OperatorCode { get; set; }
|
||||
public string Operator { get; set; }
|
||||
public string Admin { get; set; }
|
||||
public string MatchId { get; set; }
|
||||
public int? RouteIdxFrom { get; set; }
|
||||
public int? RouteIdxTo { get; set; }
|
||||
}
|
||||
|
||||
public class Icon
|
||||
{
|
||||
public string Res { get; set; }
|
||||
}
|
||||
|
||||
public class OperatorInfo
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string NameS { get; set; }
|
||||
public string NameN { get; set; }
|
||||
public string NameL { get; set; }
|
||||
public string Id { get; set; }
|
||||
}
|
||||
|
||||
public class Notes
|
||||
{
|
||||
public List<Note> Note { get; set; }
|
||||
}
|
||||
|
||||
public class Note
|
||||
{
|
||||
public string Value { get; set; }
|
||||
public string Key { get; set; }
|
||||
public string Type { get; set; }
|
||||
public int RouteIdxFrom { get; set; }
|
||||
public int RouteIdxTo { get; set; }
|
||||
public string TxtN { get; set; }
|
||||
}
|
||||
14
HomeApi/Models/TimeTable.cs
Normal file
14
HomeApi/Models/TimeTable.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace HomeApi.Models;
|
||||
|
||||
public class TimeTable
|
||||
{
|
||||
public string LineNumber { get; set; } // e.g. "43", "832"
|
||||
public string LineName { get; set; } // e.g. "Länstrafik - Tåg 43"
|
||||
public string TransportType { get; set; } // e.g. "Tåg", "Buss"
|
||||
public string Operator { get; set; } // e.g. "SL"
|
||||
public string StopName { get; set; } // e.g. "Vega station (Haninge kn)"
|
||||
public string DepartureTime { get; set; } // e.g. 2025-07-15 01:03
|
||||
public string Direction { get; set; } // e.g. "Farsta Strand station"
|
||||
public string JourneyDetailRef { get; set; } // e.g. "1|39437|0|1|15072025"
|
||||
public List<string> Notes { get; set; } // e.g. "Pendeltåg", "Endast 2 klass"
|
||||
}
|
||||
@@ -29,6 +29,10 @@ public static class RegisterIntegration
|
||||
services.AddRefitClient<IWeatherClient>()
|
||||
.ConfigureBaseAddress(apiConfiguration => apiConfiguration.BaseUrls.Weather);
|
||||
|
||||
services.AddRefitClient<IResRobotClient>()
|
||||
.ConfigureBaseAddress(apiConfiguration => apiConfiguration.BaseUrls.ResRobot);
|
||||
|
||||
services.AddScoped<IDepartureBoardService, DepartureBoardService>();
|
||||
services.AddScoped<IGeocodingService, GeocodingService>();
|
||||
services.AddScoped<IAuroraService, AuroraService>();
|
||||
services.AddScoped<IWeatherService, WeatherService>();
|
||||
|
||||
@@ -7,14 +7,16 @@
|
||||
},
|
||||
"ApiConfiguration": {
|
||||
"Keys": {
|
||||
"Weather": "KEY",
|
||||
"SL": ""
|
||||
"Weather": "NOT COMMITED",
|
||||
"ResRobot": "NOT COMMITED"
|
||||
},
|
||||
"BaseUrls": {
|
||||
"Nominatim": "https://nominatim.openstreetmap.org",
|
||||
"Aurora": "http://api.auroras.live",
|
||||
"Weather": "https://api.weatherapi.com/v1"
|
||||
"Weather": "https://api.weatherapi.com/v1",
|
||||
"ResRobot": "https://api.resrobot.se"
|
||||
},
|
||||
"DefaultCity": "Vega stockholms lan"
|
||||
"DefaultCity": "Vega stockholms lan",
|
||||
"DefaultStation": "Vega Station"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,15 +7,17 @@
|
||||
},
|
||||
"ApiConfiguration": {
|
||||
"Keys": {
|
||||
"Weather": "KEY",
|
||||
"SL": ""
|
||||
"Weather": "NOT COMMITED",
|
||||
"ResRobot": "NOT COMMITED"
|
||||
},
|
||||
"BaseUrls": {
|
||||
"Nominatim": "https://nominatim.openstreetmap.org",
|
||||
"Aurora": "http://api.auroras.live",
|
||||
"Weather": "https://api.weatherapi.com/v1"
|
||||
"Weather": "https://api.weatherapi.com/v1",
|
||||
"ResRobot": "https://api.resrobot.se"
|
||||
},
|
||||
"DefaultCity": "Vega stockholms lan"
|
||||
"DefaultCity": "Vega stockholms lan",
|
||||
"DefaultStation": "Vega Station"
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
|
||||
212
HomeApi/wwwroot/index.cshtml
Normal file
212
HomeApi/wwwroot/index.cshtml
Normal file
@@ -0,0 +1,212 @@
|
||||
@model HomeApi.Models.Image
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Weather Dashboard</title>
|
||||
<style>
|
||||
html, body {
|
||||
width: 800px;
|
||||
height: 480px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: #f0f4f8;
|
||||
color: #333;
|
||||
font-family: Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
font-size: 13px;
|
||||
}
|
||||
.section {
|
||||
margin-bottom: 6px;
|
||||
padding: 4px;
|
||||
}
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 6px;
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.08);
|
||||
flex: 1;
|
||||
font-size: 12px;
|
||||
}
|
||||
.scroll {
|
||||
max-height: 140px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 12px;
|
||||
}
|
||||
th, td {
|
||||
padding: 2px 4px;
|
||||
border: 1px solid #e0e0e0;
|
||||
text-align: left;
|
||||
}
|
||||
th {
|
||||
background: #e8eef3;
|
||||
font-weight: bold;
|
||||
}
|
||||
img.icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="section">
|
||||
<h2 style="font-size:16px;">@Model.Weather.CityName</h2>
|
||||
<div class="flex-row">
|
||||
Current Weather
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Temp</th>
|
||||
<th>Feels</th>
|
||||
<th>Clouds</th>
|
||||
<th>Wind</th>
|
||||
<th>Gusts</th>
|
||||
<th>Updated</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>@Model.Weather.Current.Temperature °C</td>
|
||||
<td>@Model.Weather.Current.Feelslike °C</td>
|
||||
<td>@Model.Weather.Current.Cloud%</td>
|
||||
<td>@Model.Weather.Current.WindPerMeterSecond m/s (@Model.Weather.Current.WindDirection)</td>
|
||||
<td>@Model.Weather.Current.WindGustPerMeterSecond m/s</td>
|
||||
<td>@Model.Weather.Current.LastUpdated</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<br/>
|
||||
Current Air Quality
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>CO</th>
|
||||
<th>NO₂</th>
|
||||
<th>O₃</th>
|
||||
<th>SO₂</th>
|
||||
<th>PM2.5</th>
|
||||
<th>PM10</th>
|
||||
<th>EPA</th>
|
||||
<th>DEFRA</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>@Model.Weather.Current.AirQuality?.Co</td>
|
||||
<td>@Model.Weather.Current.AirQuality?.No2</td>
|
||||
<td>@Model.Weather.Current.AirQuality?.O3</td>
|
||||
<td>@Model.Weather.Current.AirQuality?.So2</td>
|
||||
<td>@Model.Weather.Current.AirQuality?.Pm2_5</td>
|
||||
<td>@Model.Weather.Current.AirQuality?.Pm10</td>
|
||||
<td>@Model.Weather.Current.AirQuality?.Us_Epa_Index</td>
|
||||
<td>@Model.Weather.Current.AirQuality?.Gb_Defra_Index</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<br/>
|
||||
Aurora Probability
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Probability</th>
|
||||
<th>Color</th>
|
||||
<th>Highest</th>
|
||||
<th>Location</th>
|
||||
<th>Date</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>@Model.Weather.Current.AuroraProbability?.Value%</td>
|
||||
<td>@Model.Weather.Current.AuroraProbability?.Colour</td>
|
||||
<td>@Model.Weather.Current.AuroraProbability?.HighestProbability?.Value%</td>
|
||||
<td>(@Model.Weather.Current.AuroraProbability?.HighestProbability?.Lat, @Model.Weather.Current.AuroraProbability?.HighestProbability?.Long)</td>
|
||||
<td>@Model.Weather.Current.AuroraProbability?.HighestProbability?.Date.ToShortDateString()</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section scroll">
|
||||
<h3 style="font-size:14px;">Forecast</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Icon</th>
|
||||
<th>Min</th>
|
||||
<th>Max</th>
|
||||
<th>Day</th>
|
||||
<th>Feels</th>
|
||||
<th>Rain</th>
|
||||
<th>Snow</th>
|
||||
<th>Sunrise</th>
|
||||
<th>Sunset</th>
|
||||
<th>Moonrise</th>
|
||||
<th>Moonset</th>
|
||||
<th>Moon</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var f in Model.Weather.Forecast)
|
||||
{
|
||||
<tr>
|
||||
<td>@f.Date</td>
|
||||
<td><img class="icon" src="@f.DayIcon" alt="Icon" /></td>
|
||||
<td>@f.MinTempC°C</td>
|
||||
<td>@f.MaxTempC°C</td>
|
||||
<td>@f.Day?.ConditionText</td>
|
||||
<td>@f.Day?.AvgFeelslikeC°C</td>
|
||||
<td>@f.Day?.TotalChanceOfRain%</td>
|
||||
<td>@f.Day?.TotalChanceOfSnow%</td>
|
||||
<td>@f.Astro.Sunrise</td>
|
||||
<td>@f.Astro.Sunset</td>
|
||||
<td>@f.Astro.Moonrise</td>
|
||||
<td>@f.Astro.Moonset</td>
|
||||
<td>@f.Astro.Moon_Phase (@f.Astro.Moon_Illumination%)</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="section scroll">
|
||||
<h3 style="font-size:14px;">Public Transport Departures</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Type</th>
|
||||
<th>Line</th>
|
||||
<th>Name</th>
|
||||
<th>Operator</th>
|
||||
<th>Stop</th>
|
||||
<th>Departure</th>
|
||||
<th>Direction</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var t in Model.TimeTable)
|
||||
{
|
||||
<tr>
|
||||
<td>@t.TransportType</td>
|
||||
<td>@t.LineNumber</td>
|
||||
<td>@t.LineName</td>
|
||||
<td>@t.Operator</td>
|
||||
<td>@t.StopName</td>
|
||||
<td>@t.DepartureTime</td>
|
||||
<td>@t.Direction</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user