upstream sync

This commit is contained in:
Senad Uka
2023-10-12 05:23:34 +02:00
parent d3011d77ff
commit 7369739bdd
21 changed files with 1686 additions and 85 deletions

View File

@@ -0,0 +1,33 @@
package location
import (
"context"
"gitlab.com/pactual1/backend/models"
"gitlab.com/pactual1/backend/services/location/mapbox_lib"
)
type service struct {
accessToken string
}
type Service interface {
SearchPlace(context.Context, string) ([]*models.Place, error)
}
func NewService(accessToken string) Service {
return service{accessToken: accessToken}
}
func (s service) SearchPlace(ctx context.Context, query string) ([]*models.Place, error) {
geocoder := mapbox.NewFastHttpGeocoder(mapbox.AccessToken(s.accessToken))
resp, err := geocoder.ForwardGeocode(ctx, &mapbox.ForwardGeocodeRequest{SearchText: query})
if err != nil {
return nil, err
}
var places []*models.Place
for _, feature := range resp.Features {
places = append(places, &models.Place{Text: feature.Text, Coordinates: feature.Geometry.Coordinates})
}
return places, nil
}

View File

@@ -0,0 +1,7 @@
package mapbox
// Client covers all Mabpox API
type Client interface {
// Geocoder covers forward and reverse geocoding mapbox API
Geocoder
}

View File

@@ -0,0 +1,103 @@
package mapbox
import (
"context"
"os"
"github.com/valyala/fasthttp"
)
const (
defaultAPI = "https://api.mapbox.com"
)
// Option allows gradually modify config
type Option func(c config) config
type config struct {
accessToken string
rootAPI string
client FastHttpClient
logger Logger
// requestLogger will be called instead of testLogger if set.
requestLogger func(ctx context.Context) Logger
accessTokenGetValue []byte
geocodeEndpoint string
}
// withEnv overwrites config values with env is not empty
func (c config) withEnv() config {
at := os.Getenv("MAPBOX_ACCESS_TOKEN")
if at != "" {
c.accessToken = at
}
return c
}
// prepare prebuilds some reused api parts like access token http get value
func (c config) prepare() config {
c.accessTokenGetValue = []byte(questionMark + access_token + string(equalMark) + c.accessToken)
return c
}
func newConfig() config {
return config{
rootAPI: defaultAPI,
client: &fasthttp.Client{},
geocodeEndpoint: "mapbox.places",
}
}
// Log used to debug traces and to log errors.
func Log(l Logger) Option {
return func(c config) config {
c.logger = l
return c
}
}
// RequestLogger sets the way testLogger could be extracted from request context.
// If set will be used instead of Log.
func RequestLogger(extract func(ctx context.Context) Logger) Option {
return func(c config) config {
c.requestLogger = extract
return c
}
}
// AccessToken sets access_token get param.
// Could be set with MAPBOX_ACCESS_TOKEN too.
func AccessToken(at string) Option {
return func(c config) config {
c.accessToken = at
return c
}
}
// RootAPI allows to change root api address.
// default to https://api.mapbox.com
func RootAPI(rootAPI string) Option {
return func(c config) config {
c.rootAPI = rootAPI
return c
}
}
// HttpClient allows to change default fast http client
func HttpClient(c FastHttpClient) Option {
return func(fhc config) config {
fhc.client = c
return fhc
}
}
// GeocodeEndpoint sets geocode endpoint.
// could be set to mapbox.places-permanent, defualt to mapbox.places
func GeocodeEndpoint(endpoint string) Option {
return func(c config) config {
c.geocodeEndpoint = endpoint
return c
}
}

View File

@@ -0,0 +1,35 @@
package mapbox
type (
Feature struct {
ID string `json:"id"`
Type string `json:"type"`
PlaceType []string `json:"place_type"`
Relevance float64 `json:"relevance"`
Properties Properties `json:"properties"`
Text string `json:"text"`
PlaceName string `json:"place_name"`
Center []float64 `json:"center"`
Geometry Geometry `json:"geometry"`
Address string `json:"address"`
Context []Context `json:"context"`
BoundingBox []float64 `json:"bbox"`
}
Properties struct {
Accuracy string `json:"accuracy"`
ShortCode string `json:"short_code"`
}
Geometry struct {
Type string `json:"type"`
Coordinates []float64 `json:"coordinates"`
}
Context struct {
ID string `json:"id"`
Text string `json:"text"`
Wikidata string `json:"wikidata"`
ShortCode string `json:"short_code"`
}
)

View File

@@ -0,0 +1,555 @@
// Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT.
package mapbox
import (
json "encoding/json"
easyjson "github.com/mailru/easyjson"
jlexer "github.com/mailru/easyjson/jlexer"
jwriter "github.com/mailru/easyjson/jwriter"
)
// suppress unused package warning
var (
_ *json.RawMessage
_ *jlexer.Lexer
_ *jwriter.Writer
_ easyjson.Marshaler
)
func easyjson3e8ab7adDecodeGithubComHumansNetMapboxSdkGoMapbox(in *jlexer.Lexer, out *Properties) {
isTopLevel := in.IsStart()
if in.IsNull() {
if isTopLevel {
in.Consumed()
}
in.Skip()
return
}
in.Delim('{')
for !in.IsDelim('}') {
key := in.UnsafeString()
in.WantColon()
if in.IsNull() {
in.Skip()
in.WantComma()
continue
}
switch key {
case "accuracy":
out.Accuracy = string(in.String())
case "short_code":
out.ShortCode = string(in.String())
default:
in.SkipRecursive()
}
in.WantComma()
}
in.Delim('}')
if isTopLevel {
in.Consumed()
}
}
func easyjson3e8ab7adEncodeGithubComHumansNetMapboxSdkGoMapbox(out *jwriter.Writer, in Properties) {
out.RawByte('{')
first := true
_ = first
{
const prefix string = ",\"accuracy\":"
out.RawString(prefix[1:])
out.String(string(in.Accuracy))
}
{
const prefix string = ",\"short_code\":"
out.RawString(prefix)
out.String(string(in.ShortCode))
}
out.RawByte('}')
}
// MarshalJSON supports json.Marshaler interface
func (v Properties) MarshalJSON() ([]byte, error) {
w := jwriter.Writer{}
easyjson3e8ab7adEncodeGithubComHumansNetMapboxSdkGoMapbox(&w, v)
return w.Buffer.BuildBytes(), w.Error
}
// MarshalEasyJSON supports easyjson.Marshaler interface
func (v Properties) MarshalEasyJSON(w *jwriter.Writer) {
easyjson3e8ab7adEncodeGithubComHumansNetMapboxSdkGoMapbox(w, v)
}
// UnmarshalJSON supports json.Unmarshaler interface
func (v *Properties) UnmarshalJSON(data []byte) error {
r := jlexer.Lexer{Data: data}
easyjson3e8ab7adDecodeGithubComHumansNetMapboxSdkGoMapbox(&r, v)
return r.Error()
}
// UnmarshalEasyJSON supports easyjson.Unmarshaler interface
func (v *Properties) UnmarshalEasyJSON(l *jlexer.Lexer) {
easyjson3e8ab7adDecodeGithubComHumansNetMapboxSdkGoMapbox(l, v)
}
func easyjson3e8ab7adDecodeGithubComHumansNetMapboxSdkGoMapbox1(in *jlexer.Lexer, out *Geometry) {
isTopLevel := in.IsStart()
if in.IsNull() {
if isTopLevel {
in.Consumed()
}
in.Skip()
return
}
in.Delim('{')
for !in.IsDelim('}') {
key := in.UnsafeString()
in.WantColon()
if in.IsNull() {
in.Skip()
in.WantComma()
continue
}
switch key {
case "type":
out.Type = string(in.String())
case "coordinates":
if in.IsNull() {
in.Skip()
out.Coordinates = nil
} else {
in.Delim('[')
if out.Coordinates == nil {
if !in.IsDelim(']') {
out.Coordinates = make([]float64, 0, 8)
} else {
out.Coordinates = []float64{}
}
} else {
out.Coordinates = (out.Coordinates)[:0]
}
for !in.IsDelim(']') {
var v1 float64
v1 = float64(in.Float64())
out.Coordinates = append(out.Coordinates, v1)
in.WantComma()
}
in.Delim(']')
}
default:
in.SkipRecursive()
}
in.WantComma()
}
in.Delim('}')
if isTopLevel {
in.Consumed()
}
}
func easyjson3e8ab7adEncodeGithubComHumansNetMapboxSdkGoMapbox1(out *jwriter.Writer, in Geometry) {
out.RawByte('{')
first := true
_ = first
{
const prefix string = ",\"type\":"
out.RawString(prefix[1:])
out.String(string(in.Type))
}
{
const prefix string = ",\"coordinates\":"
out.RawString(prefix)
if in.Coordinates == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 {
out.RawString("null")
} else {
out.RawByte('[')
for v2, v3 := range in.Coordinates {
if v2 > 0 {
out.RawByte(',')
}
out.Float64(float64(v3))
}
out.RawByte(']')
}
}
out.RawByte('}')
}
// MarshalJSON supports json.Marshaler interface
func (v Geometry) MarshalJSON() ([]byte, error) {
w := jwriter.Writer{}
easyjson3e8ab7adEncodeGithubComHumansNetMapboxSdkGoMapbox1(&w, v)
return w.Buffer.BuildBytes(), w.Error
}
// MarshalEasyJSON supports easyjson.Marshaler interface
func (v Geometry) MarshalEasyJSON(w *jwriter.Writer) {
easyjson3e8ab7adEncodeGithubComHumansNetMapboxSdkGoMapbox1(w, v)
}
// UnmarshalJSON supports json.Unmarshaler interface
func (v *Geometry) UnmarshalJSON(data []byte) error {
r := jlexer.Lexer{Data: data}
easyjson3e8ab7adDecodeGithubComHumansNetMapboxSdkGoMapbox1(&r, v)
return r.Error()
}
// UnmarshalEasyJSON supports easyjson.Unmarshaler interface
func (v *Geometry) UnmarshalEasyJSON(l *jlexer.Lexer) {
easyjson3e8ab7adDecodeGithubComHumansNetMapboxSdkGoMapbox1(l, v)
}
func easyjson3e8ab7adDecodeGithubComHumansNetMapboxSdkGoMapbox2(in *jlexer.Lexer, out *Feature) {
isTopLevel := in.IsStart()
if in.IsNull() {
if isTopLevel {
in.Consumed()
}
in.Skip()
return
}
in.Delim('{')
for !in.IsDelim('}') {
key := in.UnsafeString()
in.WantColon()
if in.IsNull() {
in.Skip()
in.WantComma()
continue
}
switch key {
case "id":
out.ID = string(in.String())
case "type":
out.Type = string(in.String())
case "place_type":
if in.IsNull() {
in.Skip()
out.PlaceType = nil
} else {
in.Delim('[')
if out.PlaceType == nil {
if !in.IsDelim(']') {
out.PlaceType = make([]string, 0, 4)
} else {
out.PlaceType = []string{}
}
} else {
out.PlaceType = (out.PlaceType)[:0]
}
for !in.IsDelim(']') {
var v4 string
v4 = string(in.String())
out.PlaceType = append(out.PlaceType, v4)
in.WantComma()
}
in.Delim(']')
}
case "relevance":
out.Relevance = float64(in.Float64())
case "properties":
(out.Properties).UnmarshalEasyJSON(in)
case "text":
out.Text = string(in.String())
case "place_name":
out.PlaceName = string(in.String())
case "center":
if in.IsNull() {
in.Skip()
out.Center = nil
} else {
in.Delim('[')
if out.Center == nil {
if !in.IsDelim(']') {
out.Center = make([]float64, 0, 8)
} else {
out.Center = []float64{}
}
} else {
out.Center = (out.Center)[:0]
}
for !in.IsDelim(']') {
var v5 float64
v5 = float64(in.Float64())
out.Center = append(out.Center, v5)
in.WantComma()
}
in.Delim(']')
}
case "geometry":
(out.Geometry).UnmarshalEasyJSON(in)
case "address":
out.Address = string(in.String())
case "context":
if in.IsNull() {
in.Skip()
out.Context = nil
} else {
in.Delim('[')
if out.Context == nil {
if !in.IsDelim(']') {
out.Context = make([]Context, 0, 1)
} else {
out.Context = []Context{}
}
} else {
out.Context = (out.Context)[:0]
}
for !in.IsDelim(']') {
var v6 Context
(v6).UnmarshalEasyJSON(in)
out.Context = append(out.Context, v6)
in.WantComma()
}
in.Delim(']')
}
case "bbox":
if in.IsNull() {
in.Skip()
out.BoundingBox = nil
} else {
in.Delim('[')
if out.BoundingBox == nil {
if !in.IsDelim(']') {
out.BoundingBox = make([]float64, 0, 8)
} else {
out.BoundingBox = []float64{}
}
} else {
out.BoundingBox = (out.BoundingBox)[:0]
}
for !in.IsDelim(']') {
var v7 float64
v7 = float64(in.Float64())
out.BoundingBox = append(out.BoundingBox, v7)
in.WantComma()
}
in.Delim(']')
}
default:
in.SkipRecursive()
}
in.WantComma()
}
in.Delim('}')
if isTopLevel {
in.Consumed()
}
}
func easyjson3e8ab7adEncodeGithubComHumansNetMapboxSdkGoMapbox2(out *jwriter.Writer, in Feature) {
out.RawByte('{')
first := true
_ = first
{
const prefix string = ",\"id\":"
out.RawString(prefix[1:])
out.String(string(in.ID))
}
{
const prefix string = ",\"type\":"
out.RawString(prefix)
out.String(string(in.Type))
}
{
const prefix string = ",\"place_type\":"
out.RawString(prefix)
if in.PlaceType == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 {
out.RawString("null")
} else {
out.RawByte('[')
for v8, v9 := range in.PlaceType {
if v8 > 0 {
out.RawByte(',')
}
out.String(string(v9))
}
out.RawByte(']')
}
}
{
const prefix string = ",\"relevance\":"
out.RawString(prefix)
out.Float64(float64(in.Relevance))
}
{
const prefix string = ",\"properties\":"
out.RawString(prefix)
(in.Properties).MarshalEasyJSON(out)
}
{
const prefix string = ",\"text\":"
out.RawString(prefix)
out.String(string(in.Text))
}
{
const prefix string = ",\"place_name\":"
out.RawString(prefix)
out.String(string(in.PlaceName))
}
{
const prefix string = ",\"center\":"
out.RawString(prefix)
if in.Center == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 {
out.RawString("null")
} else {
out.RawByte('[')
for v10, v11 := range in.Center {
if v10 > 0 {
out.RawByte(',')
}
out.Float64(float64(v11))
}
out.RawByte(']')
}
}
{
const prefix string = ",\"geometry\":"
out.RawString(prefix)
(in.Geometry).MarshalEasyJSON(out)
}
{
const prefix string = ",\"address\":"
out.RawString(prefix)
out.String(string(in.Address))
}
{
const prefix string = ",\"context\":"
out.RawString(prefix)
if in.Context == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 {
out.RawString("null")
} else {
out.RawByte('[')
for v12, v13 := range in.Context {
if v12 > 0 {
out.RawByte(',')
}
(v13).MarshalEasyJSON(out)
}
out.RawByte(']')
}
}
{
const prefix string = ",\"bbox\":"
out.RawString(prefix)
if in.BoundingBox == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 {
out.RawString("null")
} else {
out.RawByte('[')
for v14, v15 := range in.BoundingBox {
if v14 > 0 {
out.RawByte(',')
}
out.Float64(float64(v15))
}
out.RawByte(']')
}
}
out.RawByte('}')
}
// MarshalJSON supports json.Marshaler interface
func (v Feature) MarshalJSON() ([]byte, error) {
w := jwriter.Writer{}
easyjson3e8ab7adEncodeGithubComHumansNetMapboxSdkGoMapbox2(&w, v)
return w.Buffer.BuildBytes(), w.Error
}
// MarshalEasyJSON supports easyjson.Marshaler interface
func (v Feature) MarshalEasyJSON(w *jwriter.Writer) {
easyjson3e8ab7adEncodeGithubComHumansNetMapboxSdkGoMapbox2(w, v)
}
// UnmarshalJSON supports json.Unmarshaler interface
func (v *Feature) UnmarshalJSON(data []byte) error {
r := jlexer.Lexer{Data: data}
easyjson3e8ab7adDecodeGithubComHumansNetMapboxSdkGoMapbox2(&r, v)
return r.Error()
}
// UnmarshalEasyJSON supports easyjson.Unmarshaler interface
func (v *Feature) UnmarshalEasyJSON(l *jlexer.Lexer) {
easyjson3e8ab7adDecodeGithubComHumansNetMapboxSdkGoMapbox2(l, v)
}
func easyjson3e8ab7adDecodeGithubComHumansNetMapboxSdkGoMapbox3(in *jlexer.Lexer, out *Context) {
isTopLevel := in.IsStart()
if in.IsNull() {
if isTopLevel {
in.Consumed()
}
in.Skip()
return
}
in.Delim('{')
for !in.IsDelim('}') {
key := in.UnsafeString()
in.WantColon()
if in.IsNull() {
in.Skip()
in.WantComma()
continue
}
switch key {
case "id":
out.ID = string(in.String())
case "text":
out.Text = string(in.String())
case "wikidata":
out.Wikidata = string(in.String())
case "short_code":
out.ShortCode = string(in.String())
default:
in.SkipRecursive()
}
in.WantComma()
}
in.Delim('}')
if isTopLevel {
in.Consumed()
}
}
func easyjson3e8ab7adEncodeGithubComHumansNetMapboxSdkGoMapbox3(out *jwriter.Writer, in Context) {
out.RawByte('{')
first := true
_ = first
{
const prefix string = ",\"id\":"
out.RawString(prefix[1:])
out.String(string(in.ID))
}
{
const prefix string = ",\"text\":"
out.RawString(prefix)
out.String(string(in.Text))
}
{
const prefix string = ",\"wikidata\":"
out.RawString(prefix)
out.String(string(in.Wikidata))
}
{
const prefix string = ",\"short_code\":"
out.RawString(prefix)
out.String(string(in.ShortCode))
}
out.RawByte('}')
}
// MarshalJSON supports json.Marshaler interface
func (v Context) MarshalJSON() ([]byte, error) {
w := jwriter.Writer{}
easyjson3e8ab7adEncodeGithubComHumansNetMapboxSdkGoMapbox3(&w, v)
return w.Buffer.BuildBytes(), w.Error
}
// MarshalEasyJSON supports easyjson.Marshaler interface
func (v Context) MarshalEasyJSON(w *jwriter.Writer) {
easyjson3e8ab7adEncodeGithubComHumansNetMapboxSdkGoMapbox3(w, v)
}
// UnmarshalJSON supports json.Unmarshaler interface
func (v *Context) UnmarshalJSON(data []byte) error {
r := jlexer.Lexer{Data: data}
easyjson3e8ab7adDecodeGithubComHumansNetMapboxSdkGoMapbox3(&r, v)
return r.Error()
}
// UnmarshalEasyJSON supports easyjson.Unmarshaler interface
func (v *Context) UnmarshalEasyJSON(l *jlexer.Lexer) {
easyjson3e8ab7adDecodeGithubComHumansNetMapboxSdkGoMapbox3(l, v)
}

View File

@@ -0,0 +1,421 @@
package mapbox
import (
"context"
"fmt"
"net/http"
"strconv"
"strings"
"github.com/pkg/errors"
"github.com/valyala/fasthttp"
)
const (
limit = "limit"
types = "types"
country = "country"
language = "language"
reverseMode = "reverseMode"
autocomplete = "autocomplete"
fuzzymatch = "fuzzymatch"
bbox = "bbox"
proximity = "proximity"
routing = "routing"
trueStr = "true"
oneStr = "1"
access_token = "access_token"
floatFormatNoExponent = 'f'
respHeaderRateLimitInterval = "X-Rate-Limit-Interval"
respHeaderRateLimitLimit = "X-Rate-Limit-Limit"
respHeaderRateLimitReset = "X-Rate-Limit-Reset"
)
var (
responseFormatJSON = []byte(".json")
getMethod = []byte("GET")
)
type GeoPoint struct {
Lon float64
Lat float64
}
type ReverseGeocodeRequest struct {
GeoPoint GeoPoint
// Limit results to one or more countries.
Limit int
// Filter results to include only a subset (one or more) of the available feature types.
// Options are country, region, postcode, district, place, locality, neighborhood, address, and poi.
// Multiple options can be comma-separated. Note that poi.landmark is a deprecated type that, while still supported,
// returns the same data as is returned using the poi type.
Types []string
// Permitted values are ISO 3166 alpha 2(https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) country codes separated by commas.
Country string
// Specify the users language. This parameter controls the language of the text supplied in responses.
// Options are IETF language tags comprised of a mandatory ISO 639-1 language code and, optionally,
// one or more IETF subtags for country or script.
// More than one value can also be specified, separated by commas,
// for applications that need to display labels in multiple languages.
// For more information on which specific languages are supported, see https://docs.mapbox.com/api/search/#language-coverage
Language string
// Decides how results are sorted in a reverse geocoding query
// if multiple results are requested using a limit other than 1.
// Options are distance (default), which causes the closest feature
// to always be returned first, and score, which allows high-prominence features
// to be sorted higher than nearer, lower-prominence features.
ReverseMode int
// Specify whether to request additional metadata about the recommended navigation destination corresponding
// to the feature (true) or not (false, default). Only applicable for address features.
// For example, if routing=true the response could include data about a point on the road the feature fronts.
// Response features may include an array containing one or more routable points.
// Routable points cannot always be determined.
// Consuming applications should fall back to using the features normal geometry for routing
// if a separate routable point is not returned.
Routing bool
}
// RateLimit wraps mapbox API rate limit resp headers
type RateLimit struct {
Interval []byte
Limit []byte
Reset []byte
}
// easyjson:json
type rawReverseGeoResp struct {
Features []Feature `json:"features"`
Query []float64 `json:"query"`
}
// easyjson:json
type rawForwardGeoResp struct {
Features []Feature `json:"features"`
Query []string `json:"query"`
}
// GeocodeResponse
type GeocodeResponse struct {
RateLimit RateLimit
// Raw mapbox API response
RawResp []byte
// passed query to mapbox
ReverseQuery GeoPoint
ForwardQuery []string
// response result type
Type string
// response data
Features []Feature
}
type ForwardGeocodeRequest struct {
//The feature youre trying to look up.
//This could be an address, a point of interest name, a city name, etc.
//When searching for points of interest, it can also be a category name (for example, “coffee shop”).
//For information on categories, see the Point of interest category coverage section.
//The search text should be expressed as a URL-encoded UTF-8 string,
//and must not contain the semicolon character (either raw or URL-encoded).
//Your search text, once decoded, must consist of at most 20 words and numbers separated by spacing and punctuation,
//and at most 256 characters.
//
//The accuracy of coordinates returned by a forward geocoding request can be impacted
//by how the addresses in the query are formatted. Learn more about address formatting
//best practices in the https://docs.mapbox.com/help/troubleshooting/address-geocoding-format-guide.
SearchText string
//Specify whether to return autocomplete results (true, default) or not (false).
//When autocomplete is enabled, results will be included that start with the requested string,
//rather than just responses that match it exactly.
//For example, a query for India might return both India and Indiana with autocomplete enabled,
//but only India if its disabled.
//
//When autocomplete is enabled, each user keystroke counts as one request to the Geocoding API.
//For example, a search for "coff" would be reflected as four separate Geocoding API requests.
//To reduce the total requests sent, you can configure your application
//to only call the Geocoding API after a specific number of characters are typed.
Autocomplete *bool // default true
//Limit results to only those contained within the supplied bounding box
//Bounding boxes should be supplied as four numbers separated by commas,
//in minLon,minLat,maxLon,maxLat order.
//The bounding box cannot cross the 180th meridian.
Bbox []float64
//Limit results to one or more countries.
//Permitted values are ISO 3166 alpha 2 country codes separated by commas.
Country string
//Specify whether the Geocoding API should attempt approximate,
//as well as exact, matching when performing searches (true, default),
//or whether it should opt out of this behavior and only attempt exact matching (false).
//For example, the default setting might return Washington, DC for a query of wahsington,
//even though the query was misspelled.
FuzzyMatch *bool // default true
//Specify the users language.
//This parameter controls the language of the text supplied in responses, and also affects result scoring,
//with results matching the users query in the requested language being preferred over results
//that match in another language. For example, an autocomplete query for things
//that start with Frank might return Frankfurt as the first result with an English (en) language parameter,
//but Frankreich (“France”) with a German (de) language parameter.
//
//Options are IETF language tags comprised of a mandatory ISO 639-1 language code and, optionally,
//one or more IETF subtags for country or script.
//
//More than one value can also be specified, separated by commas,
//for applications that need to display labels in multiple languages.
//
//For more information on which specific languages are supported, see the https://docs.mapbox.com/api/search/#language-coverage.
Language string
//Specify the maximum number of results to return. The default is 5 and the maximum supported is 10.
Limit int // default 5
//Bias the response to favor results that are closer to this location
Proximity *GeoPoint
//Specify whether to request additional metadata about the recommended navigation destination
//corresponding to the feature (true) or not (false, default). Only applicable for address features.
//
//For example, if routing=true the response could include data about a point on the road the feature fronts.
//Response features may include an array containing one or more routable points.
//Routable points cannot always be determined.
//Consuming applications should fall back to using the features normal geometry for routing
//if a separate routable point is not returned.
Routing bool //default false
//Filter results to include only a subset (one or more) of the available feature types.
//Options are country, region, postcode, district, place, locality, neighborhood, address, and poi.
//Multiple options can be comma-separated. Note that poi.landmark is a deprecated type that,
//while still supported, returns the same data as is returned using the poi type.
//
//For more information on the available types, see the https://docs.mapbox.com/api/search/#data-types.
Types []string
}
// Geocoder encapsulates forward and reverse geocode calls.
type Geocoder interface {
// ReverseGeocode calls geocode/v5 reverse mapbox API
ReverseGeocode(ctx context.Context, req *ReverseGeocodeRequest) (*GeocodeResponse, error)
// ReverseGeocode calls geocode/v5 reverse mapbox API
ForwardGeocode(ctx context.Context, req *ForwardGeocodeRequest) (*GeocodeResponse, error)
}
// FastHttpGeocoder is a fasthttp Geocoder implementation
type FastHttpGeocoder struct {
config
geocodeAPIURL []byte
stringBufPull *stringsBufferPool
}
// ReverseGeocode calls geocode/v5 reverse mapbox API thought fasthttp client.
func (c *FastHttpGeocoder) ReverseGeocode(ctx context.Context, req *ReverseGeocodeRequest) (*GeocodeResponse, error) {
freq := fasthttp.AcquireRequest()
defer fasthttp.ReleaseRequest(freq)
fresp := fasthttp.AcquireResponse()
defer fasthttp.ReleaseResponse(fresp)
// split multivalues to limit memory consumption
values := make(map[string]string, 5)
if req.Country != "" {
values[country] = req.Country
}
if req.Limit != 0 {
values[limit] = strconv.Itoa(req.Limit)
}
if req.Language != "" {
values[language] = req.Language
}
if req.Routing {
values[routing] = trueStr
}
if req.ReverseMode == 1 {
values[reverseMode] = oneStr
}
if len(req.Types) > 0 {
values[types] = strings.Join(req.Types, ",")
}
buf := c.stringBufPull.acquireStringsBuilder()
defer c.stringBufPull.releaseStringsBuilder(buf)
buf.Write(c.geocodeAPIURL)
buf.WriteString(strconv.FormatFloat(req.GeoPoint.Lon, floatFormatNoExponent, 6, 64))
buf.WriteByte(comma)
buf.WriteString(strconv.FormatFloat(req.GeoPoint.Lat, floatFormatNoExponent, 6, 64))
buf.Write(responseFormatJSON)
buf.Write(c.accessTokenGetValue)
encodeValues(buf, values)
reqURI := buf.Bytes()
c.withLogger(ctx, func(logger Logger) {
logger.Debugf("mapbox_sdk: reverse geocode request %s", buf.String())
})
freq.Header.SetMethodBytes(getMethod)
freq.SetRequestURIBytes(reqURI)
if err := c.client.Do(freq, fresp); err != nil {
return nil, err
}
respBytes := make([]byte, len(fresp.Body()))
copy(respBytes, fresp.Body())
c.withLogger(ctx, func(logger Logger) {
logger.Debugf("mapbox_sdk: reverse geocode response %s", string(respBytes))
})
if fresp.Header.StatusCode() != http.StatusOK {
return nil, errors.Errorf("failed to reverse geocode URI %s statusCode %d resp %s",
reqURI, fresp.Header.StatusCode(), string(respBytes))
}
respRaw := rawReverseGeoResp{}
if err := respRaw.UnmarshalJSON(respBytes); err != nil {
return nil, errors.Wrapf(err, "failed to unmarshall raw reverse geocode resp %s", string(respBytes))
}
if len(respRaw.Query) != 2 {
return nil, errors.Errorf("unexpected len of query coordinates in resp %s", string(respBytes))
}
return &GeocodeResponse{
RateLimit: readRespRateLimit(fresp),
RawResp: respBytes,
ReverseQuery: GeoPoint{
Lon: respRaw.Query[0],
Lat: respRaw.Query[1],
},
Features: respRaw.Features,
}, nil
}
// ReverseGeocode calls geocode/v5 reverse mapbox API thought fasthttp client.
func (c *FastHttpGeocoder) ForwardGeocode(ctx context.Context, req *ForwardGeocodeRequest) (*GeocodeResponse, error) {
freq := fasthttp.AcquireRequest()
defer fasthttp.ReleaseRequest(freq)
fresp := fasthttp.AcquireResponse()
defer fasthttp.ReleaseResponse(fresp)
// split multivalues to limit memory consumption
values := make(map[string]string, 9)
if req.Country != "" {
values[country] = req.Country
}
if req.Limit != 0 {
values[limit] = strconv.Itoa(req.Limit)
}
if req.Language != "" {
values[language] = req.Language
}
if req.Routing {
values[routing] = trueStr
}
if req.Autocomplete != nil {
values[autocomplete] = fmt.Sprint(*req.Autocomplete)
} else {
values[autocomplete] = trueStr
}
if req.FuzzyMatch != nil {
values[fuzzymatch] = fmt.Sprint(*req.FuzzyMatch)
} else {
values[fuzzymatch] = trueStr
}
if len(req.Bbox) == 4 {
values[bbox] = fmt.Sprintf("%f,%f,%f,%f", req.Bbox[0], req.Bbox[1], req.Bbox[2], req.Bbox[3])
}
if req.Proximity != nil {
values[proximity] = fmt.Sprintf("%f,%f", req.Proximity.Lon, req.Proximity.Lat)
}
values[routing] = fmt.Sprint(req.Routing)
if len(req.Types) > 0 {
values[types] = strings.Join(req.Types, ",")
}
buf := c.stringBufPull.acquireStringsBuilder()
defer c.stringBufPull.releaseStringsBuilder(buf)
buf.Write(c.geocodeAPIURL)
buf.WriteString(req.SearchText)
buf.Write(responseFormatJSON)
buf.Write(c.accessTokenGetValue)
encodeValues(buf, values)
reqURI := buf.Bytes()
c.withLogger(ctx, func(logger Logger) {
logger.Debugf("mapbox_sdk: forward geocode request %s", buf.String())
})
freq.Header.SetMethodBytes(getMethod)
freq.SetRequestURIBytes(reqURI)
if err := c.client.Do(freq, fresp); err != nil {
return nil, err
}
respBytes := make([]byte, len(fresp.Body()))
copy(respBytes, fresp.Body())
c.withLogger(ctx, func(logger Logger) {
logger.Debugf("mapbox_sdk: forward geocode response %s", string(respBytes))
})
if fresp.Header.StatusCode() != http.StatusOK {
return nil, errors.Errorf("failed to reverse geocode URI %s statusCode %d resp %s",
reqURI, fresp.Header.StatusCode(), string(respBytes))
}
respRaw := rawForwardGeoResp{}
if err := respRaw.UnmarshalJSON(respBytes); err != nil {
return nil, errors.Wrapf(err, "failed to unmarshall raw reverse geocode resp %s", string(respBytes))
}
return &GeocodeResponse{
RateLimit: readRespRateLimit(fresp),
RawResp: respBytes,
Features: respRaw.Features,
ForwardQuery: respRaw.Query,
}, nil
}
func NewFastHttpGeocoder(opts ...Option) *FastHttpGeocoder {
c := FastHttpGeocoder{
config: newConfig(),
stringBufPull: newStringsBufferPool(),
geocodeAPIURL: []byte("/geocoding/v5/"),
}
for _, o := range opts {
c.config = o(c.config)
}
c.config = c.config.withEnv()
c.config = c.config.prepare()
c.geocodeAPIURL = []byte(c.rootAPI + string(c.geocodeAPIURL) + c.geocodeEndpoint + slash)
return &c
}
func readRespRateLimit(resp *fasthttp.Response) RateLimit {
return RateLimit{
Interval: resp.Header.Peek(respHeaderRateLimitInterval),
Limit: resp.Header.Peek(respHeaderRateLimitLimit),
Reset: resp.Header.Peek(respHeaderRateLimitReset),
}
}

View File

@@ -0,0 +1,293 @@
// Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT.
package mapbox
import (
json "encoding/json"
easyjson "github.com/mailru/easyjson"
jlexer "github.com/mailru/easyjson/jlexer"
jwriter "github.com/mailru/easyjson/jwriter"
)
// suppress unused package warning
var (
_ *json.RawMessage
_ *jlexer.Lexer
_ *jwriter.Writer
_ easyjson.Marshaler
)
func easyjson46e924aeDecodeGithubComHumansNetMapboxSdkGoMapbox(in *jlexer.Lexer, out *rawReverseGeoResp) {
isTopLevel := in.IsStart()
if in.IsNull() {
if isTopLevel {
in.Consumed()
}
in.Skip()
return
}
in.Delim('{')
for !in.IsDelim('}') {
key := in.UnsafeString()
in.WantColon()
if in.IsNull() {
in.Skip()
in.WantComma()
continue
}
switch key {
case "features":
if in.IsNull() {
in.Skip()
out.Features = nil
} else {
in.Delim('[')
if out.Features == nil {
if !in.IsDelim(']') {
out.Features = make([]Feature, 0, 1)
} else {
out.Features = []Feature{}
}
} else {
out.Features = (out.Features)[:0]
}
for !in.IsDelim(']') {
var v1 Feature
(v1).UnmarshalEasyJSON(in)
out.Features = append(out.Features, v1)
in.WantComma()
}
in.Delim(']')
}
case "query":
if in.IsNull() {
in.Skip()
out.Query = nil
} else {
in.Delim('[')
if out.Query == nil {
if !in.IsDelim(']') {
out.Query = make([]float64, 0, 8)
} else {
out.Query = []float64{}
}
} else {
out.Query = (out.Query)[:0]
}
for !in.IsDelim(']') {
var v2 float64
v2 = float64(in.Float64())
out.Query = append(out.Query, v2)
in.WantComma()
}
in.Delim(']')
}
default:
in.SkipRecursive()
}
in.WantComma()
}
in.Delim('}')
if isTopLevel {
in.Consumed()
}
}
func easyjson46e924aeEncodeGithubComHumansNetMapboxSdkGoMapbox(out *jwriter.Writer, in rawReverseGeoResp) {
out.RawByte('{')
first := true
_ = first
{
const prefix string = ",\"features\":"
out.RawString(prefix[1:])
if in.Features == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 {
out.RawString("null")
} else {
out.RawByte('[')
for v3, v4 := range in.Features {
if v3 > 0 {
out.RawByte(',')
}
(v4).MarshalEasyJSON(out)
}
out.RawByte(']')
}
}
{
const prefix string = ",\"query\":"
out.RawString(prefix)
if in.Query == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 {
out.RawString("null")
} else {
out.RawByte('[')
for v5, v6 := range in.Query {
if v5 > 0 {
out.RawByte(',')
}
out.Float64(float64(v6))
}
out.RawByte(']')
}
}
out.RawByte('}')
}
// MarshalJSON supports json.Marshaler interface
func (v rawReverseGeoResp) MarshalJSON() ([]byte, error) {
w := jwriter.Writer{}
easyjson46e924aeEncodeGithubComHumansNetMapboxSdkGoMapbox(&w, v)
return w.Buffer.BuildBytes(), w.Error
}
// MarshalEasyJSON supports easyjson.Marshaler interface
func (v rawReverseGeoResp) MarshalEasyJSON(w *jwriter.Writer) {
easyjson46e924aeEncodeGithubComHumansNetMapboxSdkGoMapbox(w, v)
}
// UnmarshalJSON supports json.Unmarshaler interface
func (v *rawReverseGeoResp) UnmarshalJSON(data []byte) error {
r := jlexer.Lexer{Data: data}
easyjson46e924aeDecodeGithubComHumansNetMapboxSdkGoMapbox(&r, v)
return r.Error()
}
// UnmarshalEasyJSON supports easyjson.Unmarshaler interface
func (v *rawReverseGeoResp) UnmarshalEasyJSON(l *jlexer.Lexer) {
easyjson46e924aeDecodeGithubComHumansNetMapboxSdkGoMapbox(l, v)
}
func easyjson46e924aeDecodeGithubComHumansNetMapboxSdkGoMapbox1(in *jlexer.Lexer, out *rawForwardGeoResp) {
isTopLevel := in.IsStart()
if in.IsNull() {
if isTopLevel {
in.Consumed()
}
in.Skip()
return
}
in.Delim('{')
for !in.IsDelim('}') {
key := in.UnsafeString()
in.WantColon()
if in.IsNull() {
in.Skip()
in.WantComma()
continue
}
switch key {
case "features":
if in.IsNull() {
in.Skip()
out.Features = nil
} else {
in.Delim('[')
if out.Features == nil {
if !in.IsDelim(']') {
out.Features = make([]Feature, 0, 1)
} else {
out.Features = []Feature{}
}
} else {
out.Features = (out.Features)[:0]
}
for !in.IsDelim(']') {
var v7 Feature
(v7).UnmarshalEasyJSON(in)
out.Features = append(out.Features, v7)
in.WantComma()
}
in.Delim(']')
}
case "query":
if in.IsNull() {
in.Skip()
out.Query = nil
} else {
in.Delim('[')
if out.Query == nil {
if !in.IsDelim(']') {
out.Query = make([]string, 0, 4)
} else {
out.Query = []string{}
}
} else {
out.Query = (out.Query)[:0]
}
for !in.IsDelim(']') {
var v8 string
v8 = string(in.String())
out.Query = append(out.Query, v8)
in.WantComma()
}
in.Delim(']')
}
default:
in.SkipRecursive()
}
in.WantComma()
}
in.Delim('}')
if isTopLevel {
in.Consumed()
}
}
func easyjson46e924aeEncodeGithubComHumansNetMapboxSdkGoMapbox1(out *jwriter.Writer, in rawForwardGeoResp) {
out.RawByte('{')
first := true
_ = first
{
const prefix string = ",\"features\":"
out.RawString(prefix[1:])
if in.Features == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 {
out.RawString("null")
} else {
out.RawByte('[')
for v9, v10 := range in.Features {
if v9 > 0 {
out.RawByte(',')
}
(v10).MarshalEasyJSON(out)
}
out.RawByte(']')
}
}
{
const prefix string = ",\"query\":"
out.RawString(prefix)
if in.Query == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 {
out.RawString("null")
} else {
out.RawByte('[')
for v11, v12 := range in.Query {
if v11 > 0 {
out.RawByte(',')
}
out.String(string(v12))
}
out.RawByte(']')
}
}
out.RawByte('}')
}
// MarshalJSON supports json.Marshaler interface
func (v rawForwardGeoResp) MarshalJSON() ([]byte, error) {
w := jwriter.Writer{}
easyjson46e924aeEncodeGithubComHumansNetMapboxSdkGoMapbox1(&w, v)
return w.Buffer.BuildBytes(), w.Error
}
// MarshalEasyJSON supports easyjson.Marshaler interface
func (v rawForwardGeoResp) MarshalEasyJSON(w *jwriter.Writer) {
easyjson46e924aeEncodeGithubComHumansNetMapboxSdkGoMapbox1(w, v)
}
// UnmarshalJSON supports json.Unmarshaler interface
func (v *rawForwardGeoResp) UnmarshalJSON(data []byte) error {
r := jlexer.Lexer{Data: data}
easyjson46e924aeDecodeGithubComHumansNetMapboxSdkGoMapbox1(&r, v)
return r.Error()
}
// UnmarshalEasyJSON supports easyjson.Unmarshaler interface
func (v *rawForwardGeoResp) UnmarshalEasyJSON(l *jlexer.Lexer) {
easyjson46e924aeDecodeGithubComHumansNetMapboxSdkGoMapbox1(l, v)
}

View File

@@ -0,0 +1,9 @@
package mapbox
import (
"github.com/valyala/fasthttp"
)
type FastHttpClient interface {
Do(req *fasthttp.Request, resp *fasthttp.Response) error
}

View File

@@ -0,0 +1,22 @@
package mapbox
import (
"context"
)
type Logger interface {
Debugf(msg string, params ...interface{})
Errorf(msg string, params ...interface{})
}
// withLogger helps to reduce unnecessary allocations
func (c *config) withLogger(ctx context.Context, do func(Logger)) {
if c.requestLogger != nil {
do(c.requestLogger(ctx))
return
}
if c.logger != nil {
do(c.logger)
}
}

View File

@@ -0,0 +1,31 @@
package mapbox
import (
"bytes"
"sync"
)
type noCopy struct{}
func (*noCopy) Lock() {}
func (*noCopy) Unlock() {}
type stringsBufferPool struct {
noCopy noCopy
p sync.Pool
}
func newStringsBufferPool() *stringsBufferPool {
return &stringsBufferPool{p: sync.Pool{New: func() interface{} {
return &bytes.Buffer{}
}}}
}
func (pool *stringsBufferPool) acquireStringsBuilder() *bytes.Buffer {
return pool.p.Get().(*bytes.Buffer)
}
func (pool *stringsBufferPool) releaseStringsBuilder(b *bytes.Buffer) {
b.Reset()
pool.p.Put(b)
}

View File

@@ -0,0 +1,23 @@
package mapbox
import (
"bytes"
)
const (
slash = "/"
comma = ','
questionMark = "?"
equalMark = '='
ampersandMark = '&'
)
// encodeValues do almost the same as url.Values.Encode() but faster and reuses *strings.Builder
func encodeValues(buf *bytes.Buffer, values map[string]string) {
for k, v := range values {
buf.WriteByte(ampersandMark)
buf.WriteString(k)
buf.WriteByte(equalMark)
buf.WriteString(v)
}
}