|
|
@@ -0,0 +1,63 @@
|
|
|
+package handlers
|
|
|
+
|
|
|
+import (
|
|
|
+ "encoding/json"
|
|
|
+ "io"
|
|
|
+ "net/http"
|
|
|
+ "net/url"
|
|
|
+ "time"
|
|
|
+)
|
|
|
+
|
|
|
+const nominatimURL = "https://nominatim.openstreetmap.org/reverse"
|
|
|
+
|
|
|
+var geocodeClient = &http.Client{Timeout: 5 * time.Second}
|
|
|
+
|
|
|
+type nominatimResponse struct {
|
|
|
+ DisplayName string `json:"display_name"`
|
|
|
+}
|
|
|
+
|
|
|
+func GeocodeReverse(w http.ResponseWriter, r *http.Request) {
|
|
|
+ lat := r.URL.Query().Get("lat")
|
|
|
+ lon := r.URL.Query().Get("lon")
|
|
|
+ if lat == "" || lon == "" {
|
|
|
+ writeError(w, http.StatusBadRequest, "lat and lon are required", nil)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ u, _ := url.Parse(nominatimURL)
|
|
|
+ q := u.Query()
|
|
|
+ q.Set("lat", lat)
|
|
|
+ q.Set("lon", lon)
|
|
|
+ q.Set("format", "json")
|
|
|
+ q.Set("accept-language", "ru")
|
|
|
+ u.RawQuery = q.Encode()
|
|
|
+
|
|
|
+ req, _ := http.NewRequestWithContext(r.Context(), http.MethodGet, u.String(), nil)
|
|
|
+ req.Header.Set("User-Agent", "Photoplaces/1.0")
|
|
|
+
|
|
|
+ resp, err := geocodeClient.Do(req)
|
|
|
+ if err != nil {
|
|
|
+ writeError(w, http.StatusBadGateway, "geocoding service unavailable", err)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ defer resp.Body.Close()
|
|
|
+
|
|
|
+ if resp.StatusCode != http.StatusOK {
|
|
|
+ body, _ := io.ReadAll(resp.Body)
|
|
|
+ writeJSON(w, http.StatusOK, map[string]interface{}{
|
|
|
+ "display_name": nil,
|
|
|
+ "raw_error": string(body),
|
|
|
+ })
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ var data nominatimResponse
|
|
|
+ if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
|
|
|
+ writeError(w, http.StatusInternalServerError, "failed to parse geocoding response", err)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ writeJSON(w, http.StatusOK, map[string]interface{}{
|
|
|
+ "display_name": data.DisplayName,
|
|
|
+ })
|
|
|
+}
|