1package bundle23import (4 "encoding/binary"5 "fmt"6 "math"7)89const (10 locationMagic = "ATLASLOC"11 // 2 added the shard column. A map split into layers offers one at a time,12 // and without it every layer's locations were drawn over every other.13 // 3 moved the meaning without moving a byte: owner now indexes the world14 // payload's collections array rather than a flattened category order, and15 // the region column reads as Member -- the id of the area feature that16 // contains the point, zero for none.17 locationVersion = 318)1920// Location is one point feature as the packed payload carries it. Owner is21// the position of the feature's collection in the world payload's22// collections array, which is the only collection identity the viewer needs23// once the payload JSON has listed the collections in that same order.24// Member is the id of the area feature containing this point, zero for none.25type Location struct {26 ID int6427 Title string28 Lat float6429 Lng float6430 Member int6431 Shard int6432 Owner uint1633}3435// PackLocations lays the locations out as parallel arrays, four-byte fields36// first so a reader can view each one directly without copying or realigning.37// Coordinates are single precision, which resolves far finer than a world38// is drawn.39func PackLocations(locations []Location) []byte {40 count := len(locations)4142 titles := make([]byte, 0, count*12)43 offsets := make([]uint32, count+1)44 for index, location := range locations {45 offsets[index] = uint32(len(titles))46 titles = append(titles, location.Title...)47 }48 offsets[count] = uint32(len(titles))4950 size := 16 + count*20 + (count+1)*4 + count*2 + len(titles)51 out := make([]byte, size)52 copy(out, locationMagic)53 binary.LittleEndian.PutUint16(out[8:], locationVersion)54 binary.LittleEndian.PutUint32(out[10:], uint32(count))55 // out[14:16] is reserved, and keeps the arrays four-byte aligned.5657 at := 1658 put32 := func(value uint32) {59 binary.LittleEndian.PutUint32(out[at:], value)60 at += 461 }62 for _, location := range locations {63 put32(uint32(int32(location.ID)))64 }65 for _, location := range locations {66 put32(math.Float32bits(float32(location.Lat)))67 }68 for _, location := range locations {69 put32(math.Float32bits(float32(location.Lng)))70 }71 for _, location := range locations {72 put32(uint32(int32(location.Member)))73 }74 for _, location := range locations {75 put32(uint32(int32(location.Shard)))76 }77 for _, offset := range offsets {78 put32(offset)79 }80 for _, location := range locations {81 binary.LittleEndian.PutUint16(out[at:], location.Owner)82 at += 283 }84 copy(out[at:], titles)85 return out86}8788// UnpackLocations is the reader of PackLocations, the same layout the viewer89// decodes in the browser. It exists so a change to the packing that the90// viewer could not read fails in Go -- in the bundler's validation and in91// tests -- rather than on the map.92func UnpackLocations(data []byte) ([]Location, error) {93 if len(data) < 16 || string(data[:8]) != locationMagic {94 return nil, fmt.Errorf("location payload is not in the expected form")95 }96 if version := binary.LittleEndian.Uint16(data[8:]); version != locationVersion {97 return nil, fmt.Errorf("location payload is version %d, and this reads %d", version, locationVersion)98 }99 count := int(binary.LittleEndian.Uint32(data[10:]))100 if size := 16 + count*20 + (count+1)*4 + count*2; count < 0 || len(data) < size {101 return nil, fmt.Errorf("location payload is truncated")102 }103104 at := 16105 u32 := func(index int) uint32 { return binary.LittleEndian.Uint32(data[at+index*4:]) }106 next := func(bytes int) { at += bytes }107108 out := make([]Location, count)109 for index := range out {110 out[index].ID = int64(int32(u32(index)))111 }112 next(count * 4)113 for index := range out {114 out[index].Lat = float64(math.Float32frombits(u32(index)))115 }116 next(count * 4)117 for index := range out {118 out[index].Lng = float64(math.Float32frombits(u32(index)))119 }120 next(count * 4)121 for index := range out {122 out[index].Member = int64(int32(u32(index)))123 }124 next(count * 4)125 for index := range out {126 out[index].Shard = int64(int32(u32(index)))127 }128 next(count * 4)129 offsets := make([]uint32, count+1)130 for index := range offsets {131 offsets[index] = u32(index)132 }133 next((count + 1) * 4)134 for index := range out {135 out[index].Owner = binary.LittleEndian.Uint16(data[at+index*2:])136 }137 next(count * 2)138139 titles := data[at:]140 if count > 0 && int(offsets[count]) > len(titles) {141 return nil, fmt.Errorf("location titles are truncated")142 }143 for index := range out {144 out[index].Title = string(titles[offsets[index]:offsets[index+1]])145 }146 return out, nil147}