"use strict";
import { state } from "./state.js";
import { initMap, drawSunnyPlaces, showSunnyPlaceOnMap } from "./map.js";
/* Här samlas sånt som rör väder */
/* Sparar aktuell väderplats som visas i modalen */
let currentModalWeatherPlace = null;
/* Sparar grupperade väderplatser */
let currentGroupedSunPlaces = {};
/**
* Hämtar användarens position från webbläsaren.
* Returnerar koordinater i formatet [lon, lat]
*
* @returns {Promise<number[]>}
*/
function getUserPosition() {
return new Promise((resolve, reject) => {
/* Kontrollera att geolocation stöds */
if (!navigator.geolocation) {
reject(new Error("Din webbläsare stödjer inte geolocation."));
return;
}
navigator.geolocation.getCurrentPosition(
(position) => {
const lat = position.coords.latitude;
const lon = position.coords.longitude;
/* Spara som [lon, lat] */
resolve([lon, lat]);
},
(error) => {
console.error("Fel vid hämtning av position:", error);
reject(new Error("Kunde inte hämta din position."));
}
);
});
}
/**
* Gör om ortnamn till koordinater [lon, lat].
*
* @param {string} query
* @returns {Promise<number[]>}
*/
async function geocodePlace(query) {
const trimmedQuery = query.trim();
if (!trimmedQuery) {
throw new Error("Skriv in en plats.");
}
const url = new URL("https://nominatim.openstreetmap.org/search");
url.searchParams.set("q", trimmedQuery);
url.searchParams.set("format", "jsonv2");
url.searchParams.set("limit", "1");
url.searchParams.set("countrycodes", "se");
const response = await fetch(url, {
headers: {
Accept: "application/json"
}
});
if (!response.ok) {
throw new Error("Kunde inte söka efter platsen.");
}
const results = await response.json();
if (!results.length) {
throw new Error("Kunde inte hitta platsen.");
}
const place = results[0];
const lat = Number(place.lat);
const lon = Number(place.lon);
if (Number.isNaN(lat) || Number.isNaN(lon)) {
throw new Error("Platsen gav ogiltiga koordinater.");
}
return [lon, lat];
}
/**
* Skapar kandidatplatser runt en startpunkt.
*
* @param {number[]} center
* @param {number} radiusKm
* @returns {object[]}
*/
function generateCandidatePlaces(center, radiusKm) {
const [centerLon, centerLat] = center;
const places = [];
/* Punkt nära användaren */
places.push({
id: "sun-center",
name: "Nära dig",
lon: centerLon,
lat: centerLat
});
/* Generera punkter i en cirkel runt användaren */
const count = 12;
for (let i = 0; i < count; i++) {
const angle = (i / count) * Math.PI * 2;
const distanceKm = radiusKm * 0.75;
const latOffset = (distanceKm / 111) * Math.cos(angle);
const lonOffset =
(distanceKm / (111 * Math.cos((centerLat * Math.PI) / 180))) * Math.sin(angle);
places.push({
id: `sun-${i + 1}`,
lon: centerLon + lonOffset,
lat: centerLat + latOffset
});
}
return places;
}
/**
* Räknar ut avstånd i km mellan två punkter.
*
* @param {number[]} pointA
* @param {number[]} pointB
* @returns {number}
*/
function getDistanceKm(pointA, pointB) {
const [lon1, lat1] = pointA;
const [lon2, lat2] = pointB;
const toRadians = (value) => (value * Math.PI) / 180;
const earthRadiusKm = 6371;
const dLat = toRadians(lat2 - lat1);
const dLon = toRadians(lon2 - lon1);
const a =
Math.sin(dLat / 2) ** 2 +
Math.cos(toRadians(lat1)) *
Math.cos(toRadians(lat2)) *
Math.sin(dLon / 2) ** 2;
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return earthRadiusKm * c;
}
/**
* Returnerar en enkel vädertext utifrån API-data.
*
* @param {number} weatherCode
* @param {number} cloudCover
* @param {number} precipitation
* @returns {string}
*/
function getWeatherLabel(weatherCode, cloudCover, precipitation) {
if (precipitation > 0.2) return "Nederbörd";
if (weatherCode === 0 && cloudCover <= 15) return "Klart";
if (cloudCover <= 25) return "Lätt molnighet";
if (cloudCover <= 50) return "Halvklart";
return "Molnigt";
}
/**
* Returnerar vädertyp för sortering och presentation.
*
* @param {object} weather
* @returns {string}
*/
function getWeatherType(weather) {
if (weather.precipitation > 0.2) {
return "bad";
}
if (weather.weather_code === 0 && weather.cloud_cover <= 15) {
return "sunny";
}
if (weather.cloud_cover <= 25) {
return "clear";
}
if (weather.cloud_cover <= 50) {
return "partly-cloudy";
}
return "dry";
}
/**
* Returnerar ikon för vald vädertyp.
*
* @param {string} weatherType
* @returns {string}
*/
function getWeatherIcon(weatherType) {
if (weatherType === "sunny") return "☀️";
if (weatherType === "clear") return "🌤️";
if (weatherType === "partly-cloudy") return "⛅️";
if (weatherType === "dry") return "☁️";
return "🌧️";
}
/**
* Avgör om vädret räknas som fint.
*
* @param {object} weather
* @returns {boolean}
*/
function isGoodWeather(weather) {
const weatherType = getWeatherType(weather);
return weatherType !== "bad";
}
/**
* Hämtar väderdata för en viss timme i prognosen.
*
* @param {object} hourly
* @param {number} index
* @returns {object|null}
*/
function getHourlyWeatherAtIndex(hourly, index) {
if (!hourly?.time?.length || index < 0 || index >= hourly.time.length) {
return null;
}
return {
temperature: hourly.temperature_2m?.[index],
precipitation: hourly.precipitation?.[index],
cloud_cover: hourly.cloud_cover?.[index],
weather_code: hourly.weather_code?.[index],
is_day: hourly.is_day?.[index]
};
}
/**
* Bygger ett väderobjekt med typ, ikon och etikett.
*
* @param {object} basePlace
* @param {object} weather
* @returns {object}
*/
function buildWeatherSnapshot(basePlace, weather) {
if (!weather) {
return null;
}
const weatherData = {
...basePlace,
name: basePlace.name || "Namnlös plats",
temperature: weather.temperature,
precipitation: weather.precipitation,
cloud_cover: weather.cloud_cover,
weather_code: weather.weather_code,
is_day: weather.is_day
};
const weatherType = getWeatherType(weatherData);
return {
...weatherData,
weatherType,
weatherIcon: getWeatherIcon(weatherType),
weatherLabel: getWeatherLabel(
weather.weather_code,
weather.cloud_cover,
weather.precipitation
)
};
}
/**
* Hämtar väderdata för en plats från Open-Meteo.
*
* @param {object} place
* @returns {Promise<object>}
*/
async function fetchWeatherForPlace(place) {
const url = new URL("https://api.open-meteo.com/v1/forecast");
url.searchParams.set("latitude", place.lat);
url.searchParams.set("longitude", place.lon);
url.searchParams.set(
"current",
"temperature_2m,precipitation,cloud_cover,weather_code,is_day");
url.searchParams.set(
"hourly",
"temperature_2m,precipitation,cloud_cover,weather_code,is_day");
url.searchParams.set(
"forecast_days", "2");
const response = await fetch(url);
if (!response.ok) {
throw new Error("Kunde inte hämta väderdata.");
}
const data = await response.json();
const current = data.current;
const hourly = data.hourly;
const currentSnapshot = buildWeatherSnapshot(
{
...place,
name: place.name || ""
},
{
temperature: current.temperature_2m,
precipitation: current.precipitation,
cloud_cover: current.cloud_cover,
weather_code: current.weather_code,
is_day: current.is_day
}
);
/* Enkel första prognos:
senare idag = ungefär 6 timmar fram
imorgon = ungefär 24 timmar fram */
const laterTodaySnapshot = buildWeatherSnapshot(
{
...place,
name: place.name || ""
},
getHourlyWeatherAtIndex(hourly, 6)
);
const tomorrowSnapshot = buildWeatherSnapshot(
{
...place,
name: place.name || ""
},
getHourlyWeatherAtIndex(hourly, 24)
);
return {
...currentSnapshot,
forecasts: {
now: currentSnapshot,
laterToday: laterTodaySnapshot,
tomorrow: tomorrowSnapshot
}
};
}
/**
* Sorterar och döper om väderplatser inom en grupp.
*
* @param {object[]} places
* @returns {object[]}
*/
function sortAndNameSunGroupPlaces(places) {
const weatherPriority = {
sunny: 1,
clear: 2,
"partly-cloudy": 3,
dry: 4
};
return places
.filter((place) => place && isGoodWeather(place))
.sort((a, b) => {
const priorityA = weatherPriority[a.weatherType] || 99;
const priorityB = weatherPriority[b.weatherType] || 99;
if (priorityA !== priorityB) {
return priorityA - priorityB;
}
return (a.distanceKm || 0) - (b.distanceKm || 0);
})
.map((place, index) => ({
...place,
name: place.id === "sun-center"
? "Nära dig"
: `Plats ${index + 1}`
}));
}
/**
* Grupperar väderplatser för nu, senare idag och imorgon.
*
* @param {object[]} places
* @returns {Object<string, object[]>}
*/
function groupSunPlacesByTime(places) {
return {
"Bra väder just nu": sortAndNameSunGroupPlaces(
places.map((place) => place.forecasts?.now ? {
...place.forecasts.now,
distanceKm: place.distanceKm,
forecastGroup: "Just nu"
} : null)
),
"Bra väder senare idag": sortAndNameSunGroupPlaces(
places.map((place) => place.forecasts?.laterToday ? {
...place.forecasts.laterToday,
distanceKm: place.distanceKm,
forecastGroup: "Senare idag"
} : null)
),
"Bra väder imorgon": sortAndNameSunGroupPlaces(
places.map((place) => place.forecasts?.tomorrow ? {
...place.forecasts.tomorrow,
distanceKm: place.distanceKm,
forecastGroup: "Imorgon"
} : null)
)
};
}
/**
* Renderar grupperade väderresultat i utfällbara listor.
*
* @param {object[]} places
*/
function renderSunResultsList(places) {
const container = document.getElementById("sun-results-list");
if (!container) {
return;
}
/* Töm tidigare innehåll */
container.innerHTML = "";
const groupedPlaces = groupSunPlacesByTime(places);
const groups = Object.entries(groupedPlaces);
if (!groups.length) {
container.innerHTML = "<p>Inga platser hittades.</p>";
return;
}
groups.forEach(([groupTitle, groupPlaces]) => {
const group = document.createElement("div");
group.className = "sun-results-group";
const heading = document.createElement("h3");
heading.className = "sun-results-group__title";
const toggleBtn = document.createElement("button");
toggleBtn.type = "button";
toggleBtn.className = "sun-results-group__toggle";
toggleBtn.textContent = `${groupTitle} (${groupPlaces.length})`;
toggleBtn.setAttribute("aria-expanded", "false");
const list = document.createElement("ul");
list.className = "sun-results-group__list";
list.hidden = true;
toggleBtn.addEventListener("click", () => {
const isOpen = toggleBtn.getAttribute("aria-expanded") === "true";
toggleBtn.setAttribute("aria-expanded", String(!isOpen));
list.hidden = isOpen;
/* Uppdatera karta när grupp öppnas */
if (!isOpen) {
drawSunnyPlaces(groupPlaces);
/* Uppdatera rubrik */
const mapTitle = document.getElementById("sun-map-title");
if (mapTitle) {
if (groupTitle === "Bra väder just nu") {
mapTitle.textContent = "Karta för väderprognos just nu";
} else if (groupTitle === "Bra väder senare idag") {
mapTitle.textContent = "Karta för väderprognos senare idag";
} else if (groupTitle === "Bra väder imorgon") {
mapTitle.textContent = "Karta för väderprognos imorgon";
}
}
}
});
if (!groupPlaces.length) {
const emptyItem = document.createElement("li");
emptyItem.className = "stop-item";
emptyItem.textContent = "Inga platser hittades.";
list.appendChild(emptyItem);
}
groupPlaces.forEach((place, index) => {
const item = document.createElement("li");
item.className = "stop-item";
const button = document.createElement("button");
button.type = "button";
button.className = "stop-item__button";
button.dataset.placeId = place.id;
button.innerHTML = `<span class="stop-item__name">${place.weatherIcon || "☀️"} ${place.name}</span>`;
button.addEventListener("click", () => {
openSunModal(place);
});
item.appendChild(button);
list.appendChild(item);
});
heading.appendChild(toggleBtn);
group.appendChild(heading);
group.appendChild(list);
container.appendChild(group);
});
}
/**
* Öppnar sol-modalen och fyller den med data om vald plats.
*
* @param {object} place
*/
function openSunModal(place) {
const modal = document.getElementById("sun-modal");
const title = document.getElementById("sun-modal-title");
const forecastGroup = document.getElementById("sun-forecast-group");
const weather = document.getElementById("sun-weather");
const temperature = document.getElementById("sun-temperature");
const distance = document.getElementById("sun-distance");
const showOnMapBtn = document.getElementById("sun-show-on-map-btn");
const navigateBtn = document.getElementById("sun-navigate-btn");
if (!modal || !title || !forecastGroup || !weather || !temperature || !distance || !showOnMapBtn || !navigateBtn) {
return;
}
/* Spara aktuell plats så modalen alltid använder rätt data */
currentModalWeatherPlace = place;
/* Fyller modalen med data för vald plats */
title.textContent = `${place.weatherIcon || "☀️"} ${place.name || "Namnlös plats"}`;
forecastGroup.textContent = place.forecastGroup || "";
weather.textContent = place.weatherLabel || "Okänt väder";
temperature.textContent = place.temperature !== undefined ? `${place.temperature} °C` : "Okänd";
distance.textContent = place.distanceKm !== undefined ? `${Math.round(place.distanceKm)} km` : "Okänt";
/* Spara vilket place-id som hör till knappen */
showOnMapBtn.dataset.placeId = place.id;
/* Google Maps-länk */
navigateBtn.href = `https://www.google.com/maps/search/?api=1&query=${place.lat},${place.lon}`;
navigateBtn.target = "_blank";
navigateBtn.rel = "noopener noreferrer";
modal.hidden = false;
}
/**
* Stänger sol-modalen.
*/
function closeSunModal() {
const modal = document.getElementById("sun-modal");
if (!modal) {
return;
}
modal.hidden = true;
}
/**
* Kopplar event till sol-modalen.
* Körs en gång när solsidan startar.
*
* @param {Function} onShowOnMap
*/
function initSunModalEvents(onShowOnMap) {
const modal = document.getElementById("sun-modal");
if (!modal) {
return;
}
modal.addEventListener("click", (event) => {
const closeTarget = event.target.closest("[data-close-modal='true']");
/* Stäng modalen vid klick på overlay eller stängknapp */
if (closeTarget) {
closeSunModal();
return;
}
const showOnMapBtn = event.target.closest("#sun-show-on-map-btn");
/* Om klicket inte gäller knappen - gör inget */
if (!showOnMapBtn) {
return;
}
if (!currentModalWeatherPlace) {
return;
}
if (!currentModalWeatherPlace) {
return;
}
/* Hämta rätt grupp baserat på forecast */
let groupKey = "Bra väder just nu";
if (currentModalWeatherPlace.forecastGroup === "Senare idag") {
groupKey = "Bra väder senare idag";
}
if (currentModalWeatherPlace.forecastGroup === "Imorgon") {
groupKey = "Bra väder imorgon";
}
/* Zooma till vald plats */
if (typeof onShowOnMap === "function") {
onShowOnMap(currentModalWeatherPlace);
}
closeSunModal();
});
}
/**
* Startar funktionalitet för solsidan.
*/
export function initWeather() {
/* Hämta element från solsidan */
const sunForm = document.getElementById("sun-form");
const startInput = document.getElementById("sun-start-input");
const useLocationBtn = document.getElementById("sun-use-location-btn");
const statusMessage = document.getElementById("sun-status-message");
const sunMapElement = document.getElementById("sun-map");
/* Om vi inte är på solsidan - gör inget */
if (!sunForm || !sunMapElement) {
return;
}
/* Initiera egen karta för solsidan */
initMap("sun-map", [62.0, 15.0], 5);
/* Kopplar modalens knappar */
initSunModalEvents((place) => {
showSunnyPlaceOnMap(place);
});
/* Klick på 'Hämta min position' */
useLocationBtn?.addEventListener("click", async () => {
try {
statusMessage.textContent = "Hämtar din position...";
const userPosition = await getUserPosition();
/* Spara i globalt state */
state.userPosition = userPosition;
/* Visa i inputfältet att användaren valt sin position */
startInput.value = "Min position";
statusMessage.textContent = "Din position hämtades.";
} catch (error) {
statusMessage.textContent = error.message || "Något gick fel.";
}
});
/* Submit på solsök-formuläret */
sunForm.addEventListener("submit", async (event) => {
event.preventDefault();
try {
const radiusSelect = document.getElementById("sun-radius");
const radiusKm = Number(radiusSelect?.value || 200);
let startCoords = null;
const startValue = startInput.value.trim();
/* Om användaren redan valt 'Min position' används state */
if (startValue === "Min position" && state.userPosition) {
startCoords = state.userPosition;
} else {
statusMessage.textContent = "Söker efter plats...";
startCoords = await geocodePlace(startValue);
}
statusMessage.textContent = "Söker efter soligt väder...";
/* Generera kandidatplatser runt startpunkten */
const candidates = generateCandidatePlaces(startCoords, radiusKm);
/* Hämta väder för alla kandidatplatser. Promise.allSettled gör
att vi kan visa de anrop som lyckas även om vissa misslyckas
(t.ex. vid rate limiting). */
const weatherResultsSettled = await Promise.allSettled(
candidates.map((place) => fetchWeatherForPlace(place))
);
const weatherResults = weatherResultsSettled
.filter((result) => result.status === "fulfilled")
.map((result) => result.value);
/* Filtrera bort dåligt väder, sortera bästa vädret först och döp om platserna i resultatordning */
const weatherPriority = {
sunny: 1,
clear: 2,
"partly-cloudy": 3,
dry: 4
};
const namedPlaces = weatherResults.map((place, index) => ({
...place,
name: index === 0 && place.id === "sun-center"
? "Nära dig"
: `Plats ${index + 1}`
}));
const sunnyPlaces = namedPlaces
.filter(isGoodWeather)
.map((place) => ({
...place,
distanceKm: getDistanceKm(startCoords, [place.lon, place.lat])
}))
.sort((a, b) => {
const priorityA = weatherPriority[a.weatherType] || 99;
const priorityB = weatherPriority[b.weatherType] || 99;
if (priorityA !== priorityB) {
return priorityA - priorityB;
}
return a.distanceKm - b.distanceKm;
});
/* Spara i state */
state.weatherData = sunnyPlaces;
currentGroupedSunPlaces = groupSunPlacesByTime(sunnyPlaces);
const currentSunPlaces = currentGroupedSunPlaces["Bra väder just nu"] || [];
/* Uppdatera UI */
drawSunnyPlaces(currentSunPlaces);
renderSunResultsList(sunnyPlaces);
statusMessage.textContent = sunnyPlaces.length
? `${sunnyPlaces.length} platser med bra väder hittades.`
: "Ingen plats med bra väder hittades just nu.";
} catch (error) {
statusMessage.textContent = error.message || "Något gick fel.";
}
});
}