Pārlūkot izejas kodu

Allow specifying state in weather location

Svilen Markov 1 gadu atpakaļ
vecāks
revīzija
d8d6625478

+ 23 - 0
docs/configuration.md

@@ -579,6 +579,15 @@ Example:
   location: London, United Kingdom
 ```
 
+> [!NOTE]
+>
+> US cities which have common names can have their state specified as the second parameter like such:
+>
+> * Greenville, North Carolina, United States
+> * Greenville, South Carolina, United States
+> * Greenville, Mississippi, United States
+
+
 Preview:
 
 ![](images/weather-widget-preview.png)
@@ -592,6 +601,7 @@ Each bar represents a 2 hour interval. The yellow background represents sunrise
 | location | string | yes |  |
 | units | string | no | metric |
 | hide-location | boolean | no | false |
+| show-area-name | boolean | no | false |
 
 ##### `location`
 The name of the city and country to fetch weather information for. Attempting to launch the applcation with an invalid location will result in an error. You can use the [gecoding API page](https://open-meteo.com/en/docs/geocoding-api) to search for your specific location. Glance will use the first result from the list if there are multiple.
@@ -602,6 +612,19 @@ Whether to show the temperature in celsius or fahrenheit, possible values are `m
 ##### `hide-location`
 Optionally don't display the location name on the widget.
 
+##### `show-area-name`
+Whether to display the state/administrative area in the location name. If set to `true` the location will be displayed as:
+
+```
+Greenville, North Carolina, United States
+```
+
+Otherwise, if set to `false` (which is the default) it'll be displayed as:
+
+```
+Greenville, United States
+```
+
 ### Monitor
 Display a list of sites and whether they are reachable (online) or not. This is determined by sending a HEAD request to the specified URL, if the response is 200 then the site is OK. The time it took to receive a response is also shown in milliseconds.
 

+ 1 - 1
internal/assets/templates/weather.html

@@ -23,7 +23,7 @@
 {{ if not .HideLocation }}
 <div class="flex items-center justify-center margin-top-15 gap-7 size-h5">
     <div class="location-icon"></div>
-    <div class="text-truncate">{{ .Place.Name }}, {{ .Place.Country }}</div>
+    <div class="text-truncate">{{ .Place.Name }},{{ if .ShowAreaName }} {{ .Place.Area }},{{ end }} {{ .Place.Country }}</div>
 </div>
 {{ end }}
 {{ end }}

+ 54 - 2
internal/feed/openmeteo.go

@@ -6,6 +6,7 @@ import (
 	"net/http"
 	"net/url"
 	"slices"
+	"strings"
 	"time"
 
 	_ "time/tzdata"
@@ -17,6 +18,7 @@ type PlacesResponseJson struct {
 
 type PlaceJson struct {
 	Name      string
+	Area      string `json:"admin1"`
 	Latitude  float64
 	Longitude float64
 	Timezone  string
@@ -48,8 +50,41 @@ type weatherColumn struct {
 	HasPrecipitation bool
 }
 
+var commonCountryAbbreviations = map[string]string{
+	"US":  "United States",
+	"USA": "United States",
+	"UK":  "United Kingdom",
+}
+
+func expandCountryAbbreviations(name string) string {
+	if expanded, ok := commonCountryAbbreviations[strings.TrimSpace(name)]; ok {
+		return expanded
+	}
+
+	return name
+}
+
+// Separates the location that Open Meteo accepts from the administrative area
+// which can then be used to filter to the correct place after the list of places
+// has been retrieved. Also expands abbreviations since Open Meteo does not accept
+// country names like "US", "USA" and "UK"
+func parsePlaceName(name string) (string, string) {
+	parts := strings.Split(name, ",")
+
+	if len(parts) == 1 {
+		return name, ""
+	}
+
+	if len(parts) == 2 {
+		return parts[0] + ", " + expandCountryAbbreviations(parts[1]), ""
+	}
+
+	return parts[0] + ", " + expandCountryAbbreviations(parts[2]), strings.TrimSpace(parts[1])
+}
+
 func FetchPlaceFromName(location string) (*PlaceJson, error) {
-	requestUrl := fmt.Sprintf("https://geocoding-api.open-meteo.com/v1/search?name=%s&count=1&language=en&format=json", url.QueryEscape(location))
+	location, area := parsePlaceName(location)
+	requestUrl := fmt.Sprintf("https://geocoding-api.open-meteo.com/v1/search?name=%s&count=10&language=en&format=json", url.QueryEscape(location))
 	request, _ := http.NewRequest("GET", requestUrl, nil)
 	responseJson, err := decodeJsonFromRequest[PlacesResponseJson](defaultClient, request)
 
@@ -61,7 +96,24 @@ func FetchPlaceFromName(location string) (*PlaceJson, error) {
 		return nil, fmt.Errorf("no places found for %s", location)
 	}
 
-	place := &responseJson.Results[0]
+	var place *PlaceJson
+
+	if area != "" {
+		area = strings.ToLower(area)
+
+		for i := range responseJson.Results {
+			if strings.ToLower(responseJson.Results[i].Area) == area {
+				place = &responseJson.Results[i]
+				break
+			}
+		}
+
+		if place == nil {
+			return nil, fmt.Errorf("no place found for %s in %s", location, area)
+		}
+	} else {
+		place = &responseJson.Results[0]
+	}
 
 	loc, err := time.LoadLocation(place.Timezone)
 

+ 1 - 0
internal/widget/weather.go

@@ -12,6 +12,7 @@ import (
 type Weather struct {
 	widgetBase   `yaml:",inline"`
 	Location     string          `yaml:"location"`
+	ShowAreaName bool            `yaml:"show-area-name"`
 	HideLocation bool            `yaml:"hide-location"`
 	Units        string          `yaml:"units"`
 	Place        *feed.PlaceJson `yaml:"-"`