github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/cmd/geoviz/geoviz.go (about) 1 // Copyright 2020 The Cockroach Authors. 2 // 3 // Use of this software is governed by the Business Source License 4 // included in the file licenses/BSL.txt. 5 // 6 // As of the Change Date specified in that file, in accordance with 7 // the Business Source License, use of this software will be governed 8 // by the Apache License, Version 2.0, included in the file 9 // licenses/APL.txt. 10 11 package main 12 13 import ( 14 "crypto/sha1" 15 "encoding/csv" 16 "fmt" 17 "io" 18 "strconv" 19 "strings" 20 21 "github.com/cockroachdb/cockroach/pkg/geo" 22 "github.com/cockroachdb/cockroach/pkg/geo/geoindex" 23 "github.com/golang/geo/s2" 24 ) 25 26 // Image represents an image to build. 27 type Image struct { 28 Objects []Object `json:"objects"` 29 } 30 31 // LatLng represents the LatLng coordinate. 32 type LatLng struct { 33 Lat float64 `json:"lat"` 34 Lng float64 `json:"lng"` 35 } 36 37 // Polygon represents a polygon object. 38 type Polygon struct { 39 Geodesic bool `json:"geodesic"` 40 StrokeColor string `json:"strokeColor"` 41 FillColor string `json:"fillColor"` 42 Paths [][]LatLng `json:"paths"` 43 Marker Marker `json:"marker"` 44 Label string `json:"label"` 45 } 46 47 // Polyline represents a polyline object. 48 type Polyline struct { 49 Geodesic bool `json:"geodesic"` 50 StrokeColor string `json:"strokeColor"` 51 Path []LatLng `json:"path"` 52 Marker Marker `json:"marker"` 53 } 54 55 // Marker represents a marker object. 56 type Marker struct { 57 Title string `json:"title"` 58 // Content is displayed in an infowindow. 59 Content string `json:"content"` 60 Position LatLng `json:"position"` 61 Label string `json:"label"` 62 } 63 64 // Object is a set of correlated things to draw. 65 type Object struct { 66 Title string `json:"title"` 67 Color string `json:"color"` 68 Polygons []Polygon `json:"polygons"` 69 Polylines []Polyline `json:"polylines"` 70 Markers []Marker `json:"markers"` 71 } 72 73 func s2PointToLatLng(fromPoint s2.Point) LatLng { 74 from := s2.LatLngFromPoint(fromPoint) 75 return LatLng{Lat: from.Lat.Degrees(), Lng: from.Lng.Degrees()} 76 } 77 78 // AddGeography adds a given Geography to an image. 79 func (img *Image) AddGeography(g *geo.Geography, title string, color string) { 80 regions, err := g.AsS2(geo.EmptyBehaviorOmit) 81 if err != nil { 82 panic(err) 83 } 84 object := Object{Title: title, Color: color} 85 for _, region := range regions { 86 switch region := region.(type) { 87 case s2.Point: 88 latlng := s2PointToLatLng(region) 89 content := fmt.Sprintf("<h3>%s (point) (%f,%f)</h3>", title, latlng.Lat, latlng.Lng) 90 object.Markers = append( 91 object.Markers, 92 Marker{ 93 Title: title, 94 Position: latlng, 95 Content: content, 96 Label: title, 97 }, 98 ) 99 case *s2.Polyline: 100 polyline := Polyline{ 101 Geodesic: true, 102 StrokeColor: color, 103 Marker: Marker{Title: title, Content: fmt.Sprintf("<h3>%s (line)</h3>", title)}, 104 } 105 for _, point := range *region { 106 polyline.Path = append(polyline.Path, s2PointToLatLng(point)) 107 } 108 object.Polylines = append(object.Polylines, polyline) 109 case *s2.Polygon: 110 polygon := Polygon{ 111 Geodesic: true, 112 StrokeColor: color, 113 FillColor: color, 114 Marker: Marker{Title: title, Content: fmt.Sprintf("<h3>%s (polygon)</h3>", title)}, 115 Label: title, 116 } 117 outerLoop := []LatLng{} 118 for _, point := range region.Loop(0).Vertices() { 119 outerLoop = append(outerLoop, s2PointToLatLng(point)) 120 } 121 innerLoops := [][]LatLng{} 122 for _, loop := range region.Loops()[1:] { 123 vertices := loop.Vertices() 124 loopRet := make([]LatLng, len(vertices)) 125 for i, vertex := range vertices { 126 loopRet[len(vertices)-1-i] = s2PointToLatLng(vertex) 127 } 128 innerLoops = append(innerLoops, loopRet) 129 } 130 polygon.Paths = append([][]LatLng{outerLoop}, innerLoops...) 131 object.Polygons = append(object.Polygons, polygon) 132 } 133 } 134 img.Objects = append(img.Objects, object) 135 } 136 137 // AddS2Cells adds S2Cells to an image. 138 func (img *Image) AddS2Cells(title string, color string, cellIDs ...s2.CellID) { 139 object := Object{Title: title, Color: color} 140 for _, cellID := range cellIDs { 141 cell := s2.CellFromCellID(cellID) 142 loop := []LatLng{} 143 // Draws on 4 corners. 144 for i := 0; i < 4; i++ { 145 point := cell.Vertex(i) 146 loop = append(loop, s2PointToLatLng(point)) 147 } 148 content := fmt.Sprintf( 149 "<h3>%s (S2 cell)</h3><br/>cell ID: %d<br/>cell string: %s<br/>cell token: %s<br/>cell binary: %064b", 150 title, 151 cellID, 152 cellID.String(), 153 cellID.ToToken(), 154 cellID, 155 ) 156 polygon := Polygon{ 157 Geodesic: true, 158 StrokeColor: color, 159 FillColor: color, 160 Paths: [][]LatLng{loop}, 161 Marker: Marker{Title: title, Content: content}, 162 Label: title, 163 } 164 object.Polygons = append(object.Polygons, polygon) 165 } 166 img.Objects = append(img.Objects, object) 167 } 168 169 // ImageFromReader returns an Image based on a reader. 170 func ImageFromReader(r io.Reader) (*Image, error) { 171 csvReader := csv.NewReader(r) 172 gviz := &Image{} 173 recordNum := 0 174 for { 175 record, err := csvReader.Read() 176 if err == io.EOF { 177 break 178 } 179 if err != nil { 180 return nil, err 181 } 182 recordNum++ 183 184 if len(record) != 4 { 185 return nil, fmt.Errorf("records must be of format op,name(can be blank),color(can be blank),data") 186 } 187 op, name, color, data := record[0], record[1], record[2], record[3] 188 // If color is blank, fill it in with a consistent hash. 189 if color == "" { 190 hashed := sha1.Sum([]byte(op + name + data)) 191 color = fmt.Sprintf("#%x", hashed[:3]) 192 } 193 // If name is blank fill it in with a consistent hash. 194 if name == "" { 195 name = fmt.Sprintf("shape #%d", recordNum) 196 } 197 switch op { 198 case "draw", "drawgeography": 199 g, err := geo.ParseGeography(data) 200 if err != nil { 201 return nil, fmt.Errorf("error parsing: %s", data) 202 } 203 gviz.AddGeography(g, name, color) 204 case "innercovering": 205 g, err := geo.ParseGeography(data) 206 if err != nil { 207 return nil, fmt.Errorf("error parsing: %s", data) 208 } 209 index := geoindex.NewS2GeographyIndex(geoindex.S2GeographyConfig{ 210 S2Config: &geoindex.S2Config{ 211 MinLevel: 4, 212 MaxLevel: 30, 213 MaxCells: 20, 214 }, 215 }) 216 gviz.AddS2Cells(name, color, index.TestingInnerCovering(g)...) 217 case "drawcellid": 218 cellIDs := []s2.CellID{} 219 for _, d := range strings.Split(data, ",") { 220 parsed, err := strconv.ParseInt(d, 10, 64) 221 if err != nil { 222 return nil, fmt.Errorf("error parsing: %s", data) 223 } 224 cellIDs = append(cellIDs, s2.CellID(parsed)) 225 } 226 gviz.AddS2Cells(name, color, cellIDs...) 227 case "drawcelltoken": 228 cellIDs := []s2.CellID{} 229 for _, d := range strings.Split(data, ",") { 230 cellID := s2.CellIDFromToken(d) 231 if cellID == 0 { 232 return nil, fmt.Errorf("error parsing: %s", data) 233 } 234 cellIDs = append(cellIDs, cellID) 235 } 236 gviz.AddS2Cells(name, color, cellIDs...) 237 case "covering": 238 g, err := geo.ParseGeography(data) 239 if err != nil { 240 return nil, fmt.Errorf("error parsing: %s", data) 241 } 242 gS2, err := g.AsS2(geo.EmptyBehaviorOmit) 243 if err != nil { 244 return nil, err 245 } 246 rc := &s2.RegionCoverer{MinLevel: 0, MaxLevel: 30, MaxCells: 4} 247 cellIDs := []s2.CellID{} 248 for _, s := range gS2 { 249 cellIDs = append(cellIDs, rc.Covering(s)...) 250 } 251 gviz.AddS2Cells(name, color, cellIDs...) 252 case "interiorcovering": 253 g, err := geo.ParseGeography(data) 254 if err != nil { 255 return nil, fmt.Errorf("error parsing: %s", data) 256 } 257 gS2, err := g.AsS2(geo.EmptyBehaviorOmit) 258 if err != nil { 259 return nil, err 260 } 261 rc := &s2.RegionCoverer{MinLevel: 0, MaxLevel: 30, MaxCells: 4} 262 cellIDs := []s2.CellID{} 263 for _, s := range gS2 { 264 cellIDs = append(cellIDs, rc.Covering(s)...) 265 } 266 gviz.AddS2Cells(name, color, cellIDs...) 267 } 268 } 269 return gviz, nil 270 }