1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-06-16 16:15:23 -04:00

fix(weather): robust location resolution with parallel fetch and multi-tier fallback (#2638)

Decouple weather data fetching from reverse geocoding so that
weather loads as soon as coordinates are available, even when
Nominatim is unreachable (e.g. mainland China).

- Fetch Open-Meteo weather immediately once lat/lon are known.
- Resolve city name in parallel via Nominatim -> Photon -> BigDataCloud.
- If all reverse geocoding fails, keep displaying weather with a
  placeholder city name.
- Skip reverse geocoding entirely when the user has configured a
  city name.
- Fall back to ip-api.com when GeoClue2 is unavailable or returns
  zero coordinates.
- Add request-generation tracking to discard stale geocoding
  responses after location changes.
- Hold explicit WeatherService refs in bar widget and dashboard tab.

Co-authored-by: lingdiansr <2077258365@qq.com>
This commit is contained in:
lingdianshiren
2026-06-17 02:42:01 +08:00
committed by GitHub
parent ca1a45ccf8
commit 2692777707
3 changed files with 261 additions and 29 deletions
@@ -9,9 +9,8 @@ BasePill {
visible: SettingsData.weatherEnabled
Ref {
service: WeatherService
}
Component.onCompleted: WeatherService.addRef()
Component.onDestruction: WeatherService.removeRef()
content: Component {
Item {
@@ -18,6 +18,9 @@ Item {
property bool showHourly: false
property bool available: WeatherService.weather.available
Component.onCompleted: WeatherService.addRef()
Component.onDestruction: WeatherService.removeRef()
function syncFrom(type) {
if (!dailyLoader.item || !hourlyLoader.item)
return;
+256 -26
View File
@@ -43,6 +43,8 @@ Singleton {
property int lastFetchTime: 0
property int minFetchInterval: 30000
property int persistentRetryCount: 0
property int _geocodeReqId: 0
property var _pendingCoords: null
readonly property var lowPriorityCmd: ["nice", "-n", "19", "ionice", "-c3"]
readonly property var curlBaseCmd: ["curl", "-sS", "--fail", "--connect-timeout", "3", "--max-time", "6", "--limit-rate", "100k", "--compressed"]
@@ -452,16 +454,54 @@ Singleton {
if (!location) {
return null;
}
const params = ["latitude=" + location.latitude, "longitude=" + location.longitude, "current=temperature_2m,relative_humidity_2m,apparent_temperature,is_day,precipitation,weather_code,surface_pressure,wind_speed_10m", "daily=sunrise,sunset,temperature_2m_max,temperature_2m_min,weather_code,precipitation_probability_max", "hourly=temperature_2m,weather_code,precipitation_probability,wind_speed_10m,apparent_temperature,relative_humidity_2m,surface_pressure,visibility,cloud_cover", "timezone=auto", "forecast_days=7"];
return "https://api.open-meteo.com/v1/forecast?" + params.join('&');
return getWeatherApiUrlForCoords(location.latitude, location.longitude);
}
function getGeocodingUrl(query) {
return "https://geocoding-api.open-meteo.com/v1/search?name=" + encodeURIComponent(query) + "&count=1&language=en&format=json";
}
function getConfiguredLocationName() {
return SessionData.isGreeterMode ? GreetdSettings.weatherLocation : SettingsData.weatherLocation;
}
function setLocation(lat, lon, city, country) {
root.location = {
city: city || I18n.tr("Local Weather"),
country: country || "",
latitude: lat,
longitude: lon
};
}
function updateLocationCity(city, country) {
if (!root.location)
return;
root.location = {
latitude: root.location.latitude,
longitude: root.location.longitude,
city: city || root.location.city,
country: country || root.location.country
};
if (root.weather.available) {
root.weather = Object.assign({}, root.weather, {
city: city || root.weather.city,
country: country || root.weather.country
});
}
}
function getWeatherApiUrlForCoords(lat, lon) {
if (lat == null || lon == null)
return null;
const params = ["latitude=" + lat, "longitude=" + lon, "current=temperature_2m,relative_humidity_2m,apparent_temperature,is_day,precipitation,weather_code,surface_pressure,wind_speed_10m", "daily=sunrise,sunset,temperature_2m_max,temperature_2m_min,weather_code,precipitation_probability_max", "hourly=temperature_2m,weather_code,precipitation_probability,wind_speed_10m,apparent_temperature,relative_humidity_2m,surface_pressure,visibility,cloud_cover", "timezone=auto", "forecast_days=7"];
return "https://api.open-meteo.com/v1/forecast?" + params.join('&');
}
function addRef() {
refCount++;
@@ -490,20 +530,30 @@ Singleton {
const lat = parseFloat(parts[0]);
const lon = parseFloat(parts[1]);
if (!isNaN(lat) && !isNaN(lon)) {
getLocationFromCoords(lat, lon);
if (cityName) {
// User provided both: trust the configured name and coordinates, skip geocoding
setLocation(lat, lon, cityName, "");
fetchWeather(lat, lon);
} else {
getLocationFromCoords(lat, lon);
}
return;
}
}
}
if (cityName)
if (cityName) {
getLocationFromCity(cityName);
} else {
root.handleWeatherFailure();
}
}
function getLocationFromCoords(lat, lon) {
const url = "https://nominatim.openstreetmap.org/reverse?lat=" + lat + "&lon=" + lon + "&format=json&addressdetails=1&accept-language=en";
reverseGeocodeFetcher.command = lowPriorityCmd.concat(curlBaseCmd).concat(["-H", "User-Agent: DankMaterialShell Weather Widget", url]);
reverseGeocodeFetcher.running = true;
// Use coordinates immediately for weather; resolve city name in parallel with fallbacks
setLocation(lat, lon, I18n.tr("Local Weather"), "");
fetchWeather(lat, lon);
resolveCityName(lat, lon);
}
function getLocationFromCity(city) {
@@ -512,19 +562,78 @@ Singleton {
}
function getLocationFromService() {
if (!LocationService.valid)
if (!LocationService.valid) {
getLocationFromIP();
return;
getLocationFromCoords(LocationService.latitude, LocationService.longitude);
}
const lat = LocationService.latitude;
const lon = LocationService.longitude;
if (lat === 0 && lon === 0) {
getLocationFromIP();
return;
}
getLocationFromCoords(lat, lon);
}
function fetchWeather() {
function getLocationFromIP() {
ipLocationFetcher.running = true;
}
function resolveCityName(lat, lon) {
// Cancel any in-flight city resolution to avoid stale updates
if (nominatimFetcher.running)
nominatimFetcher.running = false;
if (photonFetcher.running)
photonFetcher.running = false;
if (bigDataCloudFetcher.running)
bigDataCloudFetcher.running = false;
root._geocodeReqId++;
root._pendingCoords = {
latitude: lat,
longitude: lon,
reqId: root._geocodeReqId
};
tryNominatim(lat, lon, root._geocodeReqId);
}
function tryNominatim(lat, lon, reqId) {
const url = "https://nominatim.openstreetmap.org/reverse?lat=" + lat + "&lon=" + lon + "&format=json&addressdetails=1&accept-language=en";
nominatimFetcher.command = lowPriorityCmd.concat(curlBaseCmd).concat(["-H", "User-Agent: DankMaterialShell Weather Widget", url]);
nominatimFetcher.reqId = reqId;
nominatimFetcher.running = true;
}
function tryPhoton(lat, lon, reqId) {
const url = "https://photon.komoot.io/reverse?lat=" + lat + "&lon=" + lon + "&lang=en";
photonFetcher.command = lowPriorityCmd.concat(curlBaseCmd).concat([url]);
photonFetcher.reqId = reqId;
photonFetcher.running = true;
}
function tryBigDataCloud(lat, lon, reqId) {
const url = "https://api.bigdatacloud.net/data/reverse-geocode-client?latitude=" + lat + "&longitude=" + lon + "&localityLanguage=zh";
bigDataCloudFetcher.command = lowPriorityCmd.concat(curlBaseCmd).concat([url]);
bigDataCloudFetcher.reqId = reqId;
bigDataCloudFetcher.running = true;
}
function fetchWeather(lat, lon) {
if (root.refCount === 0 || !SettingsData.weatherEnabled) {
return;
}
if (!location) {
updateLocation();
return;
if (lat == null || lon == null) {
if (!location) {
updateLocation();
return;
}
lat = location.latitude;
lon = location.longitude;
}
if (weatherFetcher.running) {
@@ -536,7 +645,7 @@ Singleton {
return;
}
const apiUrl = getWeatherApiUrl();
const apiUrl = getWeatherApiUrlForCoords(lat, lon);
if (!apiUrl) {
return;
}
@@ -586,9 +695,123 @@ Singleton {
}
Process {
id: reverseGeocodeFetcher
id: nominatimFetcher
property int reqId: 0
running: false
stdout: StdioCollector {
onStreamFinished: {
if (nominatimFetcher.reqId !== root._geocodeReqId)
return;
const raw = text.trim();
if (!raw || raw[0] !== "{") {
root.tryPhoton(root._pendingCoords.latitude, root._pendingCoords.longitude, root._geocodeReqId);
return;
}
try {
const data = JSON.parse(raw);
const address = data.address || {};
const city = address.hamlet || address.city || address.town || address.village || I18n.tr("Unknown");
const country = address.country || I18n.tr("Unknown");
root.updateLocationCity(city, country);
} catch (e) {
root.tryPhoton(root._pendingCoords.latitude, root._pendingCoords.longitude, root._geocodeReqId);
}
}
}
onExited: exitCode => {
if (nominatimFetcher.reqId !== root._geocodeReqId)
return;
if (exitCode !== 0) {
root.tryPhoton(root._pendingCoords.latitude, root._pendingCoords.longitude, root._geocodeReqId);
}
}
}
Process {
id: photonFetcher
property int reqId: 0
running: false
stdout: StdioCollector {
onStreamFinished: {
if (photonFetcher.reqId !== root._geocodeReqId)
return;
const raw = text.trim();
if (!raw || raw[0] !== "{") {
root.tryBigDataCloud(root._pendingCoords.latitude, root._pendingCoords.longitude, root._geocodeReqId);
return;
}
try {
const data = JSON.parse(raw);
const features = data.features;
if (!features || features.length === 0) {
throw new Error("No Photon results");
}
const props = features[0].properties || {};
const city = props.city || props.town || props.village || props.locality || props.name || I18n.tr("Unknown");
const country = props.country || I18n.tr("Unknown");
root.updateLocationCity(city, country);
} catch (e) {
root.tryBigDataCloud(root._pendingCoords.latitude, root._pendingCoords.longitude, root._geocodeReqId);
}
}
}
onExited: exitCode => {
if (photonFetcher.reqId !== root._geocodeReqId)
return;
if (exitCode !== 0) {
root.tryBigDataCloud(root._pendingCoords.latitude, root._pendingCoords.longitude, root._geocodeReqId);
}
}
}
Process {
id: bigDataCloudFetcher
property int reqId: 0
running: false
stdout: StdioCollector {
onStreamFinished: {
if (bigDataCloudFetcher.reqId !== root._geocodeReqId)
return;
const raw = text.trim();
if (!raw || raw[0] !== "{") {
// All city resolution fallbacks failed; weather is already displayed
return;
}
try {
const data = JSON.parse(raw);
const city = data.city || data.locality || I18n.tr("Unknown");
const country = data.countryName || I18n.tr("Unknown");
root.updateLocationCity(city, country);
} catch (e) {
// All fallbacks failed; keep placeholder city name
}
}
}
onExited: exitCode => {
if (bigDataCloudFetcher.reqId !== root._geocodeReqId)
return;
// Final fallback; no further action needed
}
}
Process {
id: ipLocationFetcher
running: false
command: lowPriorityCmd.concat(curlBaseCmd).concat(["http://ip-api.com/json/"])
stdout: StdioCollector {
onStreamFinished: {
const raw = text.trim();
@@ -599,16 +822,21 @@ Singleton {
try {
const data = JSON.parse(raw);
const address = data.address || {};
root.location = {
city: address.hamlet || address.city || address.town || address.village || I18n.tr("Unknown"),
country: address.country || I18n.tr("Unknown"),
latitude: parseFloat(data.lat),
longitude: parseFloat(data.lon)
};
if (data.status === "fail") {
throw new Error("IP location lookup failed");
}
fetchWeather();
const lat = parseFloat(data.lat);
const lon = parseFloat(data.lon);
const city = data.city;
if (!city || isNaN(lat) || isNaN(lon)) {
throw new Error("Missing or invalid location data");
}
setLocation(lat, lon, city, data.countryName || "");
fetchWeather(lat, lon);
} catch (e) {
root.handleWeatherFailure();
}
@@ -833,8 +1061,10 @@ Singleton {
function onLocationChanged(data) {
if (!SettingsData.useAutoLocation)
return;
if (data.latitude === 0 && data.longitude === 0)
if (data.latitude === 0 && data.longitude === 0) {
root.getLocationFromIP();
return;
}
root.getLocationFromCoords(data.latitude, data.longitude);
}
}