github.com/janelia-flyem/dvid@v1.0.0/datatype/googlevoxels/googlevoxels.go (about) 1 /* 2 Package googlevoxels implements DVID support for multi-scale tiles and volumes in XY, XZ, 3 and YZ orientation using the Google BrainMaps API. 4 */ 5 package googlevoxels 6 7 import ( 8 "bytes" 9 "encoding/gob" 10 "encoding/json" 11 "fmt" 12 "image" 13 "io" 14 "io/ioutil" 15 "net/http" 16 "strconv" 17 "strings" 18 19 "github.com/janelia-flyem/dvid/datastore" 20 "github.com/janelia-flyem/dvid/datatype/imagetile" 21 "github.com/janelia-flyem/dvid/dvid" 22 "github.com/janelia-flyem/dvid/server" 23 "github.com/janelia-flyem/dvid/storage" 24 25 "golang.org/x/oauth2" 26 "golang.org/x/oauth2/google" 27 28 lz4 "github.com/janelia-flyem/go/golz4-updated" 29 ) 30 31 const ( 32 Version = "0.1" 33 RepoURL = "github.com/janelia-flyem/dvid/datatype/googlevoxels" 34 TypeName = "googlevoxels" 35 ) 36 37 const helpMessage = ` 38 API for datatypes derived from googlevoxels (github.com/janelia-flyem/dvid/datatype/googlevoxels) 39 ================================================================================================= 40 41 Command-line: 42 43 $ dvid repo <UUID> new googlevoxels <data name> <settings...> 44 45 Adds voxel support using Google BrainMaps API. 46 47 Example: 48 49 $ dvid repo 3f8c new googlevoxels grayscale volumeid=281930192:stanford jwtfile=/foo/myname-319.json 50 51 Arguments: 52 53 UUID Hexadecimal string with enough characters to uniquely identify a version node. 54 data name Name of data to create, e.g., "mygrayscale" 55 settings Configuration settings in "key=value" format separated by spaces. 56 57 Required Configuration Settings (case-insensitive keys) 58 59 volumeid The globally unique identifier of the volume within Google BrainMaps API. 60 jwtfile Path to JSON Web Token file downloaded from http://console.developers.google.com. 61 Under the BrainMaps API, visit the Credentials area, create credentials for a 62 service account key, then download that JWT file. 63 64 Optional Configuration Settings (case-insensitive keys) 65 66 tilesize Default size in pixels along one dimension of square tile. If unspecified, 512. 67 68 69 $ dvid googlevoxels volumes <jwtfile> 70 71 Contacts Google BrainMaps API and returns the available volume ids for a user identified by a 72 JSON Web Token (JWT) file. 73 74 Example: 75 76 $ dvid googlevoxels volumes /foo/myname-319.json 77 78 Arguments: 79 80 jwtfile Path to JSON Web Token file downloaded from http://console.developers.google.com. 81 Under the BrainMaps API, visit the Credentials area, create credentials for a 82 service account key, then download that JWT file. 83 84 85 ------------------ 86 87 HTTP API (Level 2 REST): 88 89 GET <api URL>/node/<UUID>/<data name>/help 90 91 Returns data-specific help message. 92 93 94 GET <api URL>/node/<UUID>/<data name>/info 95 96 Retrieves characteristics of this data in JSON format. 97 98 Example: 99 100 GET <api URL>/node/3f8c/grayscale/info 101 102 Arguments: 103 104 UUID Hexadecimal string with enough characters to uniquely identify a version node. 105 data name Name of googlevoxels data. 106 107 108 GET <api URL>/node/<UUID>/<data name>/tile/<dims>/<scaling>/<tile coord>[?options] 109 110 Retrieves a tile of named data within a version node. The default tile size is used unless 111 the query string "tilesize" is provided. 112 113 Example: 114 115 GET <api URL>/node/3f8c/grayscale/tile/xy/0/10_10_20 116 117 Arguments: 118 119 UUID Hexadecimal string with enough characters to uniquely identify a version node. 120 data name Name of data to add. 121 dims The axes of data extraction in form "i_j_k,..." Example: "0_2" can be XZ. 122 Slice strings ("xy", "xz", or "yz") are also accepted. 123 scaling Value from 0 (original resolution) to N where each step is downres by 2. 124 tile coord The tile coordinate in "x_y_z" format. See discussion of scaling above. 125 126 Query-string options: 127 128 tilesize Size in pixels along one dimension of square tile. 129 noblanks If true, any tile request for tiles outside the currently stored extents 130 will return a placeholder. 131 format "png", "jpeg" (default: "png") 132 jpeg allows lossy quality setting, e.g., "jpeg:80" (0 <= quality <= 100) 133 png allows compression levels, e.g., "png:7" (0 <= level <= 9) 134 135 136 GET <api URL>/node/<UUID>/<data name>/raw/<dims>/<size>/<offset>[/<format>][?queryopts] 137 138 Retrieves either 2d images (PNG by default) or 3d binary data, depending on the dims parameter. 139 The 3d binary data response has "Content-type" set to "application/octet-stream" and is an array of 140 voxel values in ZYX order (X iterates most rapidly). 141 142 Example: 143 144 GET <api URL>/node/3f8c/segmentation/raw/0_1/512_256/0_0_100/jpg:80 145 146 Returns a raw XY slice (0th and 1st dimensions) with width (x) of 512 voxels and 147 height (y) of 256 voxels with offset (0,0,100) in JPG format with quality 80. 148 By "raw", we mean that no additional processing is applied based on voxel 149 resolutions to make sure the retrieved image has isotropic pixels. 150 The example offset assumes the "grayscale" data in version node "3f8c" is 3d. 151 The "Content-type" of the HTTP response should agree with the requested format. 152 For example, returned PNGs will have "Content-type" of "image/png", and returned 153 nD data will be "application/octet-stream". 154 155 Arguments: 156 157 UUID Hexadecimal string with enough characters to uniquely identify a version node. 158 data name Name of data to add. 159 dims The axes of data extraction in form "i_j_k,..." 160 Slice strings ("xy", "xz", or "yz") are also accepted. 161 Example: "0_2" is XZ, and "0_1_2" is a 3d subvolume. 162 size Size in voxels along each dimension specified in <dims>. 163 offset Gives coordinate of first voxel using dimensionality of data. 164 format Valid formats depend on the dimensionality of the request and formats 165 available in server implementation. 166 2D: "png", "jpg" (default: "png") 167 jpg allows lossy quality setting, e.g., "jpg:80" 168 nD: uses default "octet-stream". 169 170 Query-string Options: 171 172 compression Allows retrieval or submission of 3d data in "raw" (default) or "lz4" format. 173 The 2d data will ignore this and use the image-based codec. 174 scale Default is 0. For scale N, returns an image down-sampled by a factor of 2^N. 175 throttle Only works for 3d data requests. If "true", makes sure only N compute-intense operation 176 (all API calls that can be throttled) are handled. If the server can't initiate the API 177 call right away, a 503 (Service Unavailable) status code is returned. 178 ` 179 180 func init() { 181 datastore.Register(NewType()) 182 183 // Need to register types that will be used to fulfill interfaces. 184 gob.Register(&Type{}) 185 gob.Register(&Data{}) 186 } 187 188 var ( 189 DefaultTileSize int32 = 512 190 DefaultTileFormat string = "png" 191 bmapsPrefix string = "https://brainmaps.googleapis.com/v1" 192 ) 193 194 // Type embeds the datastore's Type to create a unique type with tile functions. 195 // Refinements of general tile types can be implemented by embedding this type, 196 // choosing appropriate # of channels and bytes/voxel, overriding functions as 197 // needed, and calling datastore.Register(). 198 // Note that these fields are invariant for all instances of this type. Fields 199 // that can change depending on the type of data (e.g., resolution) should be 200 // in the Data type. 201 type Type struct { 202 datastore.Type 203 } 204 205 // NewDatatype returns a pointer to a new voxels Datatype with default values set. 206 func NewType() *Type { 207 return &Type{ 208 datastore.Type{ 209 Name: "googlevoxels", 210 URL: "github.com/janelia-flyem/dvid/datatype/googlevoxels", 211 Version: "0.1", 212 Requirements: &storage.Requirements{ 213 Batcher: true, 214 }, 215 }, 216 } 217 } 218 219 // --- TypeService interface --- 220 221 // NewData returns a pointer to new googlevoxels data with default values. 222 func (dtype *Type) NewDataService(uuid dvid.UUID, id dvid.InstanceID, name dvid.InstanceName, c dvid.Config) (datastore.DataService, error) { 223 // Make sure we have needed volumeid and authentication key. 224 volumeid, found, err := c.GetString("volumeid") 225 if err != nil { 226 return nil, err 227 } 228 if !found { 229 return nil, fmt.Errorf("Cannot make googlevoxels data without valid 'volumeid' setting.") 230 } 231 jwtfile, found, err := c.GetString("jwtfile") 232 if err != nil { 233 return nil, err 234 } 235 if !found { 236 return nil, fmt.Errorf("Cannot make googlevoxels data without valid 'jwtfile' specifying path to JSON Web Token") 237 } 238 239 // Read in the JSON Web Token 240 jwtdata, err := ioutil.ReadFile(jwtfile) 241 if err != nil { 242 return nil, fmt.Errorf("Cannot load JSON Web Token file (%s): %v", jwtfile, err) 243 } 244 conf, err := google.JWTConfigFromJSON(jwtdata, "https://www.googleapis.com/auth/brainmaps") 245 if err != nil { 246 return nil, fmt.Errorf("Cannot establish JWT Config file from Google: %v", err) 247 } 248 client := conf.Client(oauth2.NoContext) 249 250 // Make URL call to get the available scaled volumes. 251 url := fmt.Sprintf("%s/volumes/%s", bmapsPrefix, volumeid) 252 resp, err := client.Get(url) 253 if err != nil { 254 return nil, fmt.Errorf("Error getting volume metadata from Google: %v", err) 255 } 256 defer resp.Body.Close() 257 if resp.StatusCode != http.StatusOK { 258 return nil, fmt.Errorf("Unexpected status code %d returned when getting volume metadata for %q", resp.StatusCode, volumeid) 259 } 260 metadata, err := ioutil.ReadAll(resp.Body) 261 if err != nil { 262 return nil, err 263 } 264 var m struct { 265 Geoms Geometries `json:"geometry"` 266 } 267 if err := json.Unmarshal(metadata, &m); err != nil { 268 return nil, fmt.Errorf("Error decoding volume JSON metadata: %v", err) 269 } 270 dvid.Infof("Successfully got geometries:\nmetadata:\n%s\nparsed JSON:\n%v\n", metadata, m) 271 272 // Compute the mapping from tile scale/orientation to scaled volume index. 273 geomMap := GeometryMap{} 274 275 // (1) Find the highest resolution geometry. 276 var highResIndex GeometryIndex 277 minVoxelSize := dvid.NdFloat32{10000, 10000, 10000} 278 for i, geom := range m.Geoms { 279 if geom.PixelSize[0] < minVoxelSize[0] || geom.PixelSize[1] < minVoxelSize[1] || geom.PixelSize[2] < minVoxelSize[2] { 280 minVoxelSize = geom.PixelSize 281 highResIndex = GeometryIndex(i) 282 } 283 } 284 dvid.Infof("Google voxels %q: found highest resolution was geometry %d: %s\n", name, highResIndex, minVoxelSize) 285 286 // (2) For all geometries, find out what the scaling is relative to the highest resolution pixel size. 287 for i, geom := range m.Geoms { 288 if i == int(highResIndex) { 289 geomMap[GSpec{0, XY}] = highResIndex 290 geomMap[GSpec{0, XZ}] = highResIndex 291 geomMap[GSpec{0, YZ}] = highResIndex 292 geomMap[GSpec{0, XYZ}] = highResIndex 293 } else { 294 scaleX := geom.PixelSize[0] / minVoxelSize[0] 295 scaleY := geom.PixelSize[1] / minVoxelSize[1] 296 scaleZ := geom.PixelSize[2] / minVoxelSize[2] 297 var shape Shape 298 switch { 299 case scaleX > scaleZ && scaleY > scaleZ: 300 shape = XY 301 case scaleX > scaleY && scaleZ > scaleY: 302 shape = XZ 303 case scaleY > scaleX && scaleZ > scaleX: 304 shape = YZ 305 default: 306 shape = XYZ 307 } 308 var mag float32 309 if scaleX > mag { 310 mag = scaleX 311 } 312 if scaleY > mag { 313 mag = scaleY 314 } 315 if scaleZ > mag { 316 mag = scaleZ 317 } 318 scaling := log2(mag) 319 geomMap[GSpec{scaling, shape}] = GeometryIndex(i) 320 dvid.Infof("%s at scaling %d set to geometry %d: resolution %s\n", shape, scaling, i, geom.PixelSize) 321 } 322 } 323 324 // Create a client that will be authorized and authenticated on behalf of the account. 325 326 // Initialize the googlevoxels data 327 basedata, err := datastore.NewDataService(dtype, uuid, id, name, c) 328 if err != nil { 329 return nil, err 330 } 331 data := &Data{ 332 Data: basedata, 333 Properties: Properties{ 334 VolumeID: volumeid, 335 JWT: string(jwtdata), 336 TileSize: DefaultTileSize, 337 GeomMap: geomMap, 338 Scales: m.Geoms, 339 HighResIndex: highResIndex, 340 }, 341 client: client, 342 } 343 return data, nil 344 } 345 346 // Do handles command-line requests to the Google BrainMaps API 347 func (dtype *Type) Do(cmd datastore.Request, reply *datastore.Response) error { 348 switch cmd.Argument(1) { 349 case "volumes": 350 // Read in the JSON Web Token 351 jwtdata, err := ioutil.ReadFile(cmd.Argument(2)) 352 if err != nil { 353 return fmt.Errorf("Cannot load JSON Web Token file (%s): %v", cmd.Argument(2), err) 354 } 355 conf, err := google.JWTConfigFromJSON(jwtdata, "https://www.googleapis.com/auth/brainmaps") 356 if err != nil { 357 return fmt.Errorf("Cannot establish JWT Config file from Google: %v", err) 358 } 359 client := conf.Client(oauth2.NoContext) 360 361 // Make the call. 362 url := fmt.Sprintf("%s/volumes", bmapsPrefix) 363 resp, err := client.Get(url) 364 if err != nil { 365 return fmt.Errorf("Error getting volumes metadata from Google: %v", err) 366 } 367 defer resp.Body.Close() 368 if resp.StatusCode != http.StatusOK { 369 return fmt.Errorf("Unexpected status code %d returned when getting volumes for user", resp.StatusCode) 370 } 371 metadata, err := ioutil.ReadAll(resp.Body) 372 if err != nil { 373 return err 374 } 375 reply.Text = string(metadata) 376 return nil 377 378 default: 379 return fmt.Errorf("unknown command for type %s", dtype.GetTypeName()) 380 } 381 } 382 383 // log2 returns the power of 2 necessary to cover the given value. 384 func log2(value float32) Scaling { 385 var exp Scaling 386 pow := float32(1.0) 387 for { 388 if pow >= value { 389 return exp 390 } 391 pow *= 2 392 exp++ 393 } 394 } 395 396 func (dtype *Type) Help() string { 397 return helpMessage 398 } 399 400 // GSpec encapsulates the scale and orientation of a tile. 401 type GSpec struct { 402 scaling Scaling 403 shape Shape 404 } 405 406 func (ts GSpec) MarshalBinary() ([]byte, error) { 407 return []byte{byte(ts.scaling), byte(ts.shape)}, nil 408 } 409 410 func (ts *GSpec) UnmarshalBinary(data []byte) error { 411 if len(data) != 2 { 412 return fmt.Errorf("GSpec serialization is 2 bytes. Got %d bytes instead: %v", len(data), data) 413 } 414 ts.scaling = Scaling(data[0]) 415 ts.shape = Shape(data[1]) 416 return nil 417 } 418 419 // GetGSpec returns a GSpec for a given scale and dvid Geometry. 420 func GetGSpec(scaling Scaling, shape dvid.DataShape) (*GSpec, error) { 421 ts := new(GSpec) 422 ts.scaling = scaling 423 if err := ts.shape.FromShape(shape); err != nil { 424 return nil, err 425 } 426 return ts, nil 427 } 428 429 // Scaling describes the resolution where 0 is the highest resolution 430 type Scaling uint8 431 432 // Shape describes the orientation of a 2d or 3d image. 433 type Shape uint8 434 435 const ( 436 XY Shape = iota 437 XZ 438 YZ 439 XYZ 440 ) 441 442 func (s *Shape) FromShape(shape dvid.DataShape) error { 443 switch { 444 case shape.Equals(dvid.XY): 445 *s = XY 446 case shape.Equals(dvid.XZ): 447 *s = XZ 448 case shape.Equals(dvid.YZ): 449 *s = YZ 450 case shape.Equals(dvid.Vol3d): 451 *s = XYZ 452 default: 453 return fmt.Errorf("No Google BrainMaps shape corresponds to DVID %s shape", shape) 454 } 455 return nil 456 } 457 458 func (s Shape) String() string { 459 switch s { 460 case XY: 461 return "XY" 462 case XZ: 463 return "XZ" 464 case YZ: 465 return "YZ" 466 case XYZ: 467 return "XYZ" 468 default: 469 return "Unknown orientation" 470 } 471 } 472 473 // GeometryMap provides a mapping from DVID scale (0 is highest res) and tile orientation 474 // to the specific geometry (Google "scale" value) that supports it. 475 type GeometryMap map[GSpec]GeometryIndex 476 477 func (gm GeometryMap) MarshalJSON() ([]byte, error) { 478 s := "{" 479 mapStr := make([]string, len(gm)) 480 i := 0 481 for ts, gi := range gm { 482 mapStr[i] = fmt.Sprintf(`"%s:%d": %d`, ts.shape, ts.scaling, gi) 483 i++ 484 } 485 s += strings.Join(mapStr, ",") 486 s += "}" 487 return []byte(s), nil 488 } 489 490 type GeometryIndex int 491 492 // Geometry corresponds to a Volume Geometry in Google BrainMaps API 493 type Geometry struct { 494 VolumeSize dvid.Point3d `json:"volumeSize"` 495 ChannelCount uint32 `json:"channelCount"` 496 ChannelType string `json:"channelType"` 497 PixelSize dvid.NdFloat32 `json:"pixelSize"` 498 } 499 500 // JSON from Google API encodes unsigned long as string because javascript has limited max 501 // integers due to Javascript number types using double float. 502 503 type uint3d struct { 504 X uint32 505 Y uint32 506 Z uint32 507 } 508 509 func (u *uint3d) UnmarshalJSON(b []byte) error { 510 var m struct { 511 X string `json:"x"` 512 Y string `json:"y"` 513 Z string `json:"z"` 514 } 515 if err := json.Unmarshal(b, &m); err != nil { 516 return err 517 } 518 x, err := strconv.Atoi(m.X) 519 if err != nil { 520 return fmt.Errorf("Could not parse X coordinate with unsigned long: %v", err) 521 } 522 u.X = uint32(x) 523 524 y, err := strconv.Atoi(m.Y) 525 if err != nil { 526 return fmt.Errorf("Could not parse Y coordinate with unsigned long: %v", err) 527 } 528 u.Y = uint32(y) 529 530 z, err := strconv.Atoi(m.Z) 531 if err != nil { 532 return fmt.Errorf("Could not parse Z coordinate with unsigned long: %v", err) 533 } 534 u.Z = uint32(z) 535 return nil 536 } 537 538 func (i uint3d) String() string { 539 return fmt.Sprintf("%d x %d x %d", i.X, i.Y, i.Z) 540 } 541 542 type float3d struct { 543 X float32 `json:"x"` 544 Y float32 `json:"y"` 545 Z float32 `json:"z"` 546 } 547 548 func (f float3d) String() string { 549 return fmt.Sprintf("%f x %f x %f", f.X, f.Y, f.Z) 550 } 551 552 func (g *Geometry) UnmarshalJSON(b []byte) error { 553 if g == nil { 554 return fmt.Errorf("Can't unmarshal JSON into nil Geometry") 555 } 556 var m struct { 557 VolumeSize uint3d `json:"volumeSize"` 558 ChannelCount string `json:"channelCount"` 559 ChannelType string `json:"channelType"` 560 PixelSize float3d `json:"pixelSize"` 561 } 562 if err := json.Unmarshal(b, &m); err != nil { 563 return err 564 } 565 g.VolumeSize = dvid.Point3d{int32(m.VolumeSize.X), int32(m.VolumeSize.Y), int32(m.VolumeSize.Z)} 566 g.PixelSize = dvid.NdFloat32{m.PixelSize.X, m.PixelSize.Y, m.PixelSize.Z} 567 channels, err := strconv.Atoi(m.ChannelCount) 568 if err != nil { 569 return fmt.Errorf("Could not parse channelCount: %v", err) 570 } 571 g.ChannelCount = uint32(channels) 572 g.ChannelType = m.ChannelType 573 return nil 574 } 575 576 type Geometries []Geometry 577 578 // GoogleSubvolGeom encapsulates all information needed for voxel retrieval (aside from authentication) 579 // from the Google BrainMaps API, as well as processing the returned data. 580 type GoogleSubvolGeom struct { 581 shape Shape 582 offset dvid.Point3d 583 size dvid.Point3d // This is the size we can retrieve, not necessarily the requested size 584 sizeWant dvid.Point3d // This is the requested size. 585 gi GeometryIndex 586 edge bool // Is the tile on the edge, i.e., partially outside a scaled volume? 587 outside bool // Is the tile totally outside any scaled volume? 588 589 // cached data that immediately follows from the geometry index 590 channelCount uint32 591 channelType string 592 bytesPerVoxel int32 593 } 594 595 // GetGoogleSubvolGeom returns a google-specific voxel spec, which includes how the data is positioned relative to 596 // scaled volume boundaries. Not that the size parameter is the desired size and not what is required to fit 597 // within a scaled volume. 598 func (d *Data) GetGoogleSubvolGeom(scaling Scaling, shape dvid.DataShape, offset dvid.Point3d, size dvid.Point) (*GoogleSubvolGeom, error) { 599 gsg := new(GoogleSubvolGeom) 600 if err := gsg.shape.FromShape(shape); err != nil { 601 return nil, err 602 } 603 gsg.offset = offset 604 605 // If 2d plane, convert combination of plane and size into 3d size. 606 if size.NumDims() == 2 { 607 size2d := size.(dvid.Point2d) 608 sizeWant, err := dvid.GetPoint3dFrom2d(shape, size2d, 1) 609 if err != nil { 610 return nil, err 611 } 612 gsg.sizeWant = sizeWant 613 } else { 614 var ok bool 615 gsg.sizeWant, ok = size.(dvid.Point3d) 616 if !ok { 617 return nil, fmt.Errorf("Can't convert %v to dvid.Point3d", size) 618 } 619 } 620 621 // Determine which geometry is appropriate given the scaling and the shape/orientation 622 tileSpec, err := GetGSpec(scaling, shape) 623 if err != nil { 624 return nil, err 625 } 626 geomIndex, found := d.GeomMap[*tileSpec] 627 if !found { 628 return nil, fmt.Errorf("Could not find scaled volume in %q for %s with scaling %d", d.DataName(), shape, scaling) 629 } 630 geom := d.Scales[geomIndex] 631 gsg.gi = geomIndex 632 gsg.channelCount = geom.ChannelCount 633 gsg.channelType = geom.ChannelType 634 635 // Get the # bytes for each pixel 636 switch geom.ChannelType { 637 case "UINT8": 638 gsg.bytesPerVoxel = 1 639 case "FLOAT": 640 gsg.bytesPerVoxel = 4 641 case "UINT64": 642 gsg.bytesPerVoxel = 8 643 default: 644 return nil, fmt.Errorf("Unknown volume channel type in %s: %s", d.DataName(), geom.ChannelType) 645 } 646 647 // Check if the requested area is completely outside the volume. 648 volumeSize := geom.VolumeSize 649 if offset[0] >= volumeSize[0] || offset[1] >= volumeSize[1] || offset[2] >= volumeSize[2] { 650 gsg.outside = true 651 return gsg, nil 652 } 653 654 // Check if the requested shape is on the edge and adjust size. 655 adjSize := gsg.sizeWant 656 maxpt := offset.Add(adjSize) 657 for i := uint8(0); i < 3; i++ { 658 if maxpt.Value(i) > volumeSize[i] { 659 gsg.edge = true 660 adjSize[i] = volumeSize[i] - offset[i] 661 } 662 } 663 gsg.size = adjSize 664 665 return gsg, nil 666 } 667 668 // GetURL returns the base API URL for retrieving an image. Note that the authentication key 669 // or token needs to be added to the returned string to form a valid URL. The formatStr 670 // parameter is of the form "jpeg" or "jpeg:80" or "png:8" where an optional compression 671 // level follows the image format and a colon. Leave formatStr empty for default. 672 func (gsg GoogleSubvolGeom) GetURL(volumeid, formatStr string) (url string, opts io.Reader, err error) { 673 url = fmt.Sprintf("%s/volumes/%s", bmapsPrefix, volumeid) 674 675 jsonSpec := fmt.Sprintf(`{"geometry": {"corner":"%d,%d,%d","size":"%d,%d,%d","scale": %d}`, 676 gsg.offset[0], gsg.offset[1], gsg.offset[2], 677 gsg.size[0], gsg.size[1], gsg.size[2], gsg.gi) 678 if gsg.shape == XYZ { 679 url += "/subvolume:binary" 680 if formatStr != "" { 681 jsonSpec += `,"subvolumeFormat": "SINGLE_IMAGE"` 682 } else { 683 jsonSpec += `,"subvolumeFormat": "RAW"` 684 } 685 } else { 686 jsonSpec = `{"imageSpec":` + jsonSpec 687 url += "/imagetile:binary" 688 } 689 690 if formatStr != "" { 691 format := strings.Split(formatStr, ":") 692 var gformat string 693 switch format[0] { 694 case "jpg", "jpeg", "JPG": 695 gformat = "JPEG" 696 case "png": 697 gformat = "PNG" 698 default: 699 err = fmt.Errorf("googlevoxels tiles only support JPEG or PNG formats, not %q", format[0]) 700 return 701 } 702 jsonSpec += fmt.Sprintf(`,"imageOptions":{"imageFormat":%q`, gformat) 703 if len(format) > 1 { 704 var level int 705 level, err = strconv.Atoi(format[1]) 706 if err != nil { 707 return 708 } 709 switch format[0] { 710 case "jpeg": 711 jsonSpec += fmt.Sprintf(`,"jpegQuality":%d`, level) 712 case "png": 713 jsonSpec += fmt.Sprintf(`,"pngCompressionLevel":%d`, level) 714 } 715 } 716 jsonSpec += "}" 717 } 718 if gsg.shape == XYZ { 719 jsonSpec += "}" 720 } else { 721 jsonSpec += "}}" 722 } 723 dvid.Infof("Sending image options:\n%s\n", jsonSpec) 724 opts = bytes.NewBufferString(jsonSpec) 725 // url += "?alt=media" 726 727 return 728 } 729 730 // padData takes returned data and pads it to full expected size. 731 // currently assumes that data padding needed on far edges, not near edges. 732 func (gsg GoogleSubvolGeom) padData(data []byte) ([]byte, error) { 733 if gsg.size[0]*gsg.size[1]*gsg.size[2]*gsg.bytesPerVoxel != int32(len(data)) { 734 return nil, fmt.Errorf("Before padding, for %d x %d x %d bytes/voxel tile, received %d bytes", 735 gsg.size[0], gsg.size[1], gsg.bytesPerVoxel, len(data)) 736 } 737 738 inRowBytes := gsg.size[0] * gsg.bytesPerVoxel 739 outRowBytes := gsg.sizeWant[0] * gsg.bytesPerVoxel 740 outBytes := outRowBytes * gsg.sizeWant[1] 741 out := make([]byte, outBytes, outBytes) 742 inI := int32(0) 743 outI := int32(0) 744 for y := int32(0); y < gsg.size[1]; y++ { 745 copy(out[outI:outI+inRowBytes], data[inI:inI+inRowBytes]) 746 inI += inRowBytes 747 outI += outRowBytes 748 } 749 return out, nil 750 } 751 752 // Properties are additional properties for keyvalue data instances beyond those 753 // in standard datastore.Data. These will be persisted to metadata storage. 754 type Properties struct { 755 // Necessary information to select data from Google BrainMaps API. 756 VolumeID string 757 JWT string 758 759 // Default size in pixels along one dimension of square tile. 760 TileSize int32 761 762 // GeomMap provides mapping between scale and various image shapes to Google scaling index. 763 GeomMap GeometryMap 764 765 // Scales is the list of available precomputed scales ("geometries" in Google terms) for this data. 766 Scales Geometries 767 768 // HighResIndex is the geometry that is the highest resolution among the available scaled volumes. 769 HighResIndex GeometryIndex 770 771 // OAuth2 configuration 772 oa2conf *oauth2.Config 773 } 774 775 // CopyPropertiesFrom copies the data instance-specific properties from a given 776 // data instance into the receiver's properties. Fulfills the datastore.PropertyCopier interface. 777 func (d *Data) CopyPropertiesFrom(src datastore.DataService, fs storage.FilterSpec) error { 778 d2, ok := src.(*Data) 779 if !ok { 780 return fmt.Errorf("unable to copy properties from non-imageblk data %q", src.DataName()) 781 } 782 // These should all be immutable so can have shared reference with source. 783 d.VolumeID = d2.VolumeID 784 d.JWT = d2.JWT 785 d.TileSize = d2.TileSize 786 d.GeomMap = d2.GeomMap 787 d.Scales = d2.Scales 788 d.HighResIndex = d2.HighResIndex 789 d.oa2conf = d2.oa2conf 790 791 return nil 792 } 793 794 // MarshalJSON handles JSON serialization for googlevoxels Data. It adds "Levels" metadata equivalent 795 // to imagetile's tile specification so clients can treat googlevoxels tile API identically to 796 // imagetile. Sensitive information like AuthKey are withheld. 797 func (p Properties) MarshalJSON() ([]byte, error) { 798 var minTileCoord, maxTileCoord dvid.Point3d 799 if len(p.Scales) > 0 { 800 vol := p.Scales[0].VolumeSize 801 maxX := vol[0] / p.TileSize 802 if vol[0]%p.TileSize > 0 { 803 maxX++ 804 } 805 maxY := vol[1] / p.TileSize 806 if vol[1]%p.TileSize > 0 { 807 maxY++ 808 } 809 maxZ := vol[2] / p.TileSize 810 if vol[2]%p.TileSize > 0 { 811 maxZ++ 812 } 813 maxTileCoord = dvid.Point3d{maxX, maxY, maxZ} 814 } 815 return json.Marshal(struct { 816 VolumeID string 817 MinTileCoord dvid.Point3d 818 MaxTileCoord dvid.Point3d 819 TileSize int32 820 GeomMap GeometryMap 821 Scales Geometries 822 HighResIndex GeometryIndex 823 Levels imagetile.TileSpec 824 }{ 825 p.VolumeID, 826 minTileCoord, 827 maxTileCoord, 828 p.TileSize, 829 p.GeomMap, 830 p.Scales, 831 p.HighResIndex, 832 getGSpec(p.TileSize, p.Scales[p.HighResIndex], p.GeomMap), 833 }) 834 } 835 836 // Converts Google BrainMaps scaling to imagetile-style tile specifications. 837 // This assumes that Google levels always downsample by 2. 838 func getGSpec(tileSize int32, hires Geometry, geomMap GeometryMap) imagetile.TileSpec { 839 // Determine how many levels we have by the max of any orientation. 840 // TODO -- Warn user in some way if BrainMaps API has levels in one orientation but not in other. 841 var maxScale Scaling 842 for tileSpec := range geomMap { 843 if tileSpec.scaling > maxScale { 844 maxScale = tileSpec.scaling 845 } 846 } 847 848 // Create the levels from 0 (hires) to max level. 849 levelSpec := imagetile.LevelSpec{ 850 TileSize: dvid.Point3d{tileSize, tileSize, tileSize}, 851 } 852 levelSpec.Resolution = make(dvid.NdFloat32, 3) 853 copy(levelSpec.Resolution, hires.PixelSize) 854 ms2dGSpec := make(imagetile.TileSpec, maxScale+1) 855 for scale := Scaling(0); scale <= maxScale; scale++ { 856 curSpec := levelSpec.Duplicate() 857 ms2dGSpec[imagetile.Scaling(scale)] = imagetile.TileScaleSpec{LevelSpec: curSpec} 858 levelSpec.Resolution[0] *= 2 859 levelSpec.Resolution[1] *= 2 860 levelSpec.Resolution[2] *= 2 861 } 862 return ms2dGSpec 863 } 864 865 // Data embeds the datastore's Data and extends it with voxel-specific properties. 866 type Data struct { 867 *datastore.Data 868 Properties 869 870 client *http.Client // HTTP client that provides Authorization headers 871 } 872 873 // GetClient returns a potentially cached client that handles authorization to Google. 874 // Assumes a JSON Web Token has been loaded into Data or else returns an error. 875 func (d *Data) GetClient() (*http.Client, error) { 876 if d.client != nil { 877 return d.client, nil 878 } 879 if d.Properties.JWT == "" { 880 return nil, fmt.Errorf("No JSON Web Token has been set for this data") 881 } 882 conf, err := google.JWTConfigFromJSON([]byte(d.Properties.JWT), "https://www.googleapis.com/auth/brainmaps") 883 if err != nil { 884 return nil, fmt.Errorf("Cannot establish JWT Config file from Google: %v", err) 885 } 886 client := conf.Client(oauth2.NoContext) 887 d.client = client 888 return client, nil 889 } 890 891 func (d *Data) GetVoxelSize(ts *GSpec) (dvid.NdFloat32, error) { 892 if d.Scales == nil || len(d.Scales) == 0 { 893 return nil, fmt.Errorf("%s has no geometries and therefore no volumes for access", d.DataName()) 894 } 895 if d.GeomMap == nil { 896 return nil, fmt.Errorf("%s has not been initialized and can't return voxel sizes", d.DataName()) 897 } 898 if ts == nil { 899 return nil, fmt.Errorf("Can't get voxel sizes for nil tile spec!") 900 } 901 scaleIndex := d.GeomMap[*ts] 902 if int(scaleIndex) > len(d.Scales) { 903 return nil, fmt.Errorf("Can't map tile spec (%v) to available geometries", *ts) 904 } 905 geom := d.Scales[scaleIndex] 906 return geom.PixelSize, nil 907 } 908 909 func (d *Data) MarshalJSON() ([]byte, error) { 910 return json.Marshal(struct { 911 Base *datastore.Data 912 Extended Properties 913 }{ 914 d.Data, 915 d.Properties, 916 }) 917 } 918 919 func (d *Data) GobDecode(b []byte) error { 920 buf := bytes.NewBuffer(b) 921 dec := gob.NewDecoder(buf) 922 if err := dec.Decode(&(d.Data)); err != nil { 923 return err 924 } 925 if err := dec.Decode(&(d.Properties)); err != nil { 926 return err 927 } 928 return nil 929 } 930 931 func (d *Data) GobEncode() ([]byte, error) { 932 var buf bytes.Buffer 933 enc := gob.NewEncoder(&buf) 934 if err := enc.Encode(d.Data); err != nil { 935 return nil, err 936 } 937 if err := enc.Encode(d.Properties); err != nil { 938 return nil, err 939 } 940 return buf.Bytes(), nil 941 } 942 943 // --- DataService interface --- 944 945 func (d *Data) Help() string { 946 return helpMessage 947 } 948 949 // getBlankTileData returns a background 2d tile data 950 func (d *Data) getBlankTileImage(tile *GoogleSubvolGeom) (image.Image, error) { 951 if tile == nil { 952 return nil, fmt.Errorf("Can't get blank tile for unknown tile spec") 953 } 954 if d.Scales == nil || len(d.Scales) <= int(tile.gi) { 955 return nil, fmt.Errorf("Scaled volumes for %s not suitable for tile spec: %d scales <= %d tile scales", d.DataName(), len(d.Scales), int(tile.gi)) 956 } 957 958 // Generate the blank image 959 numBytes := tile.sizeWant[0] * tile.sizeWant[1] * tile.bytesPerVoxel 960 data := make([]byte, numBytes, numBytes) 961 return dvid.GoImageFromData(data, int(tile.sizeWant[0]), int(tile.sizeWant[1])) 962 } 963 964 func (d *Data) serveTile(w http.ResponseWriter, r *http.Request, geom *GoogleSubvolGeom, formatStr string, noblanks bool) error { 965 // If it's outside, write blank tile unless user wants no blanks. 966 if geom.outside { 967 if noblanks { 968 http.NotFound(w, r) 969 return fmt.Errorf("Requested tile is outside of available volume.") 970 } 971 img, err := d.getBlankTileImage(geom) 972 if err != nil { 973 return err 974 } 975 return dvid.WriteImageHttp(w, img, formatStr) 976 } 977 978 // If we are within volume, get data from Google. 979 url, imgOptions, err := geom.GetURL(d.VolumeID, formatStr) 980 if err != nil { 981 return err 982 } 983 984 timedLog := dvid.NewTimeLog() 985 client, err := d.GetClient() 986 if err != nil { 987 dvid.Errorf("Can't get OAuth2 connection to Google: %v\n", err) 988 return err 989 } 990 resp, err := client.Post(url, "application/json", imgOptions) 991 if err != nil { 992 return err 993 } 994 timedLog.Infof("PROXY HTTP to Google: %s, returned response %d", url, resp.StatusCode) 995 defer resp.Body.Close() 996 997 // Set the image header 998 if err := dvid.SetImageHeader(w, formatStr); err != nil { 999 return err 1000 } 1001 1002 // If it's on edge, we need to pad the tile to the tile size. 1003 if geom.edge { 1004 // We need to read whole thing in to pad it. 1005 data, err := ioutil.ReadAll(resp.Body) 1006 timedLog.Infof("Got edge tile from Google, %d bytes\n", len(data)) 1007 if err != nil { 1008 return err 1009 } 1010 paddedData, err := geom.padData(data) 1011 if err != nil { 1012 return err 1013 } 1014 _, err = w.Write(paddedData) 1015 return err 1016 } 1017 1018 // If we aren't on edge or outside, our return status should be OK. 1019 if resp.StatusCode != http.StatusOK { 1020 return fmt.Errorf("Unexpected status code %d on tile request (%q, volume id %q)", resp.StatusCode, d.DataName(), d.VolumeID) 1021 } 1022 1023 // Just send the data as we get it from Google in chunks. 1024 respBytes := 0 1025 const BufferSize = 32 * 1024 1026 buf := make([]byte, BufferSize) 1027 for { 1028 n, err := resp.Body.Read(buf) 1029 respBytes += n 1030 eof := (err == io.EOF) 1031 if err != nil && !eof { 1032 return err 1033 } 1034 if _, err = w.Write(buf[:n]); err != nil { 1035 return err 1036 } 1037 if f, ok := w.(http.Flusher); ok { 1038 f.Flush() 1039 } 1040 if eof { 1041 break 1042 } 1043 } 1044 timedLog.Infof("Got non-edge tile from Google, %d bytes\n", respBytes) 1045 return nil 1046 } 1047 1048 func (d *Data) serveVolume(w http.ResponseWriter, r *http.Request, geom *GoogleSubvolGeom, noblanks bool, formatstr string) error { 1049 // If it's outside, write blank tile unless user wants no blanks. 1050 if geom.outside { 1051 if noblanks { 1052 http.NotFound(w, r) 1053 return fmt.Errorf("Requested subvolume is outside of available volume.") 1054 } 1055 return nil 1056 } 1057 1058 // If we are within volume, get data from Google. 1059 url, imgOptions, err := geom.GetURL(d.VolumeID, formatstr) 1060 if err != nil { 1061 return err 1062 } 1063 1064 timedLog := dvid.NewTimeLog() 1065 client, err := d.GetClient() 1066 if err != nil { 1067 dvid.Errorf("Can't get OAuth2 connection to Google: %v\n", err) 1068 return err 1069 } 1070 resp, err := client.Post(url, "application/json", imgOptions) 1071 if err != nil { 1072 return err 1073 } 1074 timedLog.Infof("PROXY HTTP to Google: %s, returned response %d", url, resp.StatusCode) 1075 defer resp.Body.Close() 1076 1077 // If it's on edge, we need to pad the subvolume to the requested size. 1078 if geom.edge { 1079 return fmt.Errorf("Googlevoxels subvolume GET does not pad data on edge at this time") 1080 } 1081 1082 // If we aren't on edge or outside, our return status should be OK. 1083 if resp.StatusCode != http.StatusOK { 1084 return fmt.Errorf("Unexpected status code %d on volume request (%q, volume id %q)", resp.StatusCode, d.DataName(), d.VolumeID) 1085 } 1086 1087 w.Header().Set("Content-type", "application/octet-stream") 1088 1089 queryStrings := r.URL.Query() 1090 compression := queryStrings.Get("compression") 1091 1092 switch compression { 1093 case "lz4": 1094 data, err := ioutil.ReadAll(resp.Body) 1095 timedLog.Infof("Got raw subvolume from Google, %d bytes\n", len(data)) 1096 if err != nil { 1097 return err 1098 } 1099 1100 // Recompress and transmit as lz4 1101 lz4data := make([]byte, lz4.CompressBound(data)) 1102 outSize, err := lz4.Compress(data, lz4data) 1103 if err != nil { 1104 return err 1105 } 1106 if _, err := w.Write(lz4data[:outSize]); err != nil { 1107 return err 1108 } 1109 timedLog.Infof("Sent lz4-encoded subvolume from DVID, %d bytes\n", outSize) 1110 1111 case "", "raw": 1112 // Just stream raw data from Google 1113 respBytes := 0 1114 const BufferSize = 32 * 1024 1115 buf := make([]byte, BufferSize) 1116 for { 1117 n, err := resp.Body.Read(buf) 1118 respBytes += n 1119 eof := (err == io.EOF) 1120 if err != nil && !eof { 1121 return err 1122 } 1123 if _, err = w.Write(buf[:n]); err != nil { 1124 return err 1125 } 1126 if f, ok := w.(http.Flusher); ok { 1127 f.Flush() 1128 } 1129 if eof { 1130 break 1131 } 1132 } 1133 timedLog.Infof("Proxied encoded subvolume from Google, %d bytes\n", respBytes) 1134 default: 1135 return fmt.Errorf("unknown compression requested %q", compression) 1136 } 1137 1138 return nil 1139 } 1140 1141 // See if scaling was specified in query string, otherwise return high-res (scale 0) 1142 func getScale(r *http.Request) (Scaling, error) { 1143 var scale Scaling 1144 queryStrings := r.URL.Query() 1145 scalingStr := queryStrings.Get("scale") 1146 if scalingStr != "" { 1147 scale64, err := strconv.ParseUint(scalingStr, 10, 8) 1148 if err != nil { 1149 return 0, fmt.Errorf("Illegal tile scale: %s (%v)", scalingStr, err) 1150 } 1151 scale = Scaling(scale64) 1152 } 1153 return scale, nil 1154 } 1155 1156 func (d *Data) handleImage2d(w http.ResponseWriter, r *http.Request, parts []string) error { 1157 return nil 1158 } 1159 1160 // handleImageReq returns an image with appropriate Content-Type set. This function differs 1161 // from handleTileReq in the way parameters are passed to it. handleTileReq accepts a tile coordinate. 1162 // This function allows arbitrary offset and size, unconstrained by tile sizes. 1163 func (d *Data) handleImageReq(w http.ResponseWriter, r *http.Request, parts []string) error { 1164 if len(parts) < 7 { 1165 return fmt.Errorf("%q must be followed by shape/size/offset", parts[3]) 1166 } 1167 shapeStr, sizeStr, offsetStr := parts[4], parts[5], parts[6] 1168 planeStr := dvid.DataShapeString(shapeStr) 1169 plane, err := planeStr.DataShape() 1170 if err != nil { 1171 return err 1172 } 1173 1174 var size dvid.Point 1175 if size, err = dvid.StringToPoint(sizeStr, "_"); err != nil { 1176 return err 1177 } 1178 offset, err := dvid.StringToPoint3d(offsetStr, "_") 1179 if err != nil { 1180 return err 1181 } 1182 1183 // Determine how this request sits in the available scaled volumes. 1184 scale, err := getScale(r) 1185 if err != nil { 1186 return err 1187 } 1188 geom, err := d.GetGoogleSubvolGeom(scale, plane, offset, size) 1189 if err != nil { 1190 return err 1191 } 1192 1193 switch plane.ShapeDimensions() { 1194 case 2: 1195 var formatStr string 1196 if len(parts) >= 8 { 1197 formatStr = parts[7] 1198 } 1199 if formatStr == "" { 1200 formatStr = DefaultTileFormat 1201 } 1202 1203 return d.serveTile(w, r, geom, formatStr, false) 1204 case 3: 1205 if len(parts) >= 8 { 1206 return d.serveVolume(w, r, geom, false, parts[7]) 1207 } else { 1208 return d.serveVolume(w, r, geom, false, "") 1209 } 1210 } 1211 return nil 1212 } 1213 1214 // handleTileReq returns a tile with appropriate Content-Type set. 1215 func (d *Data) handleTileReq(w http.ResponseWriter, r *http.Request, parts []string) error { 1216 1217 if len(parts) < 7 { 1218 return fmt.Errorf("'tile' request must be following by plane, scale level, and tile coordinate") 1219 } 1220 planeStr, scalingStr, coordStr := parts[4], parts[5], parts[6] 1221 queryStrings := r.URL.Query() 1222 1223 var noblanks bool 1224 noblanksStr := dvid.InstanceName(queryStrings.Get("noblanks")) 1225 if noblanksStr == "true" { 1226 noblanks = true 1227 } 1228 1229 var tilesize int32 = DefaultTileSize 1230 tileSizeStr := queryStrings.Get("tilesize") 1231 if tileSizeStr != "" { 1232 tilesizeInt, err := strconv.Atoi(tileSizeStr) 1233 if err != nil { 1234 return err 1235 } 1236 tilesize = int32(tilesizeInt) 1237 } 1238 size := dvid.Point2d{tilesize, tilesize} 1239 1240 var formatStr string 1241 if len(parts) >= 8 { 1242 formatStr = parts[7] 1243 } 1244 if formatStr == "" { 1245 formatStr = DefaultTileFormat 1246 } 1247 1248 // Parse the tile specification 1249 plane := dvid.DataShapeString(planeStr) 1250 shape, err := plane.DataShape() 1251 if err != nil { 1252 err = fmt.Errorf("Illegal tile plane: %s (%v)", planeStr, err) 1253 server.BadRequest(w, r, err) 1254 return err 1255 } 1256 scale, err := strconv.ParseUint(scalingStr, 10, 8) 1257 if err != nil { 1258 err = fmt.Errorf("Illegal tile scale: %s (%v)", scalingStr, err) 1259 server.BadRequest(w, r, err) 1260 return err 1261 } 1262 tileCoord, err := dvid.StringToPoint(coordStr, "_") 1263 if err != nil { 1264 err = fmt.Errorf("Illegal tile coordinate: %s (%v)", coordStr, err) 1265 server.BadRequest(w, r, err) 1266 return err 1267 } 1268 1269 // Convert tile coordinate to offset. 1270 var ox, oy, oz int32 1271 switch { 1272 case shape.Equals(dvid.XY): 1273 ox = tileCoord.Value(0) * tilesize 1274 oy = tileCoord.Value(1) * tilesize 1275 oz = tileCoord.Value(2) 1276 case shape.Equals(dvid.XZ): 1277 ox = tileCoord.Value(0) * tilesize 1278 oy = tileCoord.Value(1) 1279 oz = tileCoord.Value(2) * tilesize 1280 case shape.Equals(dvid.YZ): 1281 ox = tileCoord.Value(0) 1282 oy = tileCoord.Value(1) * tilesize 1283 oz = tileCoord.Value(2) * tilesize 1284 default: 1285 return fmt.Errorf("Unknown tile orientation: %s", shape) 1286 } 1287 1288 // Determine how this request sits in the available scaled volumes. 1289 geom, err := d.GetGoogleSubvolGeom(Scaling(scale), shape, dvid.Point3d{ox, oy, oz}, size) 1290 if err != nil { 1291 server.BadRequest(w, r, err) 1292 return err 1293 } 1294 1295 // Send the tile. 1296 return d.serveTile(w, r, geom, formatStr, noblanks) 1297 } 1298 1299 // DoRPC handles the 'generate' command. 1300 func (d *Data) DoRPC(request datastore.Request, reply *datastore.Response) error { 1301 return fmt.Errorf("Unknown command. Data instance %q does not support any commands. See API help.", d.DataName()) 1302 } 1303 1304 // ServeHTTP handles all incoming HTTP requests for this data. 1305 func (d *Data) ServeHTTP(uuid dvid.UUID, ctx *datastore.VersionedCtx, w http.ResponseWriter, r *http.Request) (activity map[string]interface{}) { 1306 timedLog := dvid.NewTimeLog() 1307 1308 action := strings.ToLower(r.Method) 1309 switch action { 1310 case "get": 1311 // Acceptable 1312 default: 1313 server.BadRequest(w, r, "googlevoxels can only handle GET HTTP verbs at this time") 1314 return 1315 } 1316 1317 // Break URL request into arguments 1318 url := r.URL.Path[len(server.WebAPIPath):] 1319 parts := strings.Split(url, "/") 1320 if len(parts[len(parts)-1]) == 0 { 1321 parts = parts[:len(parts)-1] 1322 } 1323 if len(parts) < 4 { 1324 server.BadRequest(w, r, "incomplete API request") 1325 return 1326 } 1327 1328 switch parts[3] { 1329 case "help": 1330 w.Header().Set("Content-Type", "text/plain") 1331 fmt.Fprintln(w, d.Help()) 1332 1333 case "info": 1334 jsonBytes, err := d.MarshalJSON() 1335 if err != nil { 1336 server.BadRequest(w, r, err) 1337 return 1338 } 1339 w.Header().Set("Content-Type", "application/json") 1340 fmt.Fprintf(w, string(jsonBytes)) 1341 1342 case "tile": 1343 if err := d.handleTileReq(w, r, parts); err != nil { 1344 server.BadRequest(w, r, err) 1345 return 1346 } 1347 timedLog.Infof("HTTP %s: tile (%s)", r.Method, r.URL) 1348 1349 case "raw": 1350 queryStrings := r.URL.Query() 1351 if throttle := queryStrings.Get("throttle"); throttle == "on" || throttle == "true" { 1352 if server.ThrottledHTTP(w) { 1353 return 1354 } 1355 defer server.ThrottledOpDone() 1356 } 1357 if err := d.handleImageReq(w, r, parts); err != nil { 1358 server.BadRequest(w, r, err) 1359 return 1360 } 1361 timedLog.Infof("HTTP %s: image (%s)", r.Method, r.URL) 1362 default: 1363 server.BadAPIRequest(w, r, d) 1364 } 1365 return 1366 }