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  }