2016-05-06 23:32:32 +00:00
package osm
import (
"bytes"
"encoding/xml"
"fmt"
"io"
"io/ioutil"
2017-06-05 15:23:52 +00:00
"math"
2016-05-06 23:32:32 +00:00
"net/http"
"strconv"
"time"
)
const (
2018-05-19 21:53:28 +00:00
liveAPIBaseURL = "https://api.openstreetmap.org/api/0.6"
devAPIBaseURL = "https://api06.dev.openstreetmap.org/api/0.6"
2016-05-06 23:32:32 +00:00
)
2016-05-07 13:15:34 +00:00
// Client represents an OSM client which is capable of RW operations on the OpenStreetMap
2016-05-06 23:32:32 +00:00
type Client struct {
username string
password string
APIBaseURL string
HTTPClient * http . Client
CurrentUser * User
2017-06-05 15:23:52 +00:00
DebugHTTPRequests bool
2016-05-06 23:32:32 +00:00
}
2016-05-07 13:15:34 +00:00
// 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.
2016-05-06 23:32:32 +00:00
func New ( username , password string , useDevServer bool ) ( * Client , error ) {
out := & Client {
username : username ,
password : password ,
APIBaseURL : liveAPIBaseURL ,
HTTPClient : http . DefaultClient ,
2017-06-05 15:23:52 +00:00
DebugHTTPRequests : false ,
2016-05-06 23:32:32 +00:00
}
if useDevServer {
out . APIBaseURL = devAPIBaseURL
}
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 ) {
2018-05-19 22:01:47 +00:00
var reqBodyBuffer * bytes . Buffer
2017-06-05 15:23:52 +00:00
if body != nil {
2018-05-19 22:01:47 +00:00
reqBodyBuffer = new ( bytes . Buffer )
2017-06-05 15:23:52 +00:00
io . Copy ( reqBodyBuffer , body )
body = bytes . NewBuffer ( reqBodyBuffer . Bytes ( ) )
}
2016-05-06 23:32:32 +00:00
req , _ := http . NewRequest ( method , c . APIBaseURL + path , body )
req . SetBasicAuth ( c . username , c . password )
2017-06-05 15:23:52 +00:00
if method != "GET" {
req . Header . Set ( "Content-Type" , "text/xml; charset=utf-8" )
}
2016-05-06 23:32:32 +00:00
res , err := c . HTTPClient . Do ( req )
if err != nil {
return nil , err
}
2017-06-05 15:23:52 +00:00
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 ] )
2016-05-06 23:32:32 +00:00
}
2017-06-05 15:23:52 +00:00
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 ( ) )
2016-05-06 23:32:32 +00:00
}
2017-06-05 15:23:52 +00:00
if res . StatusCode != http . StatusOK {
return nil , fmt . Errorf ( "OSM API responded with status code %d" , res . StatusCode )
}
return ioutil . NopCloser ( resBody ) , nil
2016-05-06 23:32:32 +00:00
}
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
}
2016-05-07 13:15:34 +00:00
// 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
2016-05-06 23:32:32 +00:00
type Wrap struct {
XMLName xml . Name ` xml:"osm" `
User * User ` xml:"user,omitempty" `
Changesets [ ] * Changeset ` xml:"changeset,omitempty" `
Nodes [ ] * Node ` xml:"node,omitempty" `
}
2016-05-07 13:15:34 +00:00
// Changeset contains information about a changeset in the API. You need to create a changeset before submitting any changes to the API.
2016-05-06 23:32:32 +00:00
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" `
}
2016-05-07 13:15:34 +00:00
// GetMyChangesets retrieves a list of (open) changesets from the API
2016-05-06 23:32:32 +00:00
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 )
}
2016-05-07 13:15:34 +00:00
// CreateChangeset creates a new changeset
2016-05-07 12:49:53 +00:00
func ( c * Client ) CreateChangeset ( ) ( * Changeset , error ) {
2017-06-05 15:23:52 +00:00
body := bytes . NewBufferString ( xml . Header )
enc := xml . NewEncoder ( body )
enc . Indent ( "" , " " )
if err := enc . Encode ( Wrap { Changesets : [ ] * Changeset { { } } } ) ; err != nil {
2016-05-06 23:32:32 +00:00
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
}
2016-05-07 13:15:34 +00:00
// SaveChangeset updates or creates a changeset
2016-05-07 12:49:53 +00:00
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 } }
2017-06-05 15:23:52 +00:00
body := bytes . NewBufferString ( xml . Header )
enc := xml . NewEncoder ( body )
enc . Indent ( "" , " " )
if err := enc . Encode ( data ) ; err != nil {
2016-05-07 12:49:53 +00:00
return err
}
_ , err := c . doPlain ( "PUT" , urlPath , body )
return err
}
2016-05-07 13:15:34 +00:00
// RetrieveMapObjects queries all objects within the passed bounds. You need to ensure the min values are below the max values.
2016-05-06 23:32:32 +00:00
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 )
}
2016-05-07 13:15:34 +00:00
// User contains information about an User in the OpenStreetMap
2016-05-06 23:32:32 +00:00
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" `
}
2016-05-07 13:15:34 +00:00
// Node represents one node in the OpenStreetMap
2016-05-06 23:32:32 +00:00
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" `
}
2016-05-07 13:15:34 +00:00
// SaveNode creates or updates a node with an association to the passed changeset which needs to be open and known to the API.
2016-05-06 23:32:32 +00:00
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 )
}
2016-05-07 12:12:30 +00:00
n . Changeset = cs . ID
data := Wrap { Nodes : [ ] * Node { n } }
2017-06-05 15:23:52 +00:00
body := bytes . NewBufferString ( xml . Header )
enc := xml . NewEncoder ( body )
enc . Indent ( "" , " " )
if err := enc . Encode ( data ) ; err != nil {
2016-05-06 23:32:32 +00:00
return err
}
_ , err := c . doPlain ( "PUT" , urlPath , body )
return err
}
2016-05-07 13:15:34 +00:00
// Tag represents a key-value pair used in all objects inside OpenStreetMap
2016-05-06 23:32:32 +00:00
type Tag struct {
XMLName xml . Name ` xml:"tag" `
Key string ` xml:"k,attr" `
Value string ` xml:"v,attr" `
}