Maps
Weather Forecast
View Source
Real-time weather app with current conditions, 10-day forecast, hourly temperature charts, and interactive map showing nearby cities.
📍Ellicott City, MD▼
⌖My Location
Current weather5:45 PM
OvercastFeels like 41°
The skies will be overcast. The low will be 42°.
Air quality🟢 25
Wind💨 4 mph S
Humidity💧 53%
Visibility👁 72.4 mi
Pressure📊 29.52 in
Dew point🌡 30°
🌧
🌡
💨
Hourly
Overview
Precipitation
Wind
Humidity
Cloud cover
Visibility
Feels like
11Yesterday
44°30°
14Today
55°41°
15Thu
42°18°
16Fri
36°13°
17Sat
49°33°
18Sun
36°23°
19Mon
31°20°
20Tue
25°14°
21Wed
21°12°
22Thu
32°21°
23Fri
37°24°
OverviewToday, Jan 14
4 PM
51°
Now
48°
6 PM
46°
7 PM
45°
8 PM
44°
9 PM
44°
10 PM
43°
11 PM
42°
Temperature
🌅7:25 AM
🌇5:07 PM
🌙Waning Crescent
Select Location
✕
🔍
Press Enter to searchPopular Cities
📍Ellicott City, MD✓
📍New York, NY
📍Los Angeles, CA
📍Chicago, IL
📍Houston, TX
📍Phoenix, AZ
📍Philadelphia, PA
📍San Antonio, TX
📍San Diego, CA
📍Dallas, TX
📍Miami, FL
📍Denver, CO
📍Seattle, WA
📍Boston, MA
📍Atlanta, GA
📍San Francisco, CA
📍Washington, DC
📍Las Vegas, NV
📍Portland, OR
📍Honolulu, HI
📍Anchorage, AK
Explore More Demos
Oorian Core
Task Manager
Interactive task manager demonstrating pure Oorian features: forms, tables, e...
Oorian Core
Browser APIs
Access browser features from Java: Geolocation, Clipboard, Storage, Notificat...
Charts
Sales Dashboard
Line and bar charts showing monthly sales data with smooth animations and too...
WeatherForecastDemo.java
✕
/***********************************************************************************
* Copyright (c) 2025
* Corvus Engineering, LLC
* All Rights Reserved
***********************************************************************************/
package com.oorian.website.demos;
import com.oorian.apexcharts.*;
import com.oorian.css.*;
import com.oorian.html.elements.*;
import com.oorian.html.js.geolocation.GeolocationApi;
import com.oorian.html.js.geolocation.GeolocationErrorEvent;
import com.oorian.html.js.geolocation.GeolocationEvent;
import com.oorian.html.js.geolocation.GeolocationListener;
import com.oorian.html.js.geolocation.GeolocationOptions;
import com.oorian.leaflet.*;
import com.oorian.messaging.events.client.InputChangeEvent;
import com.oorian.messaging.events.client.InputCompleteEvent;
import com.oorian.messaging.events.client.InputListener;
import com.oorian.messaging.events.client.MouseClickListener;
import com.oorian.messaging.events.client.MouseClickedEvent;
import com.oorian.website.weather.*;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/****************************************************************************************************************
* Weather Forecast demo component.
* Showcases a professional weather application UI with real data from Open-Meteo API.
* Features current conditions, 10-day forecast, hourly forecast with temperature chart,
* and an interactive map using Leaflet.
****************************************************************************************************************/
public class WeatherForecastDemo extends Div implements MouseClickListener, InputListener, GeolocationListener
{
private static final String PRIMARY_BG = "#1a2744";
private static final String SECONDARY_BG = "#243656";
private static final String CARD_BG = "#ffffff";
private static final String TEXT_PRIMARY = "#ffffff";
private static final String TEXT_SECONDARY = "#94a3b8";
private static final String ACCENT_COLOR = "#3b82f6";
private static final double DEFAULT_LAT = 39.2673;
private static final double DEFAULT_LNG = -76.7983;
private static final String DEFAULT_LOCATION = "Ellicott City, MD";
// Preset locations for the location picker
private static final String[][] LOCATIONS = {
{"Ellicott City, MD", "39.2673", "-76.7983"},
{"New York, NY", "40.7128", "-74.0060"},
{"Los Angeles, CA", "34.0522", "-118.2437"},
{"Chicago, IL", "41.8781", "-87.6298"},
{"Houston, TX", "29.7604", "-95.3698"},
{"Phoenix, AZ", "33.4484", "-112.0740"},
{"Philadelphia, PA", "39.9526", "-75.1652"},
{"San Antonio, TX", "29.4241", "-98.4936"},
{"San Diego, CA", "32.7157", "-117.1611"},
{"Dallas, TX", "32.7767", "-96.7970"},
{"Miami, FL", "25.7617", "-80.1918"},
{"Denver, CO", "39.7392", "-104.9903"},
{"Seattle, WA", "47.6062", "-122.3321"},
{"Boston, MA", "42.3601", "-71.0589"},
{"Atlanta, GA", "33.7490", "-84.3880"},
{"San Francisco, CA", "37.7749", "-122.4194"},
{"Washington, DC", "38.9072", "-77.0369"},
{"Las Vegas, NV", "36.1699", "-115.1398"},
{"Portland, OR", "45.5152", "-122.6784"},
{"Honolulu, HI", "21.3069", "-157.8583"},
{"Anchorage, AK", "61.2181", "-149.9003"}
};
private WeatherData weatherData;
private String activeTab = "Overview";
private int selectedDayIndex = 0;
// Dynamic UI elements
private final Map<String, Div> tabButtons = new HashMap<>();
private final Map<Integer, Div> dayCards = new HashMap<>();
private final Map<String, Div> locationItems = new HashMap<>();
private Div locationSelector;
private Span locationLabel;
private Div myLocationBtn;
private Div locationModal;
private Div modalOverlay;
private Div modalCloseBtn;
private TextInput searchInput;
private Div searchResultsContainer;
private Div presetLocationsContainer;
private final Map<Div, OpenMeteoClient.GeoLocation> searchResultItems = new HashMap<>();
private Span hourlyTitle;
private Span dateLabel;
private Div hourlyRow;
private Div chartContainer;
private AxChart hourlyChart;
private Div legendDot;
private Span legendLabel;
private Div mainContainer;
private LfMap weatherMap;
public WeatherForecastDemo()
{
loadWeatherData();
createWeatherApp();
}
private void loadWeatherData()
{
try
{
OpenMeteoClient client = new OpenMeteoClient();
weatherData = client.getWeather(DEFAULT_LAT, DEFAULT_LNG, DEFAULT_LOCATION);
}
catch (Exception e)
{
// Log the error and create fallback data if API fails
System.err.println("Weather API failed: " + e.getMessage());
e.printStackTrace();
weatherData = createFallbackData();
}
}
private WeatherData createFallbackData()
{
// Return realistic mock data for demonstration
WeatherData data = new WeatherData();
data.setLocationName(DEFAULT_LOCATION);
data.setLatitude(DEFAULT_LAT);
data.setLongitude(DEFAULT_LNG);
data.setTimezone("America/New_York");
// Create current weather
WeatherData.CurrentWeather current = new WeatherData.CurrentWeather();
current.setTime(LocalDateTime.now());
current.setTemperature(72);
current.setApparentTemperature(74);
current.setHumidity(45);
current.setWindSpeed(8);
current.setWindDirection(180);
current.setPressure(1015);
current.setWeatherCode(1); // Mainly clear
current.setCloudCover(20);
current.setVisibility(10);
current.setUvIndex(5);
data.setCurrent(current);
// Create hourly forecast (next 24 hours)
List<WeatherData.HourlyForecast> hourly = new ArrayList<>();
LocalDateTime hourStart = LocalDateTime.now().withMinute(0).withSecond(0);
double[] hourlyTemps = {70, 71, 72, 73, 74, 75, 76, 77, 76, 75, 74, 72, 70, 68, 66, 65, 64, 63, 64, 66, 68, 70, 71, 72};
int[] hourlyCodes = {1, 1, 0, 0, 0, 1, 1, 2, 2, 1, 1, 0, 0, 0, 1, 1, 2, 2, 1, 0, 0, 0, 1, 1};
for (int i = 0; i < 24; i++)
{
WeatherData.HourlyForecast h = new WeatherData.HourlyForecast();
h.setTime(hourStart.plusHours(i));
h.setTemperature(hourlyTemps[i]);
h.setApparentTemperature(hourlyTemps[i] + 2);
h.setWeatherCode(hourlyCodes[i]);
h.setPrecipitationProbability(i < 12 ? 5 : 10);
h.setHumidity(45 + (i % 10));
h.setWindSpeed(5 + (i % 5));
h.setCloudCover(20 + (i % 30));
h.setVisibility(10);
hourly.add(h);
}
data.setHourly(hourly);
// Create daily forecast (10 days)
List<WeatherData.DailyForecast> daily = new ArrayList<>();
LocalDate today = LocalDate.now();
double[] dailyHighs = {75, 78, 80, 76, 72, 74, 77, 79, 81, 78};
double[] dailyLows = {58, 60, 62, 58, 55, 56, 59, 61, 63, 60};
int[] dailyCodes = {1, 0, 0, 2, 3, 1, 0, 0, 1, 2};
for (int i = 0; i < 10; i++)
{
WeatherData.DailyForecast d = new WeatherData.DailyForecast();
d.setDate(today.plusDays(i).atStartOfDay());
d.setTemperatureMax(dailyHighs[i]);
d.setTemperatureMin(dailyLows[i]);
d.setWeatherCode(dailyCodes[i]);
d.setPrecipitationProbability(i == 3 || i == 4 ? 40 : 10);
d.setPrecipitationSum(i == 3 ? 0.1 : 0);
d.setWindSpeedMax(10 + i);
d.setUvIndexMax(5 + (i % 3));
d.setSunrise(today.plusDays(i).atTime(6, 30));
d.setSunset(today.plusDays(i).atTime(20, 15));
daily.add(d);
}
data.setDaily(daily);
return data;
}
private void createWeatherApp()
{
setBackgroundColor("#f0f4f8");
setMinHeight("800px");
setPosition(Position.RELATIVE);
addElement(createStyles());
mainContainer = new Div();
mainContainer.setMaxWidth("1400px");
mainContainer.setMargin("0 auto");
mainContainer.setPadding("20px");
mainContainer.addElement(createHeader());
mainContainer.addElement(createMainContent());
mainContainer.addElement(createForecastTabs());
mainContainer.addElement(createDailyForecast());
mainContainer.addElement(createHourlyOverview());
addElement(mainContainer);
addElement(createLocationModal());
}
private Style createStyles()
{
Style style = new Style();
style.setText("""
.weather-stat {
display: flex;
flex-direction: column;
align-items: flex-start;
}
.weather-stat-label {
font-size: 12px;
color: #94a3b8;
margin-bottom: 4px;
display: flex;
align-items: center;
gap: 4px;
}
.weather-stat-value {
font-size: 15px;
color: #ffffff;
font-weight: 500;
}
.forecast-tab {
padding: 10px 20px;
background: transparent;
border: none;
color: #64748b;
font-size: 14px;
cursor: pointer;
border-radius: 20px;
transition: all 0.2s;
}
.forecast-tab:hover {
background: #e2e8f0;
}
.forecast-tab.active {
background: #3b82f6;
color: white;
}
.day-card {
display: flex;
flex-direction: column;
align-items: center;
padding: 16px 16px;
min-width: 110px;
border-radius: 12px;
transition: all 0.2s;
cursor: pointer;
}
.day-card:hover {
background: #f1f5f9;
}
.day-card.today {
background: #1e3a5f;
}
.day-card.today * {
color: white !important;
}
.hourly-item {
display: flex;
flex-direction: column;
align-items: center;
min-width: 70px;
padding: 8px 4px;
}
.location-selector:hover {
background: #cbd5e1 !important;
}
.location-item:hover {
background: #f1f5f9;
}
.my-location-btn:hover {
background: #2563eb !important;
transform: scale(1.02);
}
""");
return style;
}
private Div createHeader()
{
Div header = new Div();
header.setDisplay(Display.FLEX);
header.setAlignItems(AlignItems.CENTER);
header.addStyleAttribute("gap", "12px");
header.setMarginBottom("20px");
// Location selector pill button
locationSelector = new Div();
locationSelector.addClass("location-selector");
locationSelector.setDisplay(Display.FLEX);
locationSelector.setAlignItems(AlignItems.CENTER);
locationSelector.addStyleAttribute("gap", "8px");
locationSelector.addStyleAttribute("cursor", "pointer");
locationSelector.setPadding("10px 16px");
locationSelector.setBorderRadius("24px");
locationSelector.setBackgroundColor("#e2e8f0");
locationSelector.addStyleAttribute("transition", "all 0.2s");
locationSelector.registerListener(this, MouseClickedEvent.class);
// Location pin icon
Span pinIcon = new Span();
pinIcon.setText("\uD83D\uDCCD");
pinIcon.setFontSize("16px");
locationSelector.addElement(pinIcon);
// Location name
locationLabel = new Span();
locationLabel.setText(weatherData.getLocationName());
locationLabel.setFontSize("14px");
locationLabel.setFontWeight("600");
locationLabel.setColor("#1e293b");
locationSelector.addElement(locationLabel);
// Dropdown arrow
Span arrow = new Span();
arrow.setText("\u25BC");
arrow.setFontSize("8px");
arrow.setColor("#64748b");
locationSelector.addElement(arrow);
header.addElement(locationSelector);
// My Location button
myLocationBtn = new Div();
myLocationBtn.addClass("my-location-btn");
myLocationBtn.setDisplay(Display.FLEX);
myLocationBtn.setAlignItems(AlignItems.CENTER);
myLocationBtn.addStyleAttribute("gap", "6px");
myLocationBtn.addStyleAttribute("cursor", "pointer");
myLocationBtn.setPadding("10px 16px");
myLocationBtn.setBorderRadius("24px");
myLocationBtn.setBackgroundColor(ACCENT_COLOR);
myLocationBtn.addStyleAttribute("transition", "all 0.2s");
myLocationBtn.registerListener(this, MouseClickedEvent.class);
myLocationBtn.registerListener(this, GeolocationEvent.class);
myLocationBtn.registerListener(this, GeolocationErrorEvent.class);
// Crosshairs icon
Span crosshairsIcon = new Span();
crosshairsIcon.setText("\u2316");
crosshairsIcon.setFontSize("16px");
crosshairsIcon.setColor("#ffffff");
myLocationBtn.addElement(crosshairsIcon);
// Button text
Span btnText = new Span();
btnText.setText("My Location");
btnText.setFontSize("14px");
btnText.setFontWeight("600");
btnText.setColor("#ffffff");
myLocationBtn.addElement(btnText);
header.addElement(myLocationBtn);
return header;
}
private Div createLocationModal()
{
// Modal overlay (covers entire screen)
modalOverlay = new Div();
modalOverlay.setPosition(Position.FIXED);
modalOverlay.setTop("0");
modalOverlay.setLeft("0");
modalOverlay.setRight("0");
modalOverlay.setBottom("0");
modalOverlay.setBackgroundColor("rgba(0, 0, 0, 0.5)");
modalOverlay.setDisplay(Display.NONE);
modalOverlay.addStyleAttribute("z-index", "1000");
modalOverlay.addStyleAttribute("justify-content", "center");
modalOverlay.addStyleAttribute("align-items", "center");
modalOverlay.registerListener(this, MouseClickedEvent.class);
// Modal dialog
locationModal = new Div();
locationModal.setBackgroundColor(CARD_BG);
locationModal.setBorderRadius("16px");
locationModal.setMaxWidth("480px");
locationModal.setWidth("90%");
locationModal.setMaxHeight("80vh");
locationModal.addStyleAttribute("overflow", "hidden");
locationModal.addStyleAttribute("box-shadow", "0 25px 50px -12px rgba(0, 0, 0, 0.25)");
locationModal.registerListener(this, MouseClickedEvent.class);
// Modal header
Div modalHeader = new Div();
modalHeader.setDisplay(Display.FLEX);
modalHeader.setJustifyContent(JustifyContent.SPACE_BETWEEN);
modalHeader.setAlignItems(AlignItems.CENTER);
modalHeader.setPadding("20px 24px");
modalHeader.setBorderBottom("1px solid #e2e8f0");
Span modalTitle = new Span();
modalTitle.setText("Select Location");
modalTitle.setFontSize("18px");
modalTitle.setFontWeight("600");
modalTitle.setColor("#1e293b");
modalHeader.addElement(modalTitle);
// Close button
modalCloseBtn = new Div();
modalCloseBtn.setText("\u2715");
modalCloseBtn.setFontSize("18px");
modalCloseBtn.setColor("#64748b");
modalCloseBtn.addStyleAttribute("cursor", "pointer");
modalCloseBtn.setPadding("4px 8px");
modalCloseBtn.setBorderRadius("4px");
modalCloseBtn.registerListener(this, MouseClickedEvent.class);
modalHeader.addElement(modalCloseBtn);
locationModal.addElement(modalHeader);
// Search section
Div searchSection = new Div();
searchSection.setPadding("16px 24px");
searchSection.setBorderBottom("1px solid #e2e8f0");
// Search input container
Div searchContainer = new Div();
searchContainer.setPosition(Position.RELATIVE);
// Search icon
Span searchIcon = new Span();
searchIcon.setText("\uD83D\uDD0D");
searchIcon.setPosition(Position.ABSOLUTE);
searchIcon.setLeft("12px");
searchIcon.setTop("50%");
searchIcon.addStyleAttribute("transform", "translateY(-50%)");
searchIcon.setFontSize("14px");
searchIcon.setColor("#94a3b8");
searchContainer.addElement(searchIcon);
// Search input
searchInput = new TextInput();
searchInput.setName("location-search");
searchInput.addAttribute("placeholder", "Search for a city...");
searchInput.setWidth("100%");
searchInput.setPadding("12px 12px 12px 40px");
searchInput.setBorderRadius("8px");
searchInput.setBorder("1px solid #e2e8f0");
searchInput.setFontSize("15px");
searchInput.addStyleAttribute("outline", "none");
searchInput.addStyleAttribute("box-sizing", "border-box");
searchInput.registerListener(this, InputCompleteEvent.class);
searchContainer.addElement(searchInput);
searchSection.addElement(searchContainer);
// Search hint
Span searchHint = new Span();
searchHint.setText("Press Enter to search");
searchHint.setFontSize("12px");
searchHint.setColor("#94a3b8");
searchHint.setDisplay(Display.BLOCK);
searchHint.setMarginTop("8px");
searchSection.addElement(searchHint);
locationModal.addElement(searchSection);
// Search results container (hidden by default)
searchResultsContainer = new Div();
searchResultsContainer.setDisplay(Display.NONE);
searchResultsContainer.setPadding("8px 0");
searchResultsContainer.setMaxHeight("200px");
searchResultsContainer.addStyleAttribute("overflow-y", "auto");
searchResultsContainer.setBorderBottom("1px solid #e2e8f0");
locationModal.addElement(searchResultsContainer);
// Preset locations header
Div presetHeader = new Div();
presetHeader.setPadding("12px 24px 8px 24px");
Span presetTitle = new Span();
presetTitle.setText("Popular Cities");
presetTitle.setFontSize("12px");
presetTitle.setFontWeight("600");
presetTitle.setColor("#94a3b8");
presetTitle.addStyleAttribute("text-transform", "uppercase");
presetTitle.addStyleAttribute("letter-spacing", "0.5px");
presetHeader.addElement(presetTitle);
locationModal.addElement(presetHeader);
// Preset location list
presetLocationsContainer = new Div();
presetLocationsContainer.setPadding("0 0 8px 0");
presetLocationsContainer.setMaxHeight("280px");
presetLocationsContainer.addStyleAttribute("overflow-y", "auto");
for (String[] loc : LOCATIONS)
{
String name = loc[0];
Div item = createLocationItem(name, name.equals(weatherData.getLocationName()));
locationItems.put(name, item);
presetLocationsContainer.addElement(item);
}
locationModal.addElement(presetLocationsContainer);
modalOverlay.addElement(locationModal);
return modalOverlay;
}
private Div createLocationItem(String name, boolean isSelected)
{
Div item = new Div();
item.addClass("location-item");
item.setDisplay(Display.FLEX);
item.setAlignItems(AlignItems.CENTER);
item.addStyleAttribute("gap", "12px");
item.setPadding("12px 24px");
item.addStyleAttribute("cursor", "pointer");
item.addStyleAttribute("transition", "background 0.15s");
item.registerListener(this, MouseClickedEvent.class);
// Location pin icon
Span pin = new Span();
pin.setText("\uD83D\uDCCD");
pin.setFontSize("16px");
item.addElement(pin);
// Location name
Span nameSpan = new Span();
nameSpan.setText(name);
nameSpan.setFontSize("15px");
nameSpan.setColor("#1e293b");
if (isSelected)
{
nameSpan.setFontWeight("600");
nameSpan.setColor(ACCENT_COLOR);
}
item.addElement(nameSpan);
// Check mark for current location
if (isSelected)
{
Span check = new Span();
check.setText("\u2713");
check.setColor(ACCENT_COLOR);
check.setFontWeight("600");
check.setMarginLeft("auto");
item.addElement(check);
}
return item;
}
private void showLocationModal()
{
modalOverlay.setDisplay(Display.FLEX);
}
private void hideLocationModal()
{
modalOverlay.setDisplay(Display.NONE);
clearSearch();
}
private void selectLocation(String locationName)
{
// Find location data
String lat = null;
String lng = null;
for (String[] loc : LOCATIONS)
{
if (loc[0].equals(locationName))
{
lat = loc[1];
lng = loc[2];
break;
}
}
if (lat == null || lng == null)
{
return;
}
// Hide modal
hideLocationModal();
// Fetch new weather data
try
{
OpenMeteoClient client = new OpenMeteoClient();
weatherData = client.getWeather(Double.parseDouble(lat), Double.parseDouble(lng), locationName);
}
catch (Exception e)
{
System.err.println("Weather API failed: " + e.getMessage());
return;
}
// Reset to today
selectedDayIndex = 0;
activeTab = "Overview";
// Refresh the entire UI
refreshWeatherDisplay();
}
private void refreshWeatherDisplay()
{
// Update location label
if (locationLabel != null)
{
locationLabel.setText(weatherData.getLocationName());
}
// Rebuild the main content
mainContainer.removeAllElements();
dayCards.clear();
tabButtons.clear();
mainContainer.addElement(createHeader());
mainContainer.addElement(createMainContent());
mainContainer.addElement(createForecastTabs());
mainContainer.addElement(createDailyForecast());
mainContainer.addElement(createHourlyOverview());
// Rebuild modal to update selected location styling
removeElement(modalOverlay);
locationItems.clear();
addElement(createLocationModal());
}
private Div createMainContent()
{
Div main = new Div();
main.setDisplay(Display.GRID);
main.addStyleAttribute("grid-template-columns", "1fr 1fr");
main.addStyleAttribute("gap", "20px");
main.setMarginBottom("20px");
main.addElement(createCurrentWeatherPanel());
main.addElement(createMapPanel());
return main;
}
private Div createCurrentWeatherPanel()
{
Div panel = new Div();
panel.setBackgroundColor(PRIMARY_BG);
panel.setBorderRadius("16px");
panel.setPadding("24px");
panel.setColor(TEXT_PRIMARY);
// Header row
Div headerRow = new Div();
headerRow.setDisplay(Display.FLEX);
headerRow.setJustifyContent(JustifyContent.SPACE_BETWEEN);
headerRow.setAlignItems(AlignItems.FLEX_START);
headerRow.setMarginBottom("20px");
Div leftHeader = new Div();
Span title = new Span();
title.setText("Current weather");
title.setFontSize("14px");
title.setColor(TEXT_SECONDARY);
title.setDisplay(Display.BLOCK);
leftHeader.addElement(title);
if (weatherData.getCurrent() != null)
{
Span time = new Span();
time.setText(weatherData.getCurrent().getTime().format(DateTimeFormatter.ofPattern("h:mm a")));
time.setFontSize("12px");
time.setColor(TEXT_SECONDARY);
time.setDisplay(Display.BLOCK);
leftHeader.addElement(time);
}
headerRow.addElement(leftHeader);
panel.addElement(headerRow);
// Main temperature display
if (weatherData.getCurrent() != null)
{
Div tempRow = new Div();
tempRow.setDisplay(Display.FLEX);
tempRow.setAlignItems(AlignItems.CENTER);
tempRow.addStyleAttribute("gap", "20px");
tempRow.setMarginBottom("16px");
// Weather icon
Div iconWrapper = new Div();
boolean isDay = isCurrentlyDay();
RawHtml icon = new RawHtml(WeatherCode.getSvgIcon(weatherData.getCurrent().getWeatherCode(), isDay, 80));
iconWrapper.addElement(icon);
tempRow.addElement(iconWrapper);
// Temperature
Span temp = new Span();
temp.setText(String.format("%.0f", weatherData.getCurrent().getTemperature()));
temp.setFontSize("72px");
temp.setFontWeight("300");
temp.setColor(TEXT_PRIMARY);
tempRow.addElement(temp);
// Degree symbol
Span degree = new Span();
degree.setText("\u00B0F");
degree.setFontSize("32px");
degree.setColor(TEXT_SECONDARY);
degree.addStyleAttribute("align-self", "flex-start");
degree.setMarginTop("16px");
tempRow.addElement(degree);
// Condition text
Div conditionDiv = new Div();
conditionDiv.setMarginLeft("20px");
Span condition = new Span();
condition.setText(weatherData.getCurrent().getConditionText());
condition.setFontSize("20px");
condition.setFontWeight("500");
condition.setDisplay(Display.BLOCK);
condition.setMarginBottom("4px");
conditionDiv.addElement(condition);
Span feelsLike = new Span();
feelsLike.setText(String.format("Feels like %.0f\u00B0", weatherData.getCurrent().getApparentTemperature()));
feelsLike.setFontSize("14px");
feelsLike.setColor(TEXT_SECONDARY);
conditionDiv.addElement(feelsLike);
tempRow.addElement(conditionDiv);
panel.addElement(tempRow);
// Weather summary
Div summaryDiv = new Div();
summaryDiv.setMarginBottom("24px");
String summary = generateWeatherSummary();
P summaryText = new P();
summaryText.setText(summary);
summaryText.setFontSize("14px");
summaryText.setColor(TEXT_SECONDARY);
summaryText.setMarginTop("0");
summaryText.setMarginBottom("0");
summaryDiv.addElement(summaryText);
panel.addElement(summaryDiv);
// Stats row
Div statsRow = new Div();
statsRow.setDisplay(Display.FLEX);
statsRow.addStyleAttribute("gap", "32px");
statsRow.setFlexWrap(FlexWrap.WRAP);
statsRow.addElement(createStat("Air quality", "25", "\uD83D\uDFE2"));
statsRow.addElement(createStat("Wind", String.format("%.0f mph %s",
weatherData.getCurrent().getWindSpeed(),
weatherData.getCurrent().getWindDirectionText()), "\uD83D\uDCA8"));
statsRow.addElement(createStat("Humidity", weatherData.getCurrent().getHumidity() + "%", "\uD83D\uDCA7"));
statsRow.addElement(createStat("Visibility", String.format("%.1f mi", weatherData.getCurrent().getVisibility()), "\uD83D\uDC41"));
statsRow.addElement(createStat("Pressure", String.format("%.2f in", weatherData.getCurrent().getPressure() * 0.02953), "\uD83D\uDCCA"));
statsRow.addElement(createStat("Dew point", String.format("%.0f\u00B0", calculateDewPoint()), "\uD83C\uDF21"));
panel.addElement(statsRow);
}
return panel;
}
private Div createStat(String label, String value, String icon)
{
Div stat = new Div();
stat.addClass("weather-stat");
Span labelSpan = new Span();
labelSpan.addClass("weather-stat-label");
labelSpan.setText(label);
stat.addElement(labelSpan);
Span valueSpan = new Span();
valueSpan.addClass("weather-stat-value");
valueSpan.setText(icon + " " + value);
stat.addElement(valueSpan);
return stat;
}
private Div createMapPanel()
{
Div panel = new Div();
panel.setBackgroundColor(CARD_BG);
panel.setBorderRadius("16px");
panel.addStyleAttribute("overflow", "hidden");
panel.addStyleAttribute("line-height", "0");
panel.setPosition(Position.RELATIVE);
// Map
weatherMap = new LfMap(weatherData.getLatitude(), weatherData.getLongitude(), 13);
weatherMap.setId("weather-map");
weatherMap.setHeight("100%");
weatherMap.setWidth("100%");
weatherMap.addOpenStreetMapLayer();
// Add marker for selected location
addSelectedLocationMarker(weatherMap);
panel.addElement(weatherMap);
// Layer buttons at top
Div layerButtons = new Div();
layerButtons.setPosition(Position.ABSOLUTE);
layerButtons.setTop("12px");
layerButtons.setRight("12px");
layerButtons.setDisplay(Display.FLEX);
layerButtons.addStyleAttribute("gap", "4px");
layerButtons.setBackgroundColor("rgba(255, 255, 255, 0.9)");
layerButtons.setBorderRadius("8px");
layerButtons.setPadding("4px");
layerButtons.addElement(createLayerButton("\uD83C\uDF27", true));
layerButtons.addElement(createLayerButton("\uD83C\uDF21", false));
layerButtons.addElement(createLayerButton("\uD83D\uDCA8", false));
panel.addElement(layerButtons);
return panel;
}
private void addSelectedLocationMarker(LfMap map)
{
double lat = weatherData.getLatitude();
double lng = weatherData.getLongitude();
String locationName = weatherData.getLocationName();
// Get current temperature for the marker
String temp = "";
if (weatherData.getCurrent() != null)
{
temp = String.format("%.0f\u00B0F", weatherData.getCurrent().getTemperature());
}
// Create a prominent marker with location pin style
LfDivIcon divIcon = new LfDivIcon()
.setHtml(String.format(
"<div style='display:flex;flex-direction:column;align-items:center;'>" +
"<div style='background:%s;color:white;padding:4px 8px;border-radius:6px;font-size:12px;font-weight:600;box-shadow:0 2px 6px rgba(0,0,0,0.3);white-space:nowrap;'>%s</div>" +
"<div style='width:0;height:0;border-left:8px solid transparent;border-right:8px solid transparent;border-top:8px solid %s;'></div>" +
"</div>",
ACCENT_COLOR, temp, ACCENT_COLOR))
.setIconSize(80, 40)
.setIconAnchor(40, 40);
LfMarker marker = new LfMarker(lat, lng)
.setDivIcon(divIcon)
.setTooltip(locationName);
map.addMarker(marker);
}
private Div createLayerButton(String icon, boolean active)
{
Div btn = new Div();
btn.setText(icon);
btn.setPadding("8px 12px");
btn.setBorderRadius("6px");
btn.setBackgroundColor(active ? ACCENT_COLOR : "transparent");
btn.setColor(active ? "#fff" : "#64748b");
btn.addStyleAttribute("cursor", "pointer");
btn.setFontSize("14px");
return btn;
}
private Div createForecastTabs()
{
Div tabsContainer = new Div();
tabsContainer.setBackgroundColor(CARD_BG);
tabsContainer.setBorderRadius("16px");
tabsContainer.setPadding("16px 20px");
tabsContainer.setMarginBottom("20px");
Div tabsRow = new Div();
tabsRow.setDisplay(Display.FLEX);
tabsRow.setAlignItems(AlignItems.CENTER);
tabsRow.setJustifyContent(JustifyContent.SPACE_BETWEEN);
// Left tabs
Div leftTabs = new Div();
leftTabs.setDisplay(Display.FLEX);
leftTabs.addStyleAttribute("gap", "8px");
leftTabs.setAlignItems(AlignItems.CENTER);
leftTabs.setFlexWrap(FlexWrap.WRAP);
Span hourlyLabel = new Span();
hourlyLabel.setText("Hourly");
hourlyLabel.setFontWeight("600");
hourlyLabel.setColor("#1e293b");
hourlyLabel.setMarginRight("16px");
leftTabs.addElement(hourlyLabel);
// Only include tabs that have data from the API
String[] tabs = {"Overview", "Precipitation", "Wind", "Humidity", "Cloud cover", "Visibility", "Feels like"};
for (String tab : tabs)
{
Div tabBtn = new Div();
tabBtn.addClass("forecast-tab");
if (tab.equals(activeTab))
{
tabBtn.addClass("active");
}
tabBtn.setText(tab);
tabBtn.registerListener(this, MouseClickedEvent.class);
tabButtons.put(tab, tabBtn);
leftTabs.addElement(tabBtn);
}
tabsRow.addElement(leftTabs);
tabsContainer.addElement(tabsRow);
return tabsContainer;
}
private Div createDailyForecast()
{
Div container = new Div();
container.setBackgroundColor(CARD_BG);
container.setBorderRadius("16px");
container.setPadding("20px");
container.setMarginBottom("20px");
Div daysRow = new Div();
daysRow.setDisplay(Display.FLEX);
daysRow.addStyleAttribute("gap", "8px");
daysRow.addStyleAttribute("overflow-x", "auto");
if (weatherData.getDaily() != null)
{
LocalDate today = LocalDate.now();
List<WeatherData.DailyForecast> dailyList = weatherData.getDaily();
// Add yesterday (placeholder) - not clickable
daysRow.addElement(createDayCard("11", "Yesterday", 0, 44, 30, false, true, -1));
for (int i = 0; i < Math.min(10, dailyList.size()); i++)
{
WeatherData.DailyForecast day = dailyList.get(i);
LocalDate date = day.getDate().toLocalDate();
boolean isSelected = (i == selectedDayIndex);
String dayNum = String.valueOf(date.getDayOfMonth());
String dayName = (i == 0) ? "Today" : date.format(DateTimeFormatter.ofPattern("EEE"));
daysRow.addElement(createDayCard(
dayNum,
dayName,
day.getWeatherCode(),
(int) day.getTemperatureMax(),
(int) day.getTemperatureMin(),
isSelected,
false,
i
));
}
}
container.addElement(daysRow);
return container;
}
private Div createDayCard(String dayNum, String dayName, int weatherCode, int high, int low, boolean isSelected, boolean isPast, int dayIndex)
{
Div card = new Div();
card.addClass("day-card");
if (isSelected)
{
card.addClass("today");
}
// Register click listener for selectable days (not past days)
if (!isPast)
{
card.registerListener(this, MouseClickedEvent.class);
dayCards.put(dayIndex, card);
}
// Day number
Span num = new Span();
num.setText(dayNum);
num.setFontSize("18px");
num.setFontWeight("600");
num.setColor(isPast ? "#94a3b8" : "#1e293b");
num.setMarginBottom("4px");
card.addElement(num);
// Day name
Span name = new Span();
name.setText(dayName);
name.setFontSize("12px");
name.setColor(isPast ? "#94a3b8" : "#64748b");
name.setMarginBottom("12px");
card.addElement(name);
// Weather icon
Div iconDiv = new Div();
iconDiv.setMarginBottom("12px");
RawHtml icon = new RawHtml(WeatherCode.getSvgIcon(weatherCode, true, 56));
iconDiv.addElement(icon);
card.addElement(iconDiv);
// High temp
Span highTemp = new Span();
highTemp.setText(high + "\u00B0");
highTemp.setFontSize("16px");
highTemp.setFontWeight("600");
highTemp.setColor(isPast ? "#94a3b8" : "#1e293b");
highTemp.setMarginBottom("4px");
card.addElement(highTemp);
// Low temp
Span lowTemp = new Span();
lowTemp.setText(low + "\u00B0");
lowTemp.setFontSize("14px");
lowTemp.setColor("#94a3b8");
card.addElement(lowTemp);
return card;
}
private Div createHourlyOverview()
{
Div container = new Div();
container.setBackgroundColor(CARD_BG);
container.setBorderRadius("16px");
container.addStyleAttribute("overflow", "hidden");
// Header
Div header = new Div();
header.setDisplay(Display.FLEX);
header.setAlignItems(AlignItems.CENTER);
header.addStyleAttribute("gap", "12px");
header.setPadding("16px 20px");
hourlyTitle = new Span();
hourlyTitle.setText("Overview");
hourlyTitle.setFontSize("16px");
hourlyTitle.setFontWeight("600");
hourlyTitle.setColor("#1e293b");
header.addElement(hourlyTitle);
dateLabel = new Span();
dateLabel.setText(getSelectedDateLabel());
dateLabel.setFontSize("14px");
dateLabel.setColor("#64748b");
header.addElement(dateLabel);
container.addElement(header);
// Hourly items row
hourlyRow = new Div();
hourlyRow.setDisplay(Display.FLEX);
hourlyRow.addStyleAttribute("gap", "4px");
hourlyRow.setPadding("0 20px 16px 20px");
hourlyRow.addStyleAttribute("overflow-x", "auto");
populateHourlyRow(hourlyRow);
container.addElement(hourlyRow);
// Temperature chart container
chartContainer = new Div();
chartContainer.setPosition(Position.RELATIVE);
chartContainer.addStyleAttribute("background", "linear-gradient(to bottom, rgba(59, 130, 246, 0.1) 0%, rgba(59, 130, 246, 0.05) 50%, rgba(16, 185, 129, 0.05) 100%)");
chartContainer.setBorderRadius("0 0 12px 12px");
populateChart(chartContainer);
container.addElement(chartContainer);
// Footer
Div footer = new Div();
footer.setDisplay(Display.FLEX);
footer.setJustifyContent(JustifyContent.SPACE_BETWEEN);
footer.setPadding("16px 20px");
footer.setBorderTop("1px solid #e2e8f0");
Div legend = new Div();
legend.setDisplay(Display.FLEX);
legend.setAlignItems(AlignItems.CENTER);
legend.addStyleAttribute("gap", "8px");
legendDot = new Div();
legendDot.setWidth("8px");
legendDot.setHeight("8px");
legendDot.setBorderRadius("50%");
legendDot.setBackgroundColor(getChartColorForTab());
legend.addElement(legendDot);
legendLabel = new Span();
legendLabel.setText(getSeriesNameForTab());
legendLabel.setFontSize("13px");
legendLabel.setColor("#64748b");
legend.addElement(legendLabel);
footer.addElement(legend);
// Right side container for sunrise, sunset, moon phase
Div rightInfo = new Div();
rightInfo.setDisplay(Display.FLEX);
rightInfo.setAlignItems(AlignItems.CENTER);
rightInfo.addStyleAttribute("gap", "20px");
// Get today's forecast for sunrise/sunset times
WeatherData.DailyForecast today = weatherData.getDaily() != null && !weatherData.getDaily().isEmpty()
? weatherData.getDaily().get(selectedDayIndex < weatherData.getDaily().size() ? selectedDayIndex : 0)
: null;
// Sunrise
if (today != null && today.getSunrise() != null)
{
Div sunriseInfo = new Div();
sunriseInfo.setDisplay(Display.FLEX);
sunriseInfo.setAlignItems(AlignItems.CENTER);
sunriseInfo.addStyleAttribute("gap", "4px");
Span sunriseIcon = new Span();
sunriseIcon.setText("\uD83C\uDF05");
sunriseIcon.setFontSize("16px");
sunriseInfo.addElement(sunriseIcon);
Span sunriseTime = new Span();
sunriseTime.setText(today.getSunrise().format(DateTimeFormatter.ofPattern("h:mm a")));
sunriseTime.setFontSize("13px");
sunriseTime.setColor("#1e293b");
sunriseInfo.addElement(sunriseTime);
rightInfo.addElement(sunriseInfo);
}
// Sunset
if (today != null && today.getSunset() != null)
{
Div sunsetInfo = new Div();
sunsetInfo.setDisplay(Display.FLEX);
sunsetInfo.setAlignItems(AlignItems.CENTER);
sunsetInfo.addStyleAttribute("gap", "4px");
Span sunsetIcon = new Span();
sunsetIcon.setText("\uD83C\uDF07");
sunsetIcon.setFontSize("16px");
sunsetInfo.addElement(sunsetIcon);
Span sunsetTime = new Span();
sunsetTime.setText(today.getSunset().format(DateTimeFormatter.ofPattern("h:mm a")));
sunsetTime.setFontSize("13px");
sunsetTime.setColor("#1e293b");
sunsetInfo.addElement(sunsetTime);
rightInfo.addElement(sunsetInfo);
}
// Moon phase
Div moonPhase = new Div();
moonPhase.setDisplay(Display.FLEX);
moonPhase.setAlignItems(AlignItems.CENTER);
moonPhase.addStyleAttribute("gap", "4px");
Span moonIcon = new Span();
moonIcon.setText("\uD83C\uDF19");
moonIcon.setFontSize("16px");
moonPhase.addElement(moonIcon);
Span moonValue = new Span();
moonValue.setText("Waning Crescent");
moonValue.setFontSize("13px");
moonValue.setColor("#1e293b");
moonPhase.addElement(moonValue);
rightInfo.addElement(moonPhase);
footer.addElement(rightInfo);
container.addElement(footer);
return container;
}
private Div createHourlyItem(String time, int weatherCode, int temp, boolean isDay)
{
Div item = new Div();
item.addClass("hourly-item");
Span timeLabel = new Span();
timeLabel.setText(time);
timeLabel.setFontSize("12px");
timeLabel.setColor("#64748b");
timeLabel.setMarginBottom("8px");
item.addElement(timeLabel);
Div iconDiv = new Div();
iconDiv.setMarginBottom("8px");
RawHtml icon = new RawHtml(WeatherCode.getSvgIcon(weatherCode, isDay, 28));
iconDiv.addElement(icon);
item.addElement(iconDiv);
Span tempLabel = new Span();
tempLabel.setText(temp + "\u00B0");
tempLabel.setFontSize("14px");
tempLabel.setFontWeight("500");
tempLabel.setColor("#1e293b");
item.addElement(tempLabel);
return item;
}
private String generateWeatherSummary()
{
if (weatherData.getCurrent() == null || weatherData.getDaily() == null || weatherData.getDaily().isEmpty())
{
return "Weather data unavailable.";
}
String condition = weatherData.getCurrent().getConditionText().toLowerCase();
double lowTemp = weatherData.getDaily().get(0).getTemperatureMin();
String skyDescription;
if (condition.contains("clear"))
{
skyDescription = "The skies will be clear";
}
else if (condition.contains("cloud"))
{
skyDescription = "Expect cloudy skies";
}
else if (condition.contains("rain"))
{
skyDescription = "Rain is expected";
}
else if (condition.contains("snow"))
{
skyDescription = "Snow is expected";
}
else
{
skyDescription = "The skies will be " + condition;
}
return String.format("%s. The low will be %.0f\u00B0.", skyDescription, lowTemp);
}
private double calculateDewPoint()
{
if (weatherData.getCurrent() == null)
{
return 0;
}
// Convert Fahrenheit to Celsius for calculation
double tempC = (weatherData.getCurrent().getTemperature() - 32) * 5 / 9;
double humidity = weatherData.getCurrent().getHumidity();
// Magnus formula approximation
double a = 17.27;
double b = 237.7;
double alpha = ((a * tempC) / (b + tempC)) + Math.log(humidity / 100.0);
double dewPointC = (b * alpha) / (a - alpha);
// Convert back to Fahrenheit
return dewPointC * 9 / 5 + 32;
}
private boolean isCurrentlyDay()
{
if (weatherData.getDaily() == null || weatherData.getDaily().isEmpty())
{
return true;
}
WeatherData.DailyForecast today = weatherData.getDaily().get(0);
LocalDateTime now = LocalDateTime.now();
if (today.getSunrise() != null && today.getSunset() != null)
{
return now.isAfter(today.getSunrise()) && now.isBefore(today.getSunset());
}
int hour = now.getHour();
return hour >= 6 && hour < 18;
}
private boolean isDay(LocalDateTime time)
{
int hour = time.getHour();
return hour >= 6 && hour < 18;
}
@Override
public void onEvent(MouseClickedEvent event)
{
// Check if location selector was clicked
if (event.getSource() == locationSelector)
{
showLocationModal();
return;
}
// Check if My Location button was clicked
if (event.getSource() == myLocationBtn)
{
requestGeolocation();
return;
}
// Check if modal overlay was clicked (to close)
if (event.getSource() == modalOverlay)
{
hideLocationModal();
return;
}
// Ignore clicks on the modal content itself (prevents closing when clicking inside)
if (event.getSource() == locationModal)
{
return;
}
// Check if close button was clicked
if (event.getSource() == modalCloseBtn)
{
hideLocationModal();
return;
}
// Check if a location item was clicked
for (Map.Entry<String, Div> entry : locationItems.entrySet())
{
if (entry.getValue() == event.getSource())
{
String locationName = entry.getKey();
selectLocation(locationName);
return;
}
}
// Check if a search result item was clicked
for (Map.Entry<Div, OpenMeteoClient.GeoLocation> entry : searchResultItems.entrySet())
{
if (entry.getKey() == event.getSource())
{
selectSearchResult(entry.getValue());
return;
}
}
// Check if a tab was clicked
for (Map.Entry<String, Div> entry : tabButtons.entrySet())
{
if (entry.getValue() == event.getSource())
{
String newTab = entry.getKey();
if (!newTab.equals(activeTab))
{
// Update active tab styling
tabButtons.get(activeTab).removeClass("active");
tabButtons.get(newTab).addClass("active");
activeTab = newTab;
// Update the hourly display
updateHourlyDisplay();
}
return;
}
}
// Check if a day card was clicked
for (Map.Entry<Integer, Div> entry : dayCards.entrySet())
{
if (entry.getValue() == event.getSource())
{
int newDayIndex = entry.getKey();
if (newDayIndex != selectedDayIndex)
{
// Update selected day styling
Div oldCard = dayCards.get(selectedDayIndex);
if (oldCard != null)
{
oldCard.removeClass("today");
}
Div newCard = dayCards.get(newDayIndex);
if (newCard != null)
{
newCard.addClass("today");
}
selectedDayIndex = newDayIndex;
// Update the hourly display for the selected day
updateHourlyDisplay();
}
return;
}
}
}
@Override
public void onEvent(InputCompleteEvent event)
{
if (event.getSource() == searchInput)
{
String query = event.getValue().toString();
if (query != null && !query.trim().isEmpty())
{
performSearch(query.trim());
}
}
}
@Override
public void onEvent(GeolocationEvent event)
{
double lat = event.getLatitude();
double lng = event.getLongitude();
// Fetch weather for the user's location
try
{
OpenMeteoClient client = new OpenMeteoClient();
weatherData = client.getWeather(lat, lng, "Current Location");
}
catch (Exception e)
{
System.err.println("Weather API failed: " + e.getMessage());
return;
}
// Reset to today
selectedDayIndex = 0;
activeTab = "Overview";
// Refresh the entire UI
refreshWeatherDisplay();
}
@Override
public void onEvent(GeolocationErrorEvent event)
{
System.err.println("Geolocation error: " + event.getMessage());
// Could show an alert or message to the user here
}
private void requestGeolocation()
{
GeolocationApi.getCurrentPosition(myLocationBtn, GeolocationOptions.highAccuracy().setTimeout(10000));
}
private void performSearch(String query)
{
OpenMeteoClient client = new OpenMeteoClient();
List<OpenMeteoClient.GeoLocation> results = client.searchLocations(query);
displaySearchResults(results);
}
private void displaySearchResults(List<OpenMeteoClient.GeoLocation> results)
{
searchResultsContainer.removeAllElements();
searchResultItems.clear();
if (results.isEmpty())
{
// Show "no results" message
Div noResults = new Div();
noResults.setPadding("16px 24px");
noResults.setColor("#64748b");
noResults.setFontSize("14px");
noResults.setText("No locations found. Try a different search term.");
searchResultsContainer.addElement(noResults);
}
else
{
// Add header
Div header = new Div();
header.setPadding("8px 24px");
Span headerTitle = new Span();
headerTitle.setText("Search Results");
headerTitle.setFontSize("12px");
headerTitle.setFontWeight("600");
headerTitle.setColor("#94a3b8");
headerTitle.addStyleAttribute("text-transform", "uppercase");
headerTitle.addStyleAttribute("letter-spacing", "0.5px");
header.addElement(headerTitle);
searchResultsContainer.addElement(header);
// Add results
for (OpenMeteoClient.GeoLocation location : results)
{
Div item = createSearchResultItem(location);
searchResultItems.put(item, location);
searchResultsContainer.addElement(item);
}
}
// Show search results container
searchResultsContainer.setDisplay(Display.BLOCK);
}
private Div createSearchResultItem(OpenMeteoClient.GeoLocation location)
{
Div item = new Div();
item.addClass("location-item");
item.setDisplay(Display.FLEX);
item.setAlignItems(AlignItems.CENTER);
item.addStyleAttribute("gap", "12px");
item.setPadding("12px 24px");
item.addStyleAttribute("cursor", "pointer");
item.addStyleAttribute("transition", "background 0.15s");
item.registerListener(this, MouseClickedEvent.class);
// Search result icon
Span icon = new Span();
icon.setText("\uD83D\uDD0D");
icon.setFontSize("16px");
item.addElement(icon);
// Location details
Div details = new Div();
Span name = new Span();
name.setText(location.getDisplayName());
name.setFontSize("15px");
name.setColor("#1e293b");
name.setDisplay(Display.BLOCK);
details.addElement(name);
// Show country for non-US locations
if (location.getCountry() != null && !location.getCountry().isEmpty()
&& !"United States".equals(location.getCountry()))
{
Span country = new Span();
country.setText(location.getCountry());
country.setFontSize("12px");
country.setColor("#94a3b8");
country.setDisplay(Display.BLOCK);
details.addElement(country);
}
item.addElement(details);
return item;
}
private void selectSearchResult(OpenMeteoClient.GeoLocation location)
{
// Hide modal
hideLocationModal();
// Fetch weather data for the selected location
try
{
OpenMeteoClient client = new OpenMeteoClient();
weatherData = client.getWeather(location.getLatitude(), location.getLongitude(), location.getDisplayName());
}
catch (Exception e)
{
System.err.println("Weather API failed: " + e.getMessage());
return;
}
// Reset to today
selectedDayIndex = 0;
activeTab = "Overview";
// Refresh the entire UI
refreshWeatherDisplay();
// Clear search
clearSearch();
}
private void clearSearch()
{
if (searchInput != null)
{
searchInput.setValue("");
}
if (searchResultsContainer != null)
{
searchResultsContainer.removeAllElements();
searchResultsContainer.setDisplay(Display.NONE);
}
searchResultItems.clear();
}
private void updateHourlyDisplay()
{
// Update the section title
if (hourlyTitle != null)
{
hourlyTitle.setText(activeTab);
}
// Update the date label
if (dateLabel != null)
{
dateLabel.setText(getSelectedDateLabel());
}
// Update the hourly items row
if (hourlyRow != null)
{
hourlyRow.removeAllElements();
populateHourlyRow(hourlyRow);
}
// Update the chart
if (chartContainer != null)
{
chartContainer.removeAllElements();
populateChart(chartContainer);
}
// Update the legend
if (legendDot != null)
{
legendDot.setBackgroundColor(getChartColorForTab());
}
if (legendLabel != null)
{
legendLabel.setText(getSeriesNameForTab());
}
}
private void populateHourlyRow(Div row)
{
if (weatherData.getHourly() == null)
{
return;
}
List<WeatherData.HourlyForecast> hourlyList = weatherData.getHourly();
LocalDateTime now = LocalDateTime.now();
LocalDate selectedDate = getSelectedDate();
// Find start and end indices for the selected day
int startIndex = -1;
int endIndex = -1;
for (int i = 0; i < hourlyList.size(); i++)
{
LocalDate hourDate = hourlyList.get(i).getTime().toLocalDate();
if (hourDate.equals(selectedDate))
{
if (startIndex == -1)
{
startIndex = i;
}
endIndex = i;
}
else if (startIndex != -1)
{
break;
}
}
if (startIndex == -1)
{
return;
}
// For today, start from current hour; for other days, show full day
if (selectedDayIndex == 0)
{
for (int i = startIndex; i <= endIndex && i < hourlyList.size(); i++)
{
if (hourlyList.get(i).getTime().isAfter(now.minusHours(1)))
{
startIndex = Math.max(startIndex, i - 1);
break;
}
}
}
// Show up to 24 hours of data (full day)
int count = 0;
for (int i = startIndex; i <= endIndex && count < 24; i++, count++)
{
WeatherData.HourlyForecast hour = hourlyList.get(i);
boolean isNow = (selectedDayIndex == 0 && count == 1);
String timeLabel;
if (isNow)
{
timeLabel = "Now";
}
else
{
timeLabel = hour.getTime().format(DateTimeFormatter.ofPattern("h a"));
}
row.addElement(createHourlyItemForTab(hour, timeLabel, isDay(hour.getTime())));
}
}
private Div createHourlyItemForTab(WeatherData.HourlyForecast hour, String timeLabel, boolean isDay)
{
Div item = new Div();
item.addClass("hourly-item");
Span time = new Span();
time.setText(timeLabel);
time.setFontSize("12px");
time.setColor("#64748b");
time.setMarginBottom("8px");
item.addElement(time);
// Content varies based on active tab
switch (activeTab)
{
case "Precipitation":
Span precip = new Span();
precip.setText("\uD83D\uDCA7");
precip.setFontSize("24px");
precip.setMarginBottom("8px");
item.addElement(precip);
Span precipValue = new Span();
precipValue.setText(hour.getPrecipitationProbability() + "%");
precipValue.setFontSize("14px");
precipValue.setFontWeight("500");
precipValue.setColor("#3b82f6");
item.addElement(precipValue);
break;
case "Wind":
Span wind = new Span();
wind.setText("\uD83D\uDCA8");
wind.setFontSize("24px");
wind.setMarginBottom("8px");
item.addElement(wind);
Span windValue = new Span();
windValue.setText(String.format("%.0f", hour.getWindSpeed()));
windValue.setFontSize("14px");
windValue.setFontWeight("500");
windValue.setColor("#1e293b");
item.addElement(windValue);
Span windUnit = new Span();
windUnit.setText("mph");
windUnit.setFontSize("10px");
windUnit.setColor("#64748b");
item.addElement(windUnit);
break;
case "Humidity":
Span humid = new Span();
humid.setText("\uD83D\uDCA7");
humid.setFontSize("24px");
humid.setMarginBottom("8px");
item.addElement(humid);
Span humidValue = new Span();
humidValue.setText(hour.getHumidity() + "%");
humidValue.setFontSize("14px");
humidValue.setFontWeight("500");
humidValue.setColor("#06b6d4");
item.addElement(humidValue);
break;
case "Cloud cover":
Span cloud = new Span();
cloud.setText("\u2601");
cloud.setFontSize("24px");
cloud.setMarginBottom("8px");
item.addElement(cloud);
Span cloudValue = new Span();
cloudValue.setText(hour.getCloudCover() + "%");
cloudValue.setFontSize("14px");
cloudValue.setFontWeight("500");
cloudValue.setColor("#64748b");
item.addElement(cloudValue);
break;
case "Visibility":
Span vis = new Span();
vis.setText("\uD83D\uDC41");
vis.setFontSize("24px");
vis.setMarginBottom("8px");
item.addElement(vis);
Span visValue = new Span();
visValue.setText(String.format("%.1f", hour.getVisibility()));
visValue.setFontSize("14px");
visValue.setFontWeight("500");
visValue.setColor("#1e293b");
item.addElement(visValue);
Span visUnit = new Span();
visUnit.setText("mi");
visUnit.setFontSize("10px");
visUnit.setColor("#64748b");
item.addElement(visUnit);
break;
case "Feels like":
Div iconDiv = new Div();
iconDiv.setMarginBottom("8px");
RawHtml icon = new RawHtml(WeatherCode.getSvgIcon(hour.getWeatherCode(), isDay, 28));
iconDiv.addElement(icon);
item.addElement(iconDiv);
Span feelsValue = new Span();
feelsValue.setText(String.format("%.0f\u00B0", hour.getApparentTemperature()));
feelsValue.setFontSize("14px");
feelsValue.setFontWeight("500");
feelsValue.setColor("#f59e0b");
item.addElement(feelsValue);
break;
default: // Overview
Div weatherIcon = new Div();
weatherIcon.setMarginBottom("8px");
RawHtml wIcon = new RawHtml(WeatherCode.getSvgIcon(hour.getWeatherCode(), isDay, 28));
weatherIcon.addElement(wIcon);
item.addElement(weatherIcon);
Span tempValue = new Span();
tempValue.setText(String.format("%.0f\u00B0", hour.getTemperature()));
tempValue.setFontSize("14px");
tempValue.setFontWeight("500");
tempValue.setColor("#1e293b");
item.addElement(tempValue);
break;
}
return item;
}
private void populateChart(Div container)
{
if (weatherData.getHourly() == null)
{
return;
}
List<WeatherData.HourlyForecast> hourlyList = weatherData.getHourly();
LocalDateTime now = LocalDateTime.now();
LocalDate selectedDate = getSelectedDate();
// Find start and end indices for the selected day
int startIndex = -1;
int endIndex = -1;
for (int i = 0; i < hourlyList.size(); i++)
{
LocalDate hourDate = hourlyList.get(i).getTime().toLocalDate();
if (hourDate.equals(selectedDate))
{
if (startIndex == -1)
{
startIndex = i;
}
endIndex = i;
}
else if (startIndex != -1)
{
break;
}
}
if (startIndex == -1)
{
return;
}
// For today, start from current hour; for other days, show full day
if (selectedDayIndex == 0)
{
for (int i = startIndex; i <= endIndex && i < hourlyList.size(); i++)
{
if (hourlyList.get(i).getTime().isAfter(now.minusHours(1)))
{
startIndex = Math.max(startIndex, i - 1);
break;
}
}
}
// Calculate count (full day - up to 24 hours)
int count = Math.min(24, endIndex - startIndex + 1);
// Build categories and data based on active tab
String[] categories = new String[count];
String seriesName = getSeriesNameForTab();
String chartColor = getChartColorForTab();
AxSeries series = new AxSeries(seriesName);
for (int i = 0; i < count; i++)
{
WeatherData.HourlyForecast hour = hourlyList.get(startIndex + i);
if (selectedDayIndex == 0 && i == 1)
{
categories[i] = "Now";
}
else
{
categories[i] = hour.getTime().format(DateTimeFormatter.ofPattern("ha"));
}
series.addData(getDataValueForTab(hour));
}
// Create chart with type based on active tab
String chartType = getChartTypeForTab();
hourlyChart = new AxChart(chartType);
hourlyChart.setId("weatherHourlyChart");
hourlyChart.setChartHeight(300)
.setToolbar(false)
.setZoom(false)
.setLegend(false)
.setGrid(false)
.setDataLabels(false)
.setCategories(categories)
.addSeries(series)
.setColors(chartColor);
// Apply type-specific settings
if (chartType.equals(AxChart.TYPE_AREA) || chartType.equals(AxChart.TYPE_LINE))
{
hourlyChart.setCurve(AxChart.CURVE_SMOOTH).setStrokeWidth(3);
}
container.addElement(hourlyChart);
}
private String getSeriesNameForTab()
{
switch (activeTab)
{
case "Precipitation":
return "Precipitation %";
case "Wind":
return "Wind Speed";
case "Humidity":
return "Humidity %";
case "Cloud cover":
return "Cloud Cover %";
case "Visibility":
return "Visibility (mi)";
case "Feels like":
return "Feels Like";
default:
return "Temperature";
}
}
private String getChartColorForTab()
{
switch (activeTab)
{
case "Precipitation":
return "#3b82f6";
case "Wind":
return "#64748b";
case "Humidity":
return "#06b6d4";
case "Cloud cover":
return "#94a3b8";
case "Visibility":
return "#10b981";
case "Feels like":
return "#f59e0b";
default:
return "#3b82f6";
}
}
private String getChartTypeForTab()
{
switch (activeTab)
{
case "Precipitation":
return AxChart.TYPE_COLUMN;
case "Wind":
return AxChart.TYPE_LINE;
case "Humidity":
return AxChart.TYPE_AREA;
case "Cloud cover":
return AxChart.TYPE_COLUMN;
case "Visibility":
return AxChart.TYPE_LINE;
case "Feels like":
return AxChart.TYPE_AREA;
default:
return AxChart.TYPE_AREA;
}
}
private Number getDataValueForTab(WeatherData.HourlyForecast hour)
{
switch (activeTab)
{
case "Precipitation":
return hour.getPrecipitationProbability();
case "Wind":
return Math.round(hour.getWindSpeed());
case "Humidity":
return hour.getHumidity();
case "Cloud cover":
return hour.getCloudCover();
case "Visibility":
return Math.round(hour.getVisibility() * 10) / 10.0;
case "Feels like":
return Math.round(hour.getApparentTemperature());
default:
return Math.round(hour.getTemperature());
}
}
private LocalDate getSelectedDate()
{
if (weatherData.getDaily() != null && selectedDayIndex < weatherData.getDaily().size())
{
return weatherData.getDaily().get(selectedDayIndex).getDate().toLocalDate();
}
return LocalDate.now().plusDays(selectedDayIndex);
}
private String getSelectedDateLabel()
{
LocalDate date = getSelectedDate();
LocalDate today = LocalDate.now();
if (date.equals(today))
{
return "Today, " + date.format(DateTimeFormatter.ofPattern("MMM d"));
}
else if (date.equals(today.plusDays(1)))
{
return "Tomorrow, " + date.format(DateTimeFormatter.ofPattern("MMM d"));
}
else
{
return date.format(DateTimeFormatter.ofPattern("EEE, MMM d"));
}
}
@Override
public void onEvent(InputChangeEvent event)
{
throw new UnsupportedOperationException("Not supported yet."); // Generated from nbfs://nbhost/SystemFileSystem/Templates/Classes/Code/GeneratedMethodBody
}
}