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¢er=%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 }