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  }