github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/chat/maps/maps.go (about)

     1  package maps
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"crypto/tls"
     7  	"errors"
     8  	"fmt"
     9  	"image"
    10  	"image/png"
    11  	"io"
    12  	"net/http"
    13  	"time"
    14  
    15  	"github.com/keybase/client/go/chat/globals"
    16  	"github.com/keybase/client/go/chat/types"
    17  	"github.com/keybase/client/go/libkb"
    18  	"github.com/keybase/client/go/protocol/chat1"
    19  	"golang.org/x/image/draw"
    20  	"golang.org/x/net/context/ctxhttp"
    21  )
    22  
    23  // For more information about the Google Maps Static API, head here:
    24  // https://developers.google.com/maps/documentation/maps-static/dev-guide
    25  
    26  const MapsProxy = "maps-proxy.core.keybaseapi.com"
    27  const mapsHost = "maps.googleapis.com"
    28  const scale = 2
    29  const locationMapWidth = 640
    30  const locationMapHeight = 350
    31  const liveMapWidth = 640
    32  const liveMapHeight = 350
    33  const liveMapWidthScaled = liveMapWidth / scale
    34  const liveMapHeightScaled = liveMapHeight / scale
    35  
    36  func GetMapURL(ctx context.Context, apiKeySource types.ExternalAPIKeySource, lat, lon float64) (string, error) {
    37  	return GetCustomMapURL(ctx, apiKeySource, lat, lon, locationMapWidth, locationMapHeight)
    38  }
    39  
    40  func GetCustomMapURL(ctx context.Context, apiKeySource types.ExternalAPIKeySource, lat, lon float64,
    41  	width, height int) (string, error) {
    42  	key, err := apiKeySource.GetKey(ctx, chat1.ExternalAPIKeyTyp_GOOGLEMAPS)
    43  	if err != nil {
    44  		return "", err
    45  	}
    46  	widthScaled := width / scale
    47  	heightScaled := height / scale
    48  	return fmt.Sprintf(
    49  		"https://%s/maps/api/staticmap?zoom=18&center=%f,%f&size=%dx%d&scale=%d&key=%s",
    50  		MapsProxy, lat, lon, widthScaled, heightScaled, scale,
    51  		key.Googlemaps()), nil
    52  
    53  }
    54  
    55  func GetLiveMapURL(ctx context.Context, apiKeySource types.ExternalAPIKeySource, coords []chat1.Coordinate) (string, error) {
    56  	if len(coords) == 0 {
    57  		return "", errors.New("empty coords")
    58  	}
    59  	key, err := apiKeySource.GetKey(ctx, chat1.ExternalAPIKeyTyp_GOOGLEMAPS)
    60  	if err != nil {
    61  		return "", err
    62  	}
    63  	var pathStr, centerStr string
    64  	last := coords[len(coords)-1]
    65  	centerStr = fmt.Sprintf("center=%f,%f&", last.Lat, last.Lon)
    66  	if len(coords) > 1 {
    67  		pathStr = "path=color:0x4c8effff|weight:5"
    68  		for _, c := range coords {
    69  			pathStr += fmt.Sprintf("|%f,%f", c.Lat, c.Lon)
    70  		}
    71  		pathStr += "&"
    72  	}
    73  	url := fmt.Sprintf(
    74  		"https://%s/maps/api/staticmap?zoom=18&%s%ssize=%dx%d&scale=%d&key=%s",
    75  		MapsProxy, centerStr, pathStr, liveMapWidthScaled,
    76  		liveMapHeightScaled, scale, key.Googlemaps())
    77  	return url, nil
    78  }
    79  
    80  func GetExternalMapURL(ctx context.Context, lat, lon float64) string {
    81  	return fmt.Sprintf("https://www.google.com/maps/place/%f,%f/@%f,%f,15z", lat, lon, lat, lon)
    82  }
    83  
    84  func httpClient(g *libkb.GlobalContext, host string) *http.Client {
    85  	var xprt http.Transport
    86  	tlsConfig := &tls.Config{
    87  		ServerName: host,
    88  	}
    89  	xprt.TLSClientConfig = tlsConfig
    90  	return &http.Client{
    91  		Transport: libkb.NewInstrumentedRoundTripper(g,
    92  			func(*http.Request) string { return "LocationShare" }, libkb.NewClosingRoundTripper(&xprt)),
    93  		Timeout: 10 * time.Second,
    94  	}
    95  }
    96  
    97  func MapReaderFromURL(ctx context.Context, g *globals.Context, url string) (res io.ReadCloser, length int64, err error) {
    98  	req, err := http.NewRequest("GET", url, nil)
    99  	if err != nil {
   100  		return nil, 0, err
   101  	}
   102  	req.Host = mapsHost
   103  	resp, err := ctxhttp.Do(ctx, httpClient(g.ExternalG(), mapsHost), req)
   104  	if err != nil {
   105  		return nil, 0, err
   106  	}
   107  	return resp.Body, resp.ContentLength, nil
   108  }
   109  
   110  func DecorateMap(ctx context.Context, avatarReader, mapReader io.Reader) (res io.ReadCloser, length int64, err error) {
   111  	avatarImg, _, err := image.Decode(avatarReader)
   112  	if err != nil {
   113  		return res, length, err
   114  	}
   115  	avatarRadius := avatarImg.Bounds().Dx() / 2
   116  
   117  	mapPng, err := png.Decode(mapReader)
   118  	if err != nil {
   119  		return res, length, err
   120  	}
   121  	bounds := mapPng.Bounds()
   122  
   123  	middle := image.Point{bounds.Max.X / 2, bounds.Max.Y / 2}
   124  	iconRect := image.Rect(middle.X-avatarRadius, middle.Y-avatarRadius, middle.X+avatarRadius, middle.Y+avatarRadius)
   125  
   126  	decorated := image.NewRGBA(bounds)
   127  	draw.Draw(decorated, bounds, mapPng, image.Point{}, draw.Src)
   128  	draw.Draw(decorated, iconRect, avatarImg, image.Point{}, draw.Over)
   129  
   130  	var buf bytes.Buffer
   131  	err = png.Encode(&buf, decorated)
   132  	if err != nil {
   133  		return res, length, err
   134  	}
   135  	return io.NopCloser(bytes.NewReader(buf.Bytes())), int64(buf.Len()), nil
   136  }