1
0
Fork 0
mirror of https://github.com/Luzifer/gpxhydrant.git synced 2024-11-08 15:30:00 +00:00
gpxhydrant/osm/osm.go
Knut Ahlers 33264df876
Allow specifying API endpoint to contact
in order to enable testing against different API deployments like
master, tomh, ...

Signed-off-by: Knut Ahlers <knut@ahlers.me>
2018-06-17 12:46:02 +02:00

305 lines
8.1 KiB
Go

package osm
import (
"bytes"
"encoding/xml"
"errors"
"fmt"
"io"
"io/ioutil"
"math"
"net/http"
"strconv"
"time"
)
const (
liveAPIBaseURL = "https://api.openstreetmap.org/api/0.6"
devAPIBaseURL = "https://api06.dev.openstreetmap.org/api/0.6"
)
// Client represents an OSM client which is capable of RW operations on the OpenStreetMap
type Client struct {
username string
password string
APIBaseURL string
HTTPClient *http.Client
CurrentUser *User
DebugHTTPRequests bool
}
// New instantiates a new client and retrieves information about the
// current user. Set useDevServer to true to change the API URL to the
// api06.dev.openstreetmap.org server.
func New(username, password string, useDevServer bool) (*Client, error) {
if useDevServer {
return NewWithAPIEndpoint(username, password, devAPIBaseURL)
}
return NewWithAPIEndpoint(username, password, liveAPIBaseURL)
}
// NewWithAPIEndpoint instantiates a new client and retrieves
// information about the current user. Set apiEndpoint to your desired API
// endpoint (e.g. https://api06.dev.openstreetmap.org/api/0.6)
func NewWithAPIEndpoint(username, password, apiEndpoint string) (*Client, error) {
out := &Client{
username: username,
password: password,
APIBaseURL: apiEndpoint,
HTTPClient: http.DefaultClient,
DebugHTTPRequests: false,
}
if apiEndpoint == "" {
return nil, errors.New("No API endpoint given")
}
u := &Wrap{User: &User{}}
if err := out.doParse("GET", "/user/details", nil, u); err != nil {
return nil, err
}
out.CurrentUser = u.User
return out, nil
}
func (c *Client) doPlain(method, path string, body io.Reader) (string, error) {
responseBody, err := c.do(method, path, body)
if err != nil {
return "", err
}
defer responseBody.Close()
data, err := ioutil.ReadAll(responseBody)
if err != nil {
return "", err
}
return string(data), nil
}
func (c *Client) do(method, path string, body io.Reader) (io.ReadCloser, error) {
var reqBodyBuffer *bytes.Buffer
if body != nil {
reqBodyBuffer = new(bytes.Buffer)
io.Copy(reqBodyBuffer, body)
body = bytes.NewBuffer(reqBodyBuffer.Bytes())
}
req, _ := http.NewRequest(method, c.APIBaseURL+path, body)
req.SetBasicAuth(c.username, c.password)
if method != "GET" {
req.Header.Set("Content-Type", "text/xml; charset=utf-8")
}
res, err := c.HTTPClient.Do(req)
if err != nil {
return nil, err
}
resBody := bytes.NewBufferString("")
io.Copy(resBody, res.Body)
if c.DebugHTTPRequests {
buf := bytes.NewBufferString("")
fmt.Fprintf(buf, "---------- REQUEST ----------\n")
fmt.Fprintf(buf, "%s %s\n", method, req.URL.String())
for k, v := range req.Header {
fmt.Fprintf(buf, "%s: %s\n", k, v[0])
}
fmt.Fprintf(buf, "\n")
if reqBodyBuffer != nil {
trunc := int(math.Min(float64(reqBodyBuffer.Len()), 1024))
fmt.Fprintf(buf, "%s\n\n", reqBodyBuffer.String()[0:trunc])
}
fmt.Fprintf(buf, "---------- RESPONSE ----------\n")
for k, v := range res.Header {
fmt.Fprintf(buf, "%s: %s\n", k, v[0])
}
fmt.Fprintf(buf, "\n")
trunc := int(math.Min(float64(resBody.Len()), 1024))
fmt.Fprintf(buf, "%s\n", resBody.String()[0:trunc])
fmt.Println(buf.String())
}
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("OSM API responded with status code %d", res.StatusCode)
}
return ioutil.NopCloser(resBody), nil
}
func (c *Client) doParse(method, path string, body io.Reader, output interface{}) error {
responseBody, err := c.do(method, path, body)
if err != nil {
return err
}
defer responseBody.Close()
if output != nil {
return xml.NewDecoder(responseBody).Decode(output)
}
return nil
}
// Wrap is a mostly internal used struct which holds requests to / responses from the API.
// You will get a Wrap object when querying map objects from the API
type Wrap struct {
XMLName xml.Name `xml:"osm"`
User *User `xml:"user,omitempty"`
Changesets []*Changeset `xml:"changeset,omitempty"`
Nodes []*Node `xml:"node,omitempty"`
}
// Changeset contains information about a changeset in the API. You need to create a changeset before submitting any changes to the API.
type Changeset struct {
XMLName xml.Name `xml:"changeset"`
ID int64 `xml:"id,attr,omitempty"`
User string `xml:"user,attr,omitempty"`
UID int64 `xml:"uid,attr,omitempty"`
CreatedAt time.Time `xml:"created_at,attr,omitempty"`
ClosedAt time.Time `xml:"closed_at,attr,omitempty"`
Open bool `xml:"open,attr,omitempty"`
MinLat float64 `xml:"min_lat,attr,omitempty"`
MinLon float64 `xml:"min_lon,attr,omitempty"`
MaxLat float64 `xml:"max_lat,attr,omitempty"`
MaxLon float64 `xml:"max_lon,attr,omitempty"`
CommentCount int64 `xml:"comments_count,attr,omitempty"`
Tags []Tag `xml:"tag"`
}
// GetMyChangesets retrieves a list of (open) changesets from the API
func (c *Client) GetMyChangesets(onlyOpen bool) ([]*Changeset, error) {
urlPath := fmt.Sprintf("/changesets?user=%d&open=%s", c.CurrentUser.ID, strconv.FormatBool(onlyOpen))
r := &Wrap{}
return r.Changesets, c.doParse("GET", urlPath, nil, r)
}
// CreateChangeset creates a new changeset
func (c *Client) CreateChangeset() (*Changeset, error) {
body := bytes.NewBufferString(xml.Header)
enc := xml.NewEncoder(body)
enc.Indent("", " ")
if err := enc.Encode(Wrap{Changesets: []*Changeset{{}}}); err != nil {
return nil, err
}
res, err := c.doPlain("PUT", "/changeset/create", body)
if err != nil {
return nil, err
}
cs := &Wrap{}
if err := c.doParse("GET", fmt.Sprintf("/changeset/%s", res), nil, cs); err != nil {
return nil, err
}
if len(cs.Changesets) != 1 {
return nil, fmt.Errorf("Unable to retrieve new changeset #%s", res)
}
return cs.Changesets[0], nil
}
// SaveChangeset updates or creates a changeset
func (c *Client) SaveChangeset(cs *Changeset) error {
urlPath := "/changeset/create"
if cs.ID > 0 {
urlPath = fmt.Sprintf("/changeset/%d", cs.ID)
}
data := Wrap{Changesets: []*Changeset{cs}}
body := bytes.NewBufferString(xml.Header)
enc := xml.NewEncoder(body)
enc.Indent("", " ")
if err := enc.Encode(data); err != nil {
return err
}
_, err := c.doPlain("PUT", urlPath, body)
return err
}
// RetrieveMapObjects queries all objects within the passed bounds. You need to ensure the min values are below the max values.
func (c *Client) RetrieveMapObjects(minLat, minLon, maxLat, maxLon float64) (*Wrap, error) {
urlPath := fmt.Sprintf("/map?bbox=%.7f,%.7f,%.7f,%.7f", minLat, minLon, maxLat, maxLon)
res := &Wrap{}
return res, c.doParse("GET", urlPath, nil, res)
}
// User contains information about an User in the OpenStreetMap
type User struct {
XMLName xml.Name `xml:"user"`
ID int64 `xml:"id,attr"`
DisplayName string `xml:"display_name,attr"`
AccountCreated time.Time `xml:"account_created,attr"`
Description string `xml:"description"`
}
// Node represents one node in the OpenStreetMap
type Node struct {
XMLName xml.Name `xml:"node"`
ID int64 `xml:"id,attr,omitempty"`
Version int64 `xml:"version,attr,omitempty"`
Changeset int64 `xml:"changeset,attr,omitempty"`
User string `xml:"user,attr,omitempty"`
UID int64 `xml:"uid,attr,omitempty"`
Latitude float64 `xml:"lat,attr"`
Longitude float64 `xml:"lon,attr"`
Tags []Tag `xml:"tag"`
}
// SaveNode creates or updates a node with an association to the passed changeset which needs to be open and known to the API.
func (c *Client) SaveNode(n *Node, cs *Changeset) error {
if n.ID > 0 && n.Version == 0 {
return fmt.Errorf("When an ID is set the version must be present")
}
urlPath := "/node/create"
if n.ID > 0 {
urlPath = fmt.Sprintf("/node/%d", n.ID)
}
n.Changeset = cs.ID
data := Wrap{Nodes: []*Node{n}}
body := bytes.NewBufferString(xml.Header)
enc := xml.NewEncoder(body)
enc.Indent("", " ")
if err := enc.Encode(data); err != nil {
return err
}
_, err := c.doPlain("PUT", urlPath, body)
return err
}
// Tag represents a key-value pair used in all objects inside OpenStreetMap
type Tag struct {
XMLName xml.Name `xml:"tag"`
Key string `xml:"k,attr"`
Value string `xml:"v,attr"`
}