go.charczuk.com@v0.0.0-20240327042549-bc490516bd1a/experiments/open-sky-dump/main.go (about) 1 /* 2 3 Copyright (c) 2023 - Present. Will Charczuk. All rights reserved. 4 Use of this source code is governed by a MIT license that can be found in the LICENSE file at the root of the repository. 5 6 */ 7 8 package main 9 10 import ( 11 "bytes" 12 "context" 13 "encoding/json" 14 "fmt" 15 "io" 16 "math" 17 "os" 18 "strconv" 19 "strings" 20 "time" 21 22 "go.charczuk.com/sdk/cliutil" 23 "go.charczuk.com/sdk/db" 24 "go.charczuk.com/sdk/db/migration" 25 "go.charczuk.com/sdk/logutil" 26 "go.charczuk.com/sdk/quad" 27 "go.charczuk.com/sdk/uuid" 28 ) 29 30 func loadFile(path string) (*File, error) { 31 f, err := os.Open(path) 32 if err != nil { 33 return nil, err 34 } 35 defer f.Close() 36 37 var file File 38 if err = json.NewDecoder(f).Decode(&file); err != nil { 39 return nil, err 40 } 41 return &file, nil 42 } 43 44 func main() { 45 dbConfig := db.Config{ 46 Port: "26257", 47 Database: "spatial_test", 48 } 49 _ = dbConfig.Resolve(context.Background()) 50 51 conn, err := db.New(db.OptConfig(dbConfig)) 52 if err != nil { 53 cliutil.Fatal(err) 54 } 55 56 l := logutil.New() 57 logutil.Infof(l, "using connection string: %q", dbConfig.CreateLoggingDSN()) 58 59 if err = conn.Open(); err != nil { 60 cliutil.Fatal(err) 61 } 62 63 if err = ensureSchema(context.Background(), conn); err != nil { 64 cliutil.Fatal(err) 65 } 66 67 filePath := "_testdata/2023-08-07-states_all.json" 68 file, err := loadFile(filePath) 69 if err != nil { 70 cliutil.Fatal(err) 71 } 72 73 // insert all the vectors from the file 74 for _, vec := range file.States { 75 if err = createStateVector(context.Background(), conn, vec); err != nil { 76 cliutil.Fatal(err) 77 } 78 } 79 } 80 81 func createStateVector(ctx context.Context, conn *db.Connection, vec StateVector) error { 82 _, err := conn.Exec(`INSERT INTO point_of_interest ( 83 timestamp_utc 84 , latitude 85 , longitude 86 , geom 87 , callsign 88 , icao24 89 , origin_country 90 , baro_altitude 91 , on_ground 92 , velocity 93 ) VALUES($1, $2, $3, ST_SetSRID(ST_Makepoint($3,$2), 4326), $4, $5, $6, $7, $8, $9)`, time.Now().UTC(), vec.Latitude, vec.Longitude, vec.Callsign, vec.ICAO24, vec.OriginCountry, vec.BaroAltitude, vec.OnGround, vec.Velocity) 94 if err != nil { 95 return fmt.Errorf("error inserting vector: %w", err) 96 } 97 return nil 98 } 99 100 func ensureSchema(ctx context.Context, conn *db.Connection) error { 101 tableExists, err := migration.PredicateTableExists(ctx, conn, nil, "point_of_interest") 102 if err != nil { 103 return err 104 } 105 if !tableExists { 106 _, err := conn.ExecContext(ctx, ` 107 CREATE TABLE point_of_interest ( 108 id uuid not null default gen_random_uuid() 109 , timestamp_utc timestamp default current_timestamp 110 , latitude float not null 111 , longitude float not null 112 , geom GEOMETRY not null 113 , callsign text 114 , icao24 text 115 , origin_country text 116 , baro_altitude float 117 , on_ground boolean 118 , velocity float 119 , CONSTRAINT "primary" PRIMARY KEY (id) 120 , INVERTED INDEX geom_idx (geom) 121 , FAMILY "primary" (id, geom) 122 )`) 123 if err != nil { 124 return err 125 } 126 } 127 return nil 128 } 129 130 type PointOfInterest struct { 131 ID uuid.UUID `db:"id,pk"` 132 TimestampUTC time.Time `db:"timestamp_utc"` 133 Latitude float64 `db:"latitude"` 134 Longitude float64 `db:"longitude"` 135 136 Callsign string `db:"callsign"` 137 ICAO24 string `db:"icao24"` 138 OriginCountry string `db:"origin_country"` 139 BaroAltitude float64 `db:"baro_altitude"` 140 OnGround bool `db:"on_ground"` 141 Velocity float64 `db:"velocity"` 142 } 143 144 func (poi PointOfInterest) TableName() string { 145 return "point_of_interest" 146 } 147 148 func doSDK() { 149 filePaths := []string{ 150 "_testdata/2023-08-07-states_all.json", 151 } 152 153 qt := quad.New[StateVector]( 154 quad.OptCenter(quad.Point{ 155 X: 0, 156 Y: 0, 157 }), 158 quad.OptHalfDimensions(quad.Dimension{ 159 Width: 180, // -180 to 180 160 Height: 90, // -90 to 90 161 }), 162 ) 163 var points, pointsOK uint64 164 var ok bool 165 for _, filePath := range filePaths { 166 file, err := loadFile(filePath) 167 if err != nil { 168 cliutil.Fatal(err) 169 } 170 for _, sv := range file.States { 171 points++ 172 ok = qt.Insert(quad.Point{ 173 X: sv.Longitude, 174 Y: sv.Latitude, 175 }, sv) 176 if ok { 177 pointsOK++ 178 } 179 } 180 } 181 182 fmt.Printf("points ok=%d total=%d\n", pointsOK, points) 183 184 // visualize the quadtree 185 svgWidth := 2000 186 svgHeight := 857 187 var falseEasting float64 = 180.0 188 var falseNorthing float64 = -10 189 190 mapHeight := float64(svgHeight) 191 mapHeight2 := mapHeight / 2.0 192 mapWidth := float64(svgWidth) 193 pixelsPerDegree := mapWidth / 360.0 194 195 // latitude == y, or north to south 196 var mapLatitude = func(lat float64) (output int) { 197 latRad := DegreesToRadians(lat + falseNorthing) 198 mercN := math.Log(math.Tan(_pi4 + (latRad / 2.0))) 199 return int(mapHeight2 - (mapWidth * (mercN / (_2pi)))) 200 } 201 202 // longitude == x, or east to west 203 var mapLongitude = func(long float64) (output int) { 204 longFalse := long + falseEasting 205 output = int(longFalse * pixelsPerDegree) 206 return 207 } 208 209 vr, _ := newVectorRenderer(svgWidth, svgHeight) 210 vr.SetSVGAttributes(`baseprofile="tiny" fill="#ececec" viewbox="0 0 2000 857"`) 211 vr.Start() 212 globe, _ := os.ReadFile("_testdata/globe.svg") 213 vr.b.Write(globe) 214 215 vr.SetStrokeWidth(1.0) 216 vr.SetStrokeColor(ColorFromHex("000")) 217 218 qt.VisitDepth(func(n *quad.Tree[StateVector]) { 219 // draw the bounding box for the qt 220 tl, br := n.Bounds() 221 vr.MoveTo(mapLongitude(tl.X), mapLatitude(tl.Y)) 222 vr.LineTo(mapLongitude(br.X), mapLatitude(tl.Y)) 223 vr.LineTo(mapLongitude(br.X), mapLatitude(br.Y)) 224 vr.LineTo(mapLongitude(tl.X), mapLatitude(br.Y)) 225 vr.LineTo(mapLongitude(tl.X), mapLatitude(tl.Y)) 226 vr.Stroke() 227 for _, pv := range n.Values() { 228 vr.Circle(5.0, mapLongitude(pv.Point.X), mapLatitude(pv.Point.Y)) 229 } 230 }) 231 output, err := os.Create("output.svg") 232 cliutil.Fatal(err) 233 defer output.Close() 234 if err = vr.Save(output); err != nil { 235 cliutil.Fatal(err) 236 } 237 } 238 239 type File struct { 240 Time int64 `json:"time"` 241 States []StateVector `json:"states"` 242 } 243 244 type StateVector struct { 245 ICAO24 string `json:"icao24"` 246 Callsign string `json:"callsign"` 247 OriginCountry string `json:"origin_country"` 248 TimePosition int `json:"time_position"` 249 LastContact int `json:"last_contact"` 250 Longitude float64 `json:"longitude"` 251 Latitude float64 `json:"latitude"` 252 BaroAltitude float64 `json:"baro_altitude"` 253 OnGround bool `json:"on_ground"` 254 Velocity float64 `json:"velocity"` 255 TrueTack float64 `json:"true_tack"` 256 VerticalRate float64 `json:"vertical_rate"` 257 Sensors []int `json:"sensors"` 258 GeoAltitude float64 `json:"geo_altitude,omitempty"` 259 Squak string `json:"squak,omitempty"` 260 SPI bool `json:"spi"` 261 PositionSource PositionSource `json:"position_source"` 262 Category Category `json:"category"` 263 } 264 265 func (sv *StateVector) UnmarshalJSON(data []byte) error { 266 var values []any 267 if err := json.Unmarshal(data, &values); err != nil { 268 return err 269 } 270 sv.ICAO24, _ = values[0].(string) 271 sv.Callsign, _ = values[1].(string) 272 sv.OriginCountry, _ = values[2].(string) 273 sv.TimePosition, _ = values[3].(int) 274 sv.LastContact, _ = values[4].(int) 275 sv.Longitude, _ = values[5].(float64) 276 sv.Latitude, _ = values[6].(float64) 277 sv.BaroAltitude, _ = values[7].(float64) 278 sv.OnGround, _ = values[8].(bool) 279 sv.Velocity, _ = values[9].(float64) 280 sv.TrueTack, _ = values[10].(float64) 281 sv.VerticalRate, _ = values[11].(float64) 282 // sv.Sensors, _ = values[12].([]int) 283 sv.GeoAltitude, _ = values[13].(float64) 284 sv.Squak, _ = values[14].(string) 285 sv.SPI, _ = values[15].(bool) 286 287 if len(values) < 17 { 288 return nil 289 } 290 rawPositionSource, _ := values[16].(int) 291 sv.PositionSource = PositionSource(rawPositionSource) 292 293 if len(values) < 18 { 294 return nil 295 } 296 rawCategory, _ := values[17].(int) 297 sv.Category = Category(rawCategory) 298 return nil 299 } 300 301 type PositionSource int 302 303 const ( 304 PositionSourceADSB = 0 305 PositionSourceASTERIX = 1 306 PositionSourceMLAT = 2 307 PositionSourceFLARM = 3 308 ) 309 310 type Category int 311 312 const ( 313 CategoryUnknown = 0 314 CategoryNoADSB = 1 315 CategoryLight = 2 316 CategorySmall = 3 317 CategoryLarge = 3 318 CategoryHighVortexLarge = 4 319 CategoryHeavy = 5 320 CategoryHighPerformance = 6 321 CategoryRotorcraft = 7 322 CategoryGlider = 8 323 CategoryLighterThanAir = 9 324 CategoryParachutist = 10 325 CategoryUltralight = 11 326 CategoryReserved = 13 327 CategoryUnmanned = 14 328 CategorySpace = 15 329 CategorySurfaceEmergency = 16 330 CategorySurfaceService = 17 331 CategoryPointObstacle = 18 332 CategoryClusterObstacle = 19 333 CategoryLineObstacle = 20 334 ) 335 336 // newVectorRenderer returns a new vector renderer. 337 func newVectorRenderer(width, height int) (*vectorRenderer, error) { 338 buffer := bytes.NewBuffer([]byte{}) 339 return &vectorRenderer{ 340 b: buffer, 341 w: width, 342 h: height, 343 c: newCanvas(buffer), 344 s: &Style{}, 345 p: []string{}, 346 dpi: 96, 347 }, nil 348 } 349 350 // vectorRenderer renders chart commands to a bitmap. 351 type vectorRenderer struct { 352 dpi float64 353 w, h int 354 b *bytes.Buffer 355 c *canvas 356 s *Style 357 p []string 358 } 359 360 func (vr *vectorRenderer) Start() { 361 vr.c.Start(vr.w, vr.h) 362 } 363 364 func (vr *vectorRenderer) SetSVGAttributes(attributes string) { 365 vr.c.svgAttributes = attributes 366 } 367 368 func (vr *vectorRenderer) ResetStyle() { 369 vr.s = &Style{Font: vr.s.Font} 370 } 371 372 // GetDPI returns the dpi. 373 func (vr *vectorRenderer) GetDPI() float64 { 374 return vr.dpi 375 } 376 377 // SetDPI implements the interface method. 378 func (vr *vectorRenderer) SetDPI(dpi float64) { 379 vr.dpi = dpi 380 vr.c.dpi = dpi 381 } 382 383 // SetClassName implements the interface method. 384 func (vr *vectorRenderer) SetClassName(classname string) { 385 vr.s.ClassName = classname 386 } 387 388 // SetStrokeColor implements the interface method. 389 func (vr *vectorRenderer) SetStrokeColor(c Color) { 390 vr.s.StrokeColor = c 391 } 392 393 // SetFillColor implements the interface method. 394 func (vr *vectorRenderer) SetFillColor(c Color) { 395 vr.s.FillColor = c 396 } 397 398 // SetLineWidth implements the interface method. 399 func (vr *vectorRenderer) SetStrokeWidth(width float64) { 400 vr.s.StrokeWidth = width 401 } 402 403 // StrokeDashArray sets the stroke dash array. 404 func (vr *vectorRenderer) SetStrokeDashArray(dashArray []float64) { 405 vr.s.StrokeDashArray = dashArray 406 } 407 408 // MoveTo implements the interface method. 409 func (vr *vectorRenderer) MoveTo(x, y int) { 410 vr.p = append(vr.p, fmt.Sprintf("M %d %d", x, y)) 411 } 412 413 // LineTo implements the interface method. 414 func (vr *vectorRenderer) LineTo(x, y int) { 415 vr.p = append(vr.p, fmt.Sprintf("L %d %d", x, y)) 416 } 417 418 // QuadCurveTo draws a quad curve. 419 func (vr *vectorRenderer) QuadCurveTo(cx, cy, x, y int) { 420 vr.p = append(vr.p, fmt.Sprintf("Q%d,%d %d,%d", cx, cy, x, y)) 421 } 422 423 func (vr *vectorRenderer) ArcTo(cx, cy int, rx, ry, startAngle, delta float64) { 424 startAngle = RadianAdd(startAngle, _pi2) 425 endAngle := RadianAdd(startAngle, delta) 426 427 startx := cx + int(rx*math.Sin(startAngle)) 428 starty := cy - int(ry*math.Cos(startAngle)) 429 430 if len(vr.p) > 0 { 431 vr.p = append(vr.p, fmt.Sprintf("L %d %d", startx, starty)) 432 } else { 433 vr.p = append(vr.p, fmt.Sprintf("M %d %d", startx, starty)) 434 } 435 436 endx := cx + int(rx*math.Sin(endAngle)) 437 endy := cy - int(ry*math.Cos(endAngle)) 438 439 dd := RadiansToDegrees(delta) 440 441 largeArcFlag := 0 442 if delta > _pi { 443 largeArcFlag = 1 444 } 445 446 vr.p = append(vr.p, fmt.Sprintf("A %d %d %0.2f %d 1 %d %d", int(rx), int(ry), dd, largeArcFlag, endx, endy)) 447 } 448 449 // Close closes a shape. 450 func (vr *vectorRenderer) Close() { 451 vr.p = append(vr.p, "Z") 452 } 453 454 // Stroke draws the path with no fill. 455 func (vr *vectorRenderer) Stroke() { 456 vr.drawPath(vr.s.GetStrokeOptions()) 457 } 458 459 // Fill draws the path with no stroke. 460 func (vr *vectorRenderer) Fill() { 461 vr.drawPath(vr.s.GetFillOptions()) 462 } 463 464 // FillStroke draws the path with both fill and stroke. 465 func (vr *vectorRenderer) FillStroke() { 466 vr.drawPath(vr.s.GetFillAndStrokeOptions()) 467 } 468 469 // drawPath draws a path. 470 func (vr *vectorRenderer) drawPath(s Style) { 471 vr.c.Path(strings.Join(vr.p, "\n"), vr.s.GetFillAndStrokeOptions()) 472 vr.p = []string{} // clear the path 473 } 474 475 // Circle implements the interface method. 476 func (vr *vectorRenderer) Circle(radius float64, x, y int) { 477 vr.c.Circle(x, y, int(radius), vr.s.GetFillAndStrokeOptions()) 478 } 479 480 // SetFont implements the interface method. 481 func (vr *vectorRenderer) SetFont(f string) { 482 vr.s.Font = f 483 } 484 485 // SetFontColor implements the interface method. 486 func (vr *vectorRenderer) SetFontColor(c Color) { 487 vr.s.FontColor = c 488 } 489 490 // SetFontSize implements the interface method. 491 func (vr *vectorRenderer) SetFontSize(size float64) { 492 vr.s.FontSize = size 493 } 494 495 // Text draws a text blob. 496 func (vr *vectorRenderer) Text(body string, x, y int) { 497 vr.c.Text(x, y, body, vr.s.GetTextOptions()) 498 } 499 500 // SetTextRotation sets the text rotation. 501 func (vr *vectorRenderer) SetTextRotation(radians float64) { 502 vr.c.textTheta = &radians 503 } 504 505 // ClearTextRotation clears the text rotation. 506 func (vr *vectorRenderer) ClearTextRotation() { 507 vr.c.textTheta = nil 508 } 509 510 // Save saves the renderer's contents to a writer. 511 func (vr *vectorRenderer) Save(w io.Writer) error { 512 vr.c.End() 513 _, err := w.Write(vr.b.Bytes()) 514 return err 515 } 516 517 func newCanvas(w io.Writer) *canvas { 518 return &canvas{ 519 w: w, 520 dpi: DefaultDPI, 521 } 522 } 523 524 const DefaultDPI = 96.0 525 526 type canvas struct { 527 w io.Writer 528 dpi float64 529 textTheta *float64 530 width int 531 height int 532 css string 533 nonce string 534 svgAttributes string 535 } 536 537 func (c *canvas) Start(width, height int) { 538 c.width = width 539 c.height = height 540 var attributes string 541 if c.svgAttributes != "" { 542 attributes = " " + c.svgAttributes 543 } 544 c.w.Write([]byte(fmt.Sprintf(`<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="%d" height="%d"%s>\n`, c.width, c.height, attributes))) 545 if c.css != "" { 546 c.w.Write([]byte(`<style type="text/css"`)) 547 if c.nonce != "" { 548 // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy 549 c.w.Write([]byte(fmt.Sprintf(` nonce="%s"`, c.nonce))) 550 } 551 // To avoid compatibility issues between XML and CSS (f.e. with child selectors) we should encapsulate the CSS with CDATA. 552 c.w.Write([]byte(fmt.Sprintf(`><![CDATA[%s]]></style>`, c.css))) 553 } 554 } 555 556 func (c *canvas) Path(d string, style Style) { 557 var strokeDashArrayProperty string 558 if len(style.StrokeDashArray) > 0 { 559 strokeDashArrayProperty = c.getStrokeDashArray(style) 560 } 561 c.w.Write([]byte(fmt.Sprintf(`<path %s d="%s" %s/>`, strokeDashArrayProperty, d, c.styleAsSVG(style)))) 562 } 563 564 func (c *canvas) Text(x, y int, body string, style Style) { 565 if c.textTheta == nil { 566 c.w.Write([]byte(fmt.Sprintf(`<text x="%d" y="%d" %s>%s</text>`, x, y, c.styleAsSVG(style), body))) 567 } else { 568 transform := fmt.Sprintf(` transform="rotate(%0.2f,%d,%d)"`, RadiansToDegrees(*c.textTheta), x, y) 569 c.w.Write([]byte(fmt.Sprintf(`<text x="%d" y="%d" %s%s>%s</text>`, x, y, c.styleAsSVG(style), transform, body))) 570 } 571 } 572 573 func (c *canvas) Circle(x, y, r int, style Style) { 574 c.w.Write([]byte(fmt.Sprintf(`<circle cx="%d" cy="%d" r="%d" %s/>`, x, y, r, c.styleAsSVG(style)))) 575 } 576 577 func (c *canvas) End() { 578 c.w.Write([]byte("</svg>")) 579 } 580 581 // getStrokeDashArray returns the stroke-dasharray property of a style. 582 func (c *canvas) getStrokeDashArray(s Style) string { 583 if len(s.StrokeDashArray) > 0 { 584 var values []string 585 for _, v := range s.StrokeDashArray { 586 values = append(values, fmt.Sprintf("%0.1f", v)) 587 } 588 return "stroke-dasharray=\"" + strings.Join(values, ", ") + "\"" 589 } 590 return "" 591 } 592 593 // GetFontFace returns the font face for the style. 594 func (c *canvas) getFontFace(s Style) string { 595 family := "sans-serif" 596 if s.Font != "" { 597 family = s.Font 598 } 599 return fmt.Sprintf("font-family:%s", family) 600 } 601 602 // styleAsSVG returns the style as a svg style or class string. 603 func (c *canvas) styleAsSVG(s Style) string { 604 sw := s.StrokeWidth 605 sc := s.StrokeColor 606 fc := s.FillColor 607 fs := s.FontSize 608 fnc := s.FontColor 609 610 if s.ClassName != "" { 611 var classes []string 612 classes = append(classes, s.ClassName) 613 if !sc.IsZero() { 614 classes = append(classes, "stroke") 615 } 616 if !fc.IsZero() { 617 classes = append(classes, "fill") 618 } 619 if fs != 0 || s.Font != "" { 620 classes = append(classes, "text") 621 } 622 623 return fmt.Sprintf("class=\"%s\"", strings.Join(classes, " ")) 624 } 625 626 var pieces []string 627 628 if sw != 0 { 629 pieces = append(pieces, "stroke-width:"+fmt.Sprintf("%d", int(sw))) 630 } else { 631 pieces = append(pieces, "stroke-width:0") 632 } 633 634 if !sc.IsZero() { 635 pieces = append(pieces, "stroke:"+sc.String()) 636 } else { 637 pieces = append(pieces, "stroke:none") 638 } 639 640 if !fnc.IsZero() { 641 pieces = append(pieces, "fill:"+fnc.String()) 642 } else if !fc.IsZero() { 643 pieces = append(pieces, "fill:"+fc.String()) 644 } else { 645 pieces = append(pieces, "fill:none") 646 } 647 648 if fs != 0 { 649 pieces = append(pieces, "font-size:"+fmt.Sprintf("%.1fpx", PointsToPixels(c.dpi, fs))) 650 } 651 if s.Font != "" { 652 pieces = append(pieces, c.getFontFace(s)) 653 } 654 return fmt.Sprintf("style=\"%s\"", strings.Join(pieces, ";")) 655 } 656 657 const ( 658 _pi = math.Pi 659 _2pi = 2.0 * math.Pi 660 _3pi4 = (3.0 * math.Pi) / 4.0 661 _4pi3 = (4.0 * math.Pi) / 3.0 662 _3pi2 = (3.0 * math.Pi) / 2.0 663 _5pi4 = (5.0 * math.Pi) / 4.0 664 _7pi4 = (7.0 * math.Pi) / 4.0 665 _pi2 = math.Pi / 2.0 666 _pi4 = math.Pi / 4.0 667 _d2r = (math.Pi / 180.0) 668 _r2d = (180.0 / math.Pi) 669 ) 670 671 // RadiansToDegrees translates a radian value to a degree value. 672 func RadiansToDegrees(value float64) float64 { 673 return math.Mod(value, _2pi) * _r2d 674 } 675 676 // RadianAdd adds a delta to a base in radians. 677 func RadianAdd(base, delta float64) float64 { 678 value := base + delta 679 if value > _2pi { 680 return math.Mod(value, _2pi) 681 } else if value < 0 { 682 return math.Mod(_2pi+value, _2pi) 683 } 684 return value 685 } 686 687 func DegreesToRadians(degrees float64) float64 { 688 return degrees * math.Pi / 180.0 689 } 690 691 // PointsToPixels returns the pixels for a given number of points at a DPI. 692 func PointsToPixels(dpi, points float64) (pixels float64) { 693 pixels = (points * dpi) / 72.0 694 return 695 } 696 697 // PixelsToPoints returns the points for a given number of pixels at a DPI. 698 func PixelsToPoints(dpi, pixels float64) (points float64) { 699 points = (pixels * 72.0) / dpi 700 return 701 } 702 703 // Style is a simple style set. 704 type Style struct { 705 Hidden bool 706 Padding Box 707 708 ClassName string 709 710 StrokeWidth float64 711 StrokeColor Color 712 StrokeDashArray []float64 713 714 DotColor Color 715 DotWidth float64 716 717 FillColor Color 718 719 FontSize float64 720 FontColor Color 721 Font string 722 723 TextLineSpacing int 724 TextRotationDegrees float64 //0 is unset or normal 725 } 726 727 // GetStrokeOptions returns the stroke components. 728 func (s Style) GetStrokeOptions() Style { 729 return Style{ 730 ClassName: s.ClassName, 731 StrokeDashArray: s.StrokeDashArray, 732 StrokeColor: s.StrokeColor, 733 StrokeWidth: s.StrokeWidth, 734 } 735 } 736 737 // GetFillOptions returns the fill components. 738 func (s Style) GetFillOptions() Style { 739 return Style{ 740 ClassName: s.ClassName, 741 FillColor: s.FillColor, 742 } 743 } 744 745 // GetDotOptions returns the dot components. 746 func (s Style) GetDotOptions() Style { 747 return Style{ 748 ClassName: s.ClassName, 749 StrokeDashArray: nil, 750 FillColor: s.DotColor, 751 StrokeColor: s.DotColor, 752 StrokeWidth: 1.0, 753 } 754 } 755 756 // GetFillAndStrokeOptions returns the fill and stroke components. 757 func (s Style) GetFillAndStrokeOptions() Style { 758 return Style{ 759 ClassName: s.ClassName, 760 StrokeDashArray: s.StrokeDashArray, 761 FillColor: s.FillColor, 762 StrokeColor: s.StrokeColor, 763 StrokeWidth: s.StrokeWidth, 764 } 765 } 766 767 // GetTextOptions returns just the text components of the style. 768 func (s Style) GetTextOptions() Style { 769 return Style{ 770 ClassName: s.ClassName, 771 FontColor: s.FontColor, 772 FontSize: s.FontSize, 773 Font: s.Font, 774 TextLineSpacing: s.TextLineSpacing, 775 TextRotationDegrees: s.TextRotationDegrees, 776 } 777 } 778 779 var ( 780 // ColorTransparent is a fully transparent color. 781 ColorTransparent = Color{} 782 783 // ColorWhite is white. 784 ColorWhite = Color{R: 255, G: 255, B: 255, A: 255} 785 786 // ColorBlack is black. 787 ColorBlack = Color{R: 0, G: 0, B: 0, A: 255} 788 789 // ColorRed is red. 790 ColorRed = Color{R: 255, G: 0, B: 0, A: 255} 791 792 // ColorGreen is green. 793 ColorGreen = Color{R: 0, G: 255, B: 0, A: 255} 794 795 // ColorBlue is blue. 796 ColorBlue = Color{R: 0, G: 0, B: 255, A: 255} 797 ) 798 799 func parseHex(hex string) uint8 { 800 v, _ := strconv.ParseInt(hex, 16, 16) 801 return uint8(v) 802 } 803 804 // ColorFromHex returns a color from a css hex code. 805 func ColorFromHex(hex string) Color { 806 var c Color 807 if len(hex) == 3 { 808 c.R = parseHex(string(hex[0])) * 0x11 809 c.G = parseHex(string(hex[1])) * 0x11 810 c.B = parseHex(string(hex[2])) * 0x11 811 } else { 812 c.R = parseHex(string(hex[0:2])) 813 c.G = parseHex(string(hex[2:4])) 814 c.B = parseHex(string(hex[4:6])) 815 } 816 c.A = 255 817 return c 818 } 819 820 // ColorFromAlphaMixedRGBA returns the system alpha mixed rgba values. 821 func ColorFromAlphaMixedRGBA(r, g, b, a uint32) Color { 822 fa := float64(a) / 255.0 823 var c Color 824 c.R = uint8(float64(r) / fa) 825 c.G = uint8(float64(g) / fa) 826 c.B = uint8(float64(b) / fa) 827 c.A = uint8(a | (a >> 8)) 828 return c 829 } 830 831 // ColorChannelFromFloat returns a normalized byte from a given float value. 832 func ColorChannelFromFloat(v float64) uint8 { 833 return uint8(v * 255) 834 } 835 836 // Color is our internal color type because color.Color is bullshit. 837 type Color struct { 838 R, G, B, A uint8 839 } 840 841 // RGBA returns the color as a pre-alpha mixed color set. 842 func (c Color) RGBA() (r, g, b, a uint32) { 843 fa := float64(c.A) / 255.0 844 r = uint32(float64(uint32(c.R)) * fa) 845 r |= r << 8 846 g = uint32(float64(uint32(c.G)) * fa) 847 g |= g << 8 848 b = uint32(float64(uint32(c.B)) * fa) 849 b |= b << 8 850 a = uint32(c.A) 851 a |= a << 8 852 return 853 } 854 855 // IsZero returns if the color has been set or not. 856 func (c Color) IsZero() bool { 857 return c.R == 0 && c.G == 0 && c.B == 0 && c.A == 0 858 } 859 860 // IsTransparent returns if the colors alpha channel is zero. 861 func (c Color) IsTransparent() bool { 862 return c.A == 0 863 } 864 865 // WithAlpha returns a copy of the color with a given alpha. 866 func (c Color) WithAlpha(a uint8) Color { 867 return Color{ 868 R: c.R, 869 G: c.G, 870 B: c.B, 871 A: a, 872 } 873 } 874 875 // Equals returns true if the color equals another. 876 func (c Color) Equals(other Color) bool { 877 return c.R == other.R && 878 c.G == other.G && 879 c.B == other.B && 880 c.A == other.A 881 } 882 883 // AverageWith averages two colors. 884 func (c Color) AverageWith(other Color) Color { 885 return Color{ 886 R: (c.R + other.R) >> 1, 887 G: (c.G + other.G) >> 1, 888 B: (c.B + other.B) >> 1, 889 A: c.A, 890 } 891 } 892 893 // String returns a css string representation of the color. 894 func (c Color) String() string { 895 fa := float64(c.A) / float64(255) 896 return fmt.Sprintf("rgba(%v,%v,%v,%.1f)", c.R, c.G, c.B, fa) 897 } 898 899 // Box represents the main 4 dimensions of a box. 900 type Box struct { 901 Top int 902 Left int 903 Right int 904 Bottom int 905 IsSet bool 906 }