- 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: шлёт предложение встретиться в чат с собеседником - Карточки с эмодзи по типу заведения, расстояние, время работы
144 lines
3.7 KiB
Go
144 lines
3.7 KiB
Go
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
|
||
}
|