From dd25c4e6cbca7cb809c65533915bda8fa93c2211 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20H=C3=A5rek=20Andreassen?= Date: Wed, 25 Sep 2024 22:09:12 +0200 Subject: [PATCH] feat: Add nominatim MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tim HĂ„rek Andreassen --- internal/nominatim/nominatim.go | 86 ++++++++++++++++++++++++++++ internal/nominatim/nominatim_test.go | 57 ++++++++++++++++++ internal/nominatim/types.go | 39 +++++++++++++ 3 files changed, 182 insertions(+) create mode 100644 internal/nominatim/nominatim.go create mode 100644 internal/nominatim/nominatim_test.go create mode 100644 internal/nominatim/types.go diff --git a/internal/nominatim/nominatim.go b/internal/nominatim/nominatim.go new file mode 100644 index 0000000..33d81f8 --- /dev/null +++ b/internal/nominatim/nominatim.go @@ -0,0 +1,86 @@ +package nominatim + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "strconv" +) + +type Nominatim struct { + siteName string +} + +func New(siteName string) (*Nominatim, error) { + if siteName == "" { + return nil, fmt.Errorf("`siteName` must be defined.") + } + + return &Nominatim{ + siteName: siteName, + }, nil +} + +func (n *Nominatim) Search(q string) (*SearchResults, error) { + url := fmt.Sprintf("https://nominatim.openstreetmap.org/search?q=%s&format=jsonv2", q) + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + + req.Header.Set("User-Agent", n.siteName) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("%s", resp.Status) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + result := SearchResults{} + err = json.Unmarshal(body, &result) + if err != nil { + return nil, err + } + + return &result, nil + +} + +func (n *Nominatim) Lookup(q string) (*Coordinates, error) { + r, err := n.Search(q) + + if err != nil { + return nil, err + } + + if len(*r) == 0 { + return nil, fmt.Errorf("no result") + } + + firstResult := (*r)[0] + lon, err := strconv.ParseFloat(*firstResult.Longitude, 64) + if err != nil { + return nil, err + } + + lat, err := strconv.ParseFloat(*firstResult.Latitude, 64) + if err != nil { + return nil, err + } + + return &Coordinates{ + Latitude: lat, + Longitude: lon, + }, nil +} diff --git a/internal/nominatim/nominatim_test.go b/internal/nominatim/nominatim_test.go new file mode 100644 index 0000000..441c7e3 --- /dev/null +++ b/internal/nominatim/nominatim_test.go @@ -0,0 +1,57 @@ +package nominatim + +import ( + "strconv" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewClient(t *testing.T) { + assert := assert.New(t) + c, err := New("my siteName") + assert.NoError(err) + + assert.Equal("my siteName", c.siteName) + + _, err = New("") + assert.Error(err) +} + +func TestSearch(t *testing.T) { + assert := assert.New(t) + c, err := New("my siteName") + assert.NoError(err) + + r, err := c.Search("bergen") + assert.NoError(err) + + assert.NotEmpty(r) + firstResult := (*r)[0] + lon, err := strconv.ParseFloat(*firstResult.Longitude, 64) + assert.NoError(err) + lat, err := strconv.ParseFloat(*firstResult.Latitude, 64) + assert.NoError(err) + + assert.Equal(5.3259192, lon) + assert.Equal(60.3943055, lat) + + assert.Nil(firstResult.Address) +} + +func TestLookup(t *testing.T) { + assert := assert.New(t) + c, err := New("my siteName") + assert.NoError(err) + + r, err := c.Lookup("bergen") + assert.NoError(err) + + assert.NotEmpty(r) + + assert.Equal(5.3259192, r.Longitude) + assert.Equal(60.3943055, r.Latitude) + + _, err = c.Lookup("") + assert.Error(err) +} diff --git a/internal/nominatim/types.go b/internal/nominatim/types.go new file mode 100644 index 0000000..d12be7f --- /dev/null +++ b/internal/nominatim/types.go @@ -0,0 +1,39 @@ +package nominatim + +type Address struct { + ISO3166 string `json:"ISO3166-2-lvl4"` + Borough string `json:"borough"` + City string `json:"city"` + Country string `json:"country"` + CountryCode string `json:"country_code"` + Neighbourhood string `json:"neighbourhood"` + Postcode string `json:"postcode"` + Road string `json:"road"` + Shop string `json:"shop"` + Suburb string `json:"suburb"` +} + +type SearchResult struct { + Address *Address `json:"address"` + AddressType *string `json:"addresstype"` + BoundingBox *[]string `json:"boundingbox"` + Category *string `json:"category"` + DisplayName *string `json:"display_name"` + Importance *float64 `json:"importance"` + Latitude *string `json:"lat"` + Longitude *string `json:"lon"` + Licence *string `json:"licence"` + Name *string `json:"name"` + OsmId *int `json:"osm_id"` + OsmType *string `json:"osm_type"` + PlaceId *int `json:"place_id"` + PlaceRank *int `json:"place_rank"` + Type *string `json:"type"` +} + +type SearchResults = []SearchResult + +type Coordinates struct { + Latitude float64 `json:"latitude"` + Longitude float64 `json:"longitude"` +}