buhapp-backend/internal/places/overpass.go
ga 748b4a31fb Sprint 8: 2GIS tiles + places (бары/рестораны) nearby
- internal/places/overpass.go: запрос к Overpass API (OSM), bar/pub/restaurant/cafe/biergarten/nightclub
- internal/handlers/places.go: GET /api/v1/places/nearby (radius, limit)
- POST через application/x-www-form-urlencoded (раньше был broken — query params)
- 2GIS tile layer в map.js (вместо OSM)
- Кнопка 'Показать/Скрыть бары' + список под картой
- Invite to place: шлёт предложение встретиться в чат с собеседником
- Карточки с эмодзи по типу заведения, расстояние, время работы
2026-08-20 21:11:54 +00:00

144 lines
3.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package places
import (
"context"
"encoding/json"
"fmt"
"math"
"net/http"
"net/url"
"strings"
"time"
)
// Place — бар/ресторан/кафе в радиусе
type Place struct {
OSMID int64 `json:"osm_id"`
Name string `json:"name"`
Type string `json:"type"`
Cuisine string `json:"cuisine,omitempty"`
Lat float64 `json:"lat"`
Lng float64 `json:"lng"`
Distance float64 `json:"distance_m"`
Address string `json:"address,omitempty"`
Phone string `json:"phone,omitempty"`
Website string `json:"website,omitempty"`
Hours string `json:"hours,omitempty"`
}
type Client struct {
httpClient *http.Client
endpoint string
}
func New() *Client {
return &Client{
httpClient: &http.Client{Timeout: 8 * time.Second},
endpoint: "https://overpass-api.de/api/interpreter",
}
}
// NearbyPlaces — бары/рестораны/кафе рядом (Overpass API)
func (c *Client) NearbyPlaces(ctx context.Context, lat, lng, radiusKm float64, limit int) ([]Place, error) {
if limit <= 0 || limit > 100 {
limit = 30
}
// bounding box
latDelta := radiusKm / 111.0
lngDelta := radiusKm / (111.0 * math.Cos(lat*math.Pi/180))
south := lat - latDelta
north := lat + latDelta
west := lng - lngDelta
east := lng + lngDelta
query := fmt.Sprintf(`[out:json][timeout:8];
(
node["amenity"~"bar|pub|restaurant|cafe|biergarten|nightclub"](%f,%f,%f,%f);
way["amenity"~"bar|pub|restaurant|cafe|biergarten|nightclub"](%f,%f,%f,%f);
);
out center %d;`, south, west, north, east, south, west, north, east, limit)
data := url.Values{}
data.Set("data", query)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.endpoint, strings.NewReader(data.Encode()))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("User-Agent", "BuhApp/0.7 (https://buhapp.mygoodservice.ru)")
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, fmt.Errorf("overpass status %d", resp.StatusCode)
}
var raw struct {
Elements []struct {
Type string `json:"type"`
ID int64 `json:"id"`
Lat float64 `json:"lat"`
Lon float64 `json:"lon"`
Center *struct {
Lat float64 `json:"lat"`
Lon float64 `json:"lon"`
} `json:"center"`
Tags map[string]string `json:"tags"`
} `json:"elements"`
}
if err := json.NewDecoder(resp.Body).Decode(&raw); err != nil {
return nil, err
}
out := make([]Place, 0, len(raw.Elements))
for _, e := range raw.Elements {
plat, plng := e.Lat, e.Lon
if e.Center != nil {
plat, plng = e.Center.Lat, e.Center.Lon
}
if plat == 0 && plng == 0 {
continue
}
name := e.Tags["name"]
if name == "" {
name = e.Tags["name:ru"]
}
if name == "" {
continue
}
// расстояние от точки юзера
dist := haversine(lat, lng, plat, plng) * 1000
if dist > radiusKm*1000 {
continue
}
out = append(out, Place{
OSMID: e.ID,
Name: name,
Type: e.Tags["amenity"],
Cuisine: e.Tags["cuisine"],
Lat: plat,
Lng: plng,
Distance: dist,
Address: e.Tags["addr:full"],
Phone: e.Tags["phone"],
Website: e.Tags["website"],
Hours: e.Tags["opening_hours"],
})
}
return out, nil
}
func haversine(lat1, lng1, lat2, lng2 float64) float64 {
const R = 6371.0
dLat := (lat2 - lat1) * math.Pi / 180
dLng := (lng2 - lng1) * math.Pi / 180
a := math.Sin(dLat/2)*math.Sin(dLat/2) +
math.Cos(lat1*math.Pi/180)*math.Cos(lat2*math.Pi/180)*math.Sin(dLng/2)*math.Sin(dLng/2)
c := 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a))
return R * c
}