Alter 13 files
Add `.gitignore` Update `go.mod` Add `go.sum` Add `drawing.go` Add `JetBrainsMono-Bold.ttf` Add `JetBrainsMono-Italic.ttf` Add `JetBrainsMono-Regular.ttf` Add `weathericons-regular-webfont.ttf` Add `imagegen.go` Add `kindle-dashboard.test.png` Add `rttclient.go` Add `wxclient.go` Add `rootpasswordtool.txt`
This commit is contained in:
parent
1178c657e0
commit
2e9b8cdfb0
13 changed files with 693 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
backup
|
||||
*_test.go
|
22
go.mod
22
go.mod
|
@ -1,3 +1,25 @@
|
|||
module git.tdpain.net/codemicro/kindle-dashboard
|
||||
|
||||
go 1.21.4
|
||||
|
||||
require (
|
||||
github.com/carlmjohnson/requests v0.23.5
|
||||
github.com/disintegration/imaging v1.6.2
|
||||
github.com/tdewolff/canvas v0.0.0-20231102134958-6de43c767dbf
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/ByteArena/poly2tri-go v0.0.0-20170716161910-d102ad91854f // indirect
|
||||
github.com/benoitkugler/textlayout v0.3.0 // indirect
|
||||
github.com/benoitkugler/textprocessing v0.0.3 // indirect
|
||||
github.com/dsnet/compress v0.0.1 // indirect
|
||||
github.com/go-fonts/latin-modern v0.3.1 // indirect
|
||||
github.com/go-text/typesetting v0.0.0-20231013144250-6cc35dbfae7d // indirect
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||
github.com/tdewolff/minify/v2 v2.20.5 // indirect
|
||||
github.com/tdewolff/parse/v2 v2.7.3 // indirect
|
||||
golang.org/x/image v0.13.0 // indirect
|
||||
golang.org/x/net v0.17.0 // indirect
|
||||
golang.org/x/text v0.13.0 // indirect
|
||||
star-tex.org/x/tex v0.4.0 // indirect
|
||||
)
|
||||
|
|
69
go.sum
Normal file
69
go.sum
Normal file
|
@ -0,0 +1,69 @@
|
|||
git.sr.ht/~sbinet/gg v0.5.0 h1:6V43j30HM623V329xA9Ntq+WJrMjDxRjuAB1LFWF5m8=
|
||||
git.sr.ht/~sbinet/gg v0.5.0/go.mod h1:G2C0eRESqlKhS7ErsNey6HHrqU1PwsnCQlekFi9Q2Oo=
|
||||
github.com/ByteArena/poly2tri-go v0.0.0-20170716161910-d102ad91854f h1:l7moT9o/v/9acCWA64Yz/HDLqjcRTvc0noQACi4MsJw=
|
||||
github.com/ByteArena/poly2tri-go v0.0.0-20170716161910-d102ad91854f/go.mod h1:vIOkSdX3NDCPwgu8FIuTat2zDF0FPXXQ0RYFRy+oQic=
|
||||
github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b h1:slYM766cy2nI3BwyRiyQj/Ud48djTMtMebDqepE95rw=
|
||||
github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM=
|
||||
github.com/benoitkugler/pstokenizer v1.0.0/go.mod h1:l1G2Voirz0q/jj0TQfabNxVsa8HZXh/VMxFSRALWTiE=
|
||||
github.com/benoitkugler/textlayout v0.3.0 h1:2ehWXEkgb6RUokTjXh1LzdGwG4dRP6X3dqhYYDYhUVk=
|
||||
github.com/benoitkugler/textlayout v0.3.0/go.mod h1:o+1hFV+JSHBC9qNLIuwVoLedERU7sBPgEFcuSgfvi/w=
|
||||
github.com/benoitkugler/textlayout-testdata v0.1.1/go.mod h1:i/qZl09BbUOtd7Bu/W1CAubRwTWrEXWq6JwMkw8wYxo=
|
||||
github.com/benoitkugler/textprocessing v0.0.3 h1:Q2X+Z6vxuW5Bxn1R9RaNt0qcprBfpc2hEUDeTlz90Ng=
|
||||
github.com/benoitkugler/textprocessing v0.0.3/go.mod h1:/4bLyCf1QYywunMK3Gf89Nhb50YI/9POewqrLxWhxd4=
|
||||
github.com/campoy/embedmd v1.0.0 h1:V4kI2qTJJLf4J29RzI/MAt2c3Bl4dQSYPuflzwFH2hY=
|
||||
github.com/campoy/embedmd v1.0.0/go.mod h1:oxyr9RCiSXg0M3VJ3ks0UGfp98BpSSGr0kpiX3MzVl8=
|
||||
github.com/carlmjohnson/requests v0.23.5 h1:NPANcAofwwSuC6SIMwlgmHry2V3pLrSqRiSBKYbNHHA=
|
||||
github.com/carlmjohnson/requests v0.23.5/go.mod h1:zG9P28thdRnN61aD7iECFhH5iGGKX2jIjKQD9kqYH+o=
|
||||
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
||||
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||
github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q=
|
||||
github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo=
|
||||
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
|
||||
github.com/go-fonts/latin-modern v0.3.1 h1:/cT8A7uavYKvglYXvrdDw4oS5ZLkcOU22fa2HJ1/JVM=
|
||||
github.com/go-fonts/latin-modern v0.3.1/go.mod h1:ysEQXnuT/sCDOAONxC7ImeEDVINbltClhasMAqEtRK0=
|
||||
github.com/go-fonts/liberation v0.3.1 h1:9RPT2NhUpxQ7ukUvz3jeUckmN42T9D9TpjtQcqK/ceM=
|
||||
github.com/go-fonts/liberation v0.3.1/go.mod h1:jdJ+cqF+F4SUL2V+qxBth8fvBpBDS7yloUL5Fi8GTGY=
|
||||
github.com/go-latex/latex v0.0.0-20230307184459-12ec69307ad9 h1:NxXI5pTAtpEaU49bpLpQoDsu1zrteW/vxzTz8Cd2UAs=
|
||||
github.com/go-latex/latex v0.0.0-20230307184459-12ec69307ad9/go.mod h1:gWuR/CrFDDeVRFQwHPvsv9soJVB/iqymhuZQuJ3a9OM=
|
||||
github.com/go-pdf/fpdf v0.9.0 h1:PPvSaUuo1iMi9KkaAn90NuKi+P4gwMedWPHhj8YlJQw=
|
||||
github.com/go-pdf/fpdf v0.9.0/go.mod h1:oO8N111TkmKb9D7VvWGLvLJlaZUQVPM+6V42pp3iV4Y=
|
||||
github.com/go-text/typesetting v0.0.0-20231013144250-6cc35dbfae7d h1:HrdwTlHVMdi9nOW7ZnYiLmIT1hJHvipIwM0aX3rKn8I=
|
||||
github.com/go-text/typesetting v0.0.0-20231013144250-6cc35dbfae7d/go.mod h1:evDBbvNR/KaVFZ2ZlDSOWWXIUKq0wCOEtzLxRM8SG3k=
|
||||
github.com/go-text/typesetting-utils v0.0.0-20230616150549-2a7df14b6a22 h1:LBQTFxP2MfsyEDqSKmUBZaDuDHN1vpqDyOZjcqS7MYI=
|
||||
github.com/go-text/typesetting-utils v0.0.0-20230616150549-2a7df14b6a22/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/tdewolff/canvas v0.0.0-20231102134958-6de43c767dbf h1:SiFIPDecTcEpw8GTDi2NsX0Cedp+GizeammteylMvmE=
|
||||
github.com/tdewolff/canvas v0.0.0-20231102134958-6de43c767dbf/go.mod h1:QT5dPLAqJLD02UXi9Dd2BkCBV4i3zh+dVxITYpd7q2Y=
|
||||
github.com/tdewolff/minify/v2 v2.20.5 h1:IbJpmpAFESnuJPdsvFBJWsDcXE5qHsmaVQrRqhOI9sI=
|
||||
github.com/tdewolff/minify/v2 v2.20.5/go.mod h1:N78HtaitkDYAWXFbqhWX/LzgwylwudK0JvybGDVQ+Mw=
|
||||
github.com/tdewolff/parse/v2 v2.7.3 h1:SHj/ry85FdqniccvzJTG+Gt/mi/HNa1cJcTzYZnvc5U=
|
||||
github.com/tdewolff/parse/v2 v2.7.3/go.mod h1:9p2qMIHpjRSTr1qnFxQr+igogyTUTlwvf9awHSm84h8=
|
||||
github.com/tdewolff/test v1.0.10/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
|
||||
github.com/tdewolff/test v1.0.11-0.20231101010635-f1265d231d52 h1:gAQliwn+zJrkjAHVcBEYW/RFvd2St4yYimisvozAYlA=
|
||||
github.com/tdewolff/test v1.0.11-0.20231101010635-f1265d231d52/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
|
||||
github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.0.0-20210504121937-7319ad40d33e/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.13.0 h1:3cge/F/QTkNLauhf2QoE9zp+7sr+ZcL4HnoZmdwg9sg=
|
||||
golang.org/x/image v0.13.0/go.mod h1:6mmbMOeV28HuMTgA6OSRkdXKYw/t5W9Uwn2Yv1r3Yxk=
|
||||
golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
gonum.org/v1/plot v0.14.0 h1:+LBDVFYwFe4LHhdP8coW6296MBEY4nQ+Y4vuUpJopcE=
|
||||
gonum.org/v1/plot v0.14.0/go.mod h1:MLdR9424SJed+5VqC6MsouEpig9pZX2VZ57H9ko2bXU=
|
||||
star-tex.org/x/tex v0.4.0 h1:AXUwgpnHLCxZUWW3qrmjv6ezNhH3PjUVBuLLejz2cgU=
|
||||
star-tex.org/x/tex v0.4.0/go.mod h1:w91ycsU/DkkCr7GWr60GPWqp3gn2U+6VX71T0o8k8qE=
|
274
imagegen/drawing.go
Normal file
274
imagegen/drawing.go
Normal file
|
@ -0,0 +1,274 @@
|
|||
package imagegen
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/tdewolff/canvas"
|
||||
"time"
|
||||
)
|
||||
|
||||
//go:embed fonts/JetBrainsMono-Regular.ttf
|
||||
var srcfontJBMonoRegular []byte
|
||||
|
||||
//go:embed fonts/JetBrainsMono-Bold.ttf
|
||||
var srcfontJBMonoBold []byte
|
||||
|
||||
//go:embed fonts/JetBrainsMono-Italic.ttf
|
||||
var srcfontJBMonoItalic []byte
|
||||
|
||||
//go:embed fonts/weathericons-regular-webfont.ttf
|
||||
var srcFontWeatherIcons []byte
|
||||
|
||||
var (
|
||||
fontJetBrainsMono *canvas.FontFamily
|
||||
fontWeatherIcons *canvas.Font
|
||||
|
||||
fontMonoTitle *canvas.FontFace
|
||||
fontMonoSubtitleInverted *canvas.FontFace
|
||||
fontMonoLittleSubtitle *canvas.FontFace
|
||||
fontMonoMedText *canvas.FontFace
|
||||
fontMonoSmallText *canvas.FontFace
|
||||
fontWeatherIconsSmall *canvas.FontFace
|
||||
fontWeatherIconsLarge *canvas.FontFace
|
||||
)
|
||||
|
||||
func init() {
|
||||
fontJetBrainsMono = canvas.NewFontFamily("jetbrainsmono")
|
||||
|
||||
if err := fontJetBrainsMono.LoadFont(srcfontJBMonoRegular, 0, canvas.FontRegular); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if err := fontJetBrainsMono.LoadFont(srcfontJBMonoBold, 0, canvas.FontBold); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if err := fontJetBrainsMono.LoadFont(srcfontJBMonoItalic, 0, canvas.FontItalic); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fontMonoTitle = fontJetBrainsMono.Face(220)
|
||||
fontMonoSubtitleInverted = fontJetBrainsMono.Face(95, canvas.White, canvas.FontBold)
|
||||
fontMonoLittleSubtitle = fontJetBrainsMono.Face(80, canvas.FontBold)
|
||||
fontMonoMedText = fontJetBrainsMono.Face(150, canvas.FontRegular)
|
||||
fontMonoSmallText = fontJetBrainsMono.Face(75, canvas.FontRegular)
|
||||
|
||||
if i, err := canvas.LoadFont(srcFontWeatherIcons, 0, canvas.FontRegular); err != nil {
|
||||
panic(err)
|
||||
} else {
|
||||
fontWeatherIcons = i
|
||||
}
|
||||
|
||||
fontWeatherIconsSmall = fontWeatherIcons.Face(150, canvas.Black)
|
||||
fontWeatherIconsLarge = fontWeatherIcons.Face(300, canvas.Black)
|
||||
}
|
||||
|
||||
func drawTitle(ctx *canvas.Context, startFrom float64) float64 {
|
||||
now := time.Now().UTC()
|
||||
|
||||
var tod string
|
||||
if hr := now.Hour(); 0 <= hr && hr < 12 {
|
||||
tod = "morning"
|
||||
} else if 12 <= hr && hr < 17 {
|
||||
tod = "afternoon"
|
||||
} else if 17 <= hr && hr < 21 {
|
||||
tod = "evening"
|
||||
} else {
|
||||
tod = "night"
|
||||
}
|
||||
|
||||
titleText := "good " + tod
|
||||
|
||||
var (
|
||||
titleBounds canvas.Rect
|
||||
)
|
||||
|
||||
{
|
||||
// draw title
|
||||
tl := canvas.NewTextLine(fontMonoTitle, titleText, canvas.Left)
|
||||
titleBounds = tl.OutlineBounds()
|
||||
ctx.DrawText(padding, startFrom+titleBounds.H, tl)
|
||||
}
|
||||
|
||||
{
|
||||
// draw date
|
||||
rt := canvas.NewTextLine(fontMonoLittleSubtitle, now.Format("02 Jan"), canvas.Right)
|
||||
ctx.DrawText(imageWidth-padding, startFrom+titleBounds.H, rt)
|
||||
}
|
||||
|
||||
return startFrom + titleBounds.H
|
||||
}
|
||||
|
||||
func drawWeather(ctx *canvas.Context, startFrom float64, wxLoc string, wx []*weatherEntry) (float64, error) {
|
||||
if len(wx) < 1 {
|
||||
return 0, errors.New("at least 1 weather entry required")
|
||||
}
|
||||
|
||||
cursorX := float64(0)
|
||||
cursorY := startFrom + 15
|
||||
startFrom += 15
|
||||
|
||||
tl := canvas.NewTextLine(fontMonoSubtitleInverted, "Weather", canvas.Left)
|
||||
bounds := tl.OutlineBounds()
|
||||
|
||||
{
|
||||
cursorX += padding
|
||||
cursorY += padding
|
||||
//posX := padding
|
||||
//posY := startFrom + padding
|
||||
titlePad := float64(5)
|
||||
|
||||
ctx.SetFill(canvas.Black)
|
||||
ctx.DrawPath(cursorX, cursorY, canvas.Rectangle(bounds.W+titlePad*2, bounds.H+titlePad*2))
|
||||
|
||||
cursorX += titlePad
|
||||
cursorY += titlePad + bounds.H
|
||||
|
||||
ctx.DrawText(cursorX, cursorY, tl)
|
||||
|
||||
cursorX += bounds.W
|
||||
ctx.DrawText(cursorX, cursorY, canvas.NewTextLine(fontMonoLittleSubtitle, fmt.Sprintf(" in %s at %02d00z", wxLoc, wx[0].Time), canvas.Left))
|
||||
}
|
||||
|
||||
cursorY += bounds.H + 5
|
||||
cursorX = padding
|
||||
titleBaseline := startFrom + (bounds.H * 2) + 5
|
||||
|
||||
//ctx.SetFill(canvas.Red)
|
||||
//ctx.DrawPath(1, titleBaseline, canvas.Rectangle(900, 1))
|
||||
|
||||
currTempStr := fmt.Sprintf("%02d", wx[0].Temperature)
|
||||
currWindSpeedStr := fmt.Sprintf("w%dmph", wx[0].WindSpeed)
|
||||
currPrecipProbStr := fmt.Sprintf("r%d%%", wx[0].PrecipitationChance)
|
||||
|
||||
tl = canvas.NewTextLine(fontWeatherIconsLarge, weatherIcons[wx[0].Type], canvas.Left)
|
||||
bounds = tl.OutlineBounds()
|
||||
|
||||
wxIconsBaseline := titleBaseline + fontWeatherIconsLarge.Size
|
||||
|
||||
ctx.DrawText(padding*2, wxIconsBaseline, tl)
|
||||
|
||||
minitextblockxpos := (padding * 2) + 15 + bounds.W
|
||||
|
||||
rt := canvas.NewRichText(fontMonoMedText)
|
||||
rt.WriteString(currTempStr)
|
||||
rt.SetFace(fontWeatherIconsSmall)
|
||||
rt.WriteString("\uf03c")
|
||||
rtt := rt.ToText(fontMonoMedText.TextWidth(currTempStr)+fontWeatherIconsSmall.TextWidth("\uf03c"), fontMonoMedText.LineHeight()+10, canvas.Left, canvas.Left, 0.0, 0.0)
|
||||
ctx.DrawText(minitextblockxpos, titleBaseline, rtt)
|
||||
|
||||
tl = canvas.NewTextLine(fontMonoSmallText, currWindSpeedStr, canvas.Left)
|
||||
ctx.DrawText(minitextblockxpos, titleBaseline+90, tl)
|
||||
|
||||
tl = canvas.NewTextLine(fontMonoSmallText, currPrecipProbStr, canvas.Left)
|
||||
ctx.DrawText(minitextblockxpos, titleBaseline+120, tl)
|
||||
|
||||
// -----
|
||||
|
||||
minitextblockxpos += rtt.Width + 15
|
||||
dividerLineHeight := wxIconsBaseline - titleBaseline
|
||||
|
||||
for _, weather := range wx[1:] {
|
||||
ctx.SetFill(canvas.Black)
|
||||
ctx.DrawPath(minitextblockxpos, titleBaseline+20, canvas.Rectangle(3, dividerLineHeight))
|
||||
|
||||
minitextblockxpos += 15
|
||||
|
||||
wtl := canvas.NewTextLine(fontWeatherIconsSmall, weatherIcons[weather.Type], canvas.Left)
|
||||
bounds := wtl.OutlineBounds()
|
||||
|
||||
tl = canvas.NewTextLine(fontMonoSmallText, fmt.Sprintf("%02dz %02dc", weather.Time, weather.Temperature), canvas.Left)
|
||||
nbounds := tl.OutlineBounds()
|
||||
ctx.DrawText(minitextblockxpos, titleBaseline+bounds.H+nbounds.H+50, tl)
|
||||
|
||||
ctx.DrawText(minitextblockxpos+((8+nbounds.W-bounds.W)/2), titleBaseline+bounds.H+30, wtl)
|
||||
|
||||
minitextblockxpos += 15 + nbounds.W
|
||||
}
|
||||
|
||||
return titleBaseline + dividerLineHeight, nil
|
||||
}
|
||||
|
||||
// https://erikflowers.github.io/weather-icons/
|
||||
var weatherIcons = map[int]string{
|
||||
0: "\uf02e", // clear night
|
||||
1: "\uf00d", // sunny day
|
||||
2: "\uf086", // partly cloudy (night)
|
||||
3: "\uf002", // partly cloudy (day)
|
||||
// 4 not used
|
||||
5: "\uf014", // mist
|
||||
6: "\uf014", // fog
|
||||
7: "\uf013", // cloudy
|
||||
8: "\uf041", // overcast
|
||||
9: "\uf029", // light rain shower (night)
|
||||
10: "\uf009", // light rain shower (day)
|
||||
11: "\uf01c", // drizzle
|
||||
12: "\uf01a", // light rain
|
||||
13: "\uf008", // Heavy rain shower (night)
|
||||
14: "\uf028", // Heavy rain shower (day)
|
||||
15: "\uf019", // Heavy rain
|
||||
16: "\uf0b4", // Sleet shower (night)
|
||||
17: "\uf0b2", // Sleet shower (day)
|
||||
18: "\uf0b5", // Sleet
|
||||
19: "\uf024", // Hail shower (night)
|
||||
20: "\uf004", // Hail shower (day)
|
||||
21: "\uf015", // Hail
|
||||
22: "\uf067", // Light snow shower (night)
|
||||
23: "\uf065", // Light snow shower (day)
|
||||
24: "\uf064", // Light snow
|
||||
25: "\uf076", // Heavy snow shower (night)
|
||||
26: "\uf076", // Heavy snow shower (day)
|
||||
27: "\uf076", // Heavy snow
|
||||
28: "\uf02d", // Thunder shower (night)
|
||||
29: "\uf010", // Thunder shower (day)
|
||||
30: "\uf01e", // Thunder
|
||||
}
|
||||
|
||||
func drawTrains(ctx *canvas.Context, startFrom float64, services []string) (float64, error) {
|
||||
cursorX := float64(0)
|
||||
cursorY := startFrom + 15
|
||||
startFrom += 15
|
||||
|
||||
tl := canvas.NewTextLine(fontMonoSubtitleInverted, "Trains", canvas.Left)
|
||||
bounds := tl.OutlineBounds()
|
||||
|
||||
{
|
||||
cursorX += padding
|
||||
cursorY += padding
|
||||
//posX := padding
|
||||
//posY := startFrom + padding
|
||||
titlePad := float64(5)
|
||||
|
||||
ctx.SetFill(canvas.Black)
|
||||
ctx.DrawPath(cursorX, cursorY, canvas.Rectangle(bounds.W+titlePad*2, bounds.H+titlePad*2))
|
||||
|
||||
cursorX += titlePad
|
||||
cursorY += titlePad + bounds.H
|
||||
|
||||
ctx.DrawText(cursorX, cursorY, tl)
|
||||
|
||||
cursorX += bounds.W
|
||||
ctx.DrawText(cursorX, cursorY, canvas.NewTextLine(fontMonoLittleSubtitle, " at SLY", canvas.Left))
|
||||
}
|
||||
|
||||
cursorY += bounds.H + 10
|
||||
cursorX = padding
|
||||
titleBaseline := startFrom + (bounds.H * 2) + 5
|
||||
|
||||
if len(services) == 0 {
|
||||
services = []string{"*** No services in the next 30 mins ***"}
|
||||
}
|
||||
|
||||
var lh float64
|
||||
for _, str := range services {
|
||||
tl := canvas.NewTextLine(fontMonoSmallText, str, canvas.Left)
|
||||
if lh == 0 {
|
||||
lh = tl.OutlineBounds().H
|
||||
}
|
||||
ctx.DrawText(cursorX, cursorY, tl)
|
||||
cursorY += lh + 5
|
||||
}
|
||||
|
||||
return titleBaseline, nil
|
||||
}
|
BIN
imagegen/fonts/JetBrainsMono-Bold.ttf
Normal file
BIN
imagegen/fonts/JetBrainsMono-Bold.ttf
Normal file
Binary file not shown.
BIN
imagegen/fonts/JetBrainsMono-Italic.ttf
Normal file
BIN
imagegen/fonts/JetBrainsMono-Italic.ttf
Normal file
Binary file not shown.
BIN
imagegen/fonts/JetBrainsMono-Regular.ttf
Normal file
BIN
imagegen/fonts/JetBrainsMono-Regular.ttf
Normal file
Binary file not shown.
BIN
imagegen/fonts/weathericons-regular-webfont.ttf
Normal file
BIN
imagegen/fonts/weathericons-regular-webfont.ttf
Normal file
Binary file not shown.
78
imagegen/imagegen.go
Normal file
78
imagegen/imagegen.go
Normal file
|
@ -0,0 +1,78 @@
|
|||
package imagegen
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/disintegration/imaging"
|
||||
"github.com/tdewolff/canvas"
|
||||
"github.com/tdewolff/canvas/renderers/rasterizer"
|
||||
"image"
|
||||
"image/png"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
WxLocation string
|
||||
MetOfficeDatapointAPIKey string
|
||||
TrainsLocation string
|
||||
RTTUsername string
|
||||
RTTPassword string
|
||||
}
|
||||
|
||||
const (
|
||||
padding float64 = 20
|
||||
|
||||
imageWidth float64 = 800
|
||||
imageHeight float64 = 600
|
||||
)
|
||||
|
||||
func Generate(conf *Config) ([]byte, error) {
|
||||
// TODO: hardcoded secrets ick
|
||||
locStr, wx, err := getWeatherInLocation(conf.MetOfficeDatapointAPIKey, conf.WxLocation)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
trains, err := getNextTrains(conf.RTTUsername, conf.RTTPassword, conf.TrainsLocation)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fmt.Println(strings.Join(trains, "\n"))
|
||||
|
||||
for _, w := range wx {
|
||||
fmt.Printf("%#v\n", w)
|
||||
}
|
||||
|
||||
c := canvas.New(imageWidth, imageHeight)
|
||||
ctx := canvas.NewContext(c)
|
||||
ctx.SetCoordSystem(canvas.CartesianIV)
|
||||
|
||||
ctx.SetFill(canvas.White)
|
||||
ctx.DrawPath(0, 0, canvas.Rectangle(imageWidth, imageHeight))
|
||||
|
||||
bottom := drawTitle(ctx, padding)
|
||||
bottom, err = drawWeather(ctx, bottom, locStr, wx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bottom, err = drawTrains(ctx, bottom, trains)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//bottom += padding
|
||||
//
|
||||
//ctx.SetFill(canvas.Red)
|
||||
//ctx.DrawPath(1, bottom, canvas.Rectangle(900, 3))
|
||||
|
||||
rasterizer.New(imageWidth, imageHeight, canvas.DPMM(1), canvas.DefaultColorSpace)
|
||||
var img image.Image
|
||||
img = rasterizer.Draw(c, canvas.DPMM(1), canvas.DefaultColorSpace)
|
||||
img = imaging.Rotate270(img)
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
if err := png.Encode(buf, img); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
BIN
imagegen/kindle-dashboard.test.png
Normal file
BIN
imagegen/kindle-dashboard.test.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 72 KiB |
110
imagegen/rttclient.go
Normal file
110
imagegen/rttclient.go
Normal file
|
@ -0,0 +1,110 @@
|
|||
package imagegen
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/carlmjohnson/requests"
|
||||
"time"
|
||||
)
|
||||
|
||||
type rawTrainsResp struct {
|
||||
Location struct {
|
||||
Name string `json:"name"`
|
||||
Crs string `json:"crs"`
|
||||
Tiploc string `json:"tiploc"`
|
||||
Country string `json:"country"`
|
||||
System string `json:"system"`
|
||||
} `json:"location"`
|
||||
Filter interface{} `json:"filter"`
|
||||
Services []struct {
|
||||
LocationDetail struct {
|
||||
RealtimeActivated bool `json:"realtimeActivated"`
|
||||
Tiploc string `json:"tiploc"`
|
||||
Crs string `json:"crs"`
|
||||
Description string `json:"description"`
|
||||
GbttBookedArrival string `json:"gbttBookedArrival"`
|
||||
GbttBookedDeparture string `json:"gbttBookedDeparture"`
|
||||
Origin []struct {
|
||||
Tiploc string `json:"tiploc"`
|
||||
Description string `json:"description"`
|
||||
WorkingTime string `json:"workingTime"`
|
||||
PublicTime string `json:"publicTime"`
|
||||
} `json:"origin"`
|
||||
Destination []struct {
|
||||
Tiploc string `json:"tiploc"`
|
||||
Description string `json:"description"`
|
||||
WorkingTime string `json:"workingTime"`
|
||||
PublicTime string `json:"publicTime"`
|
||||
} `json:"destination"`
|
||||
IsCall bool `json:"isCall"`
|
||||
IsPublicCall bool `json:"isPublicCall"`
|
||||
RealtimeArrival string `json:"realtimeArrival"`
|
||||
RealtimeArrivalActual bool `json:"realtimeArrivalActual"`
|
||||
RealtimeDeparture string `json:"realtimeDeparture"`
|
||||
RealtimeDepartureActual bool `json:"realtimeDepartureActual"`
|
||||
DisplayAs string `json:"displayAs"`
|
||||
} `json:"locationDetail"`
|
||||
ServiceUid string `json:"serviceUid"`
|
||||
RunDate string `json:"runDate"`
|
||||
TrainIdentity string `json:"trainIdentity"`
|
||||
RunningIdentity string `json:"runningIdentity"`
|
||||
AtocCode string `json:"atocCode"`
|
||||
AtocName string `json:"atocName"`
|
||||
ServiceType string `json:"serviceType"`
|
||||
IsPassenger bool `json:"isPassenger"`
|
||||
} `json:"services"`
|
||||
}
|
||||
|
||||
func getNextTrains(username, password, location string) ([]string, error) {
|
||||
resp := new(rawTrainsResp)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*15)
|
||||
err := requests.
|
||||
URL("https://api.rtt.io").
|
||||
Pathf("/api/v1/json/search/%s", location).
|
||||
BasicAuth(username, password).
|
||||
ToJSON(&resp).
|
||||
Fetch(ctx)
|
||||
cancel()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get trains: %w", err)
|
||||
}
|
||||
|
||||
var res []string
|
||||
for _, service := range resp.Services {
|
||||
depTime, err := time.ParseInLocation("1504 2006-01-02", service.LocationDetail.GbttBookedDeparture+" "+service.RunDate, time.UTC)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse trains: %w", err)
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
if depTime.Sub(now) < time.Minute*60 {
|
||||
str := fmt.Sprintf("%s % -25s", service.LocationDetail.GbttBookedDeparture, service.LocationDetail.Destination[0].Description)
|
||||
var time string
|
||||
if service.LocationDetail.DisplayAs == "CANCELLED_CALL" {
|
||||
time = "Cancelled"
|
||||
} else if service.LocationDetail.RealtimeDeparture != service.LocationDetail.GbttBookedDeparture {
|
||||
time = "Expt " + service.LocationDetail.RealtimeDeparture
|
||||
} else if service.LocationDetail.RealtimeArrivalActual {
|
||||
time = "Arrived"
|
||||
} else {
|
||||
time = "On time"
|
||||
}
|
||||
|
||||
str += " " + time
|
||||
res = append(res, str)
|
||||
}
|
||||
}
|
||||
|
||||
for i, str := range res {
|
||||
suffix := "th"
|
||||
if i+1 == 1 {
|
||||
suffix = "st"
|
||||
} else if i+1 == 2 {
|
||||
suffix = "nd"
|
||||
} else if i+1 == 3 {
|
||||
suffix = "rd"
|
||||
}
|
||||
res[i] = fmt.Sprintf("%d%s %s", i+1, suffix, str)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
137
imagegen/wxclient.go
Normal file
137
imagegen/wxclient.go
Normal file
|
@ -0,0 +1,137 @@
|
|||
package imagegen
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/carlmjohnson/requests"
|
||||
"strconv"
|
||||
"time"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
type rawWxResp struct {
|
||||
SiteRep struct {
|
||||
DV struct {
|
||||
DataDate time.Time `json:"dataDate"`
|
||||
Type string `json:"type"`
|
||||
Location struct {
|
||||
ID string `json:"i"`
|
||||
Lat string `json:"lat"`
|
||||
Lon string `json:"lon"`
|
||||
Name string `json:"name"`
|
||||
Country string `json:"country"`
|
||||
Continent string `json:"continent"`
|
||||
Elevation string `json:"elevation"`
|
||||
Period []struct {
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value"`
|
||||
Rep []struct {
|
||||
WindDirection string `json:"D"`
|
||||
FeelsLikeTemperature string `json:"F"`
|
||||
WindGust string `json:"G"`
|
||||
RelativeHumidityPercentage string `json:"H"`
|
||||
PrecipitationProbability string `json:"Pp"`
|
||||
WindSpeed string `json:"S"`
|
||||
Temperature string `json:"T"`
|
||||
Visibility string `json:"V"`
|
||||
WeatherType string `json:"W"`
|
||||
MaxUVIndex string `json:"U"`
|
||||
MinutesPastMidnight string `json:"$"`
|
||||
} `json:"Rep"`
|
||||
} `json:"Period"`
|
||||
} `json:"Location"`
|
||||
} `json:"DV"`
|
||||
} `json:"SiteRep"`
|
||||
}
|
||||
|
||||
type weatherEntry struct {
|
||||
Time int
|
||||
Type int
|
||||
Temperature int
|
||||
PrecipitationChance int
|
||||
WindSpeed int
|
||||
}
|
||||
|
||||
func getWeatherInLocation(apiKey string, location string) (string, []*weatherEntry, error) {
|
||||
resp := new(rawWxResp)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*15)
|
||||
err := requests.
|
||||
URL("http://datapoint.metoffice.gov.uk").
|
||||
Pathf("/public/data/val/wxfcs/all/json/%s", location).
|
||||
Params(map[string][]string{
|
||||
"key": {apiKey},
|
||||
"res": {"3hourly"},
|
||||
}).
|
||||
ToJSON(&resp).
|
||||
Fetch(ctx)
|
||||
cancel()
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("get weather: %w", err)
|
||||
}
|
||||
|
||||
// get 4
|
||||
|
||||
var res []*weatherEntry
|
||||
|
||||
dayLoop:
|
||||
for _, dayWx := range resp.SiteRep.DV.Location.Period {
|
||||
day, err := time.Parse("2006-01-02Z", dayWx.Value)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("parse date: %w", err)
|
||||
}
|
||||
|
||||
for _, wx := range dayWx.Rep {
|
||||
if len(res) >= 4 {
|
||||
break dayLoop
|
||||
}
|
||||
|
||||
minsPastMidnight, err := strconv.Atoi(wx.MinutesPastMidnight)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("parse weather: %w", err)
|
||||
}
|
||||
thisTime := day.Add(time.Minute * time.Duration(minsPastMidnight))
|
||||
if resp.SiteRep.DV.DataDate.Equal(thisTime) || resp.SiteRep.DV.DataDate.Before(thisTime.Add(time.Hour*3)) {
|
||||
// if this is applicable
|
||||
wxE := new(weatherEntry)
|
||||
wxE.Time = minsPastMidnight / 60
|
||||
wxE.Type, err = strconv.Atoi(wx.WeatherType)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("parse weather: %w", err)
|
||||
}
|
||||
{
|
||||
temp, err := strconv.ParseFloat(wx.Temperature, 32)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("parse weather: %w", err)
|
||||
}
|
||||
wxE.Temperature = int(temp)
|
||||
}
|
||||
{
|
||||
precProb, err := strconv.ParseFloat(wx.PrecipitationProbability, 32)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("parse weather: %w", err)
|
||||
}
|
||||
wxE.PrecipitationChance = int(precProb)
|
||||
}
|
||||
{
|
||||
windSpeed, err := strconv.ParseFloat(wx.WindSpeed, 32)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("parse weather: %w", err)
|
||||
}
|
||||
wxE.WindSpeed = int(windSpeed)
|
||||
}
|
||||
res = append(res, wxE)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loc := []rune(resp.SiteRep.DV.Location.Name)
|
||||
for i, x := range loc {
|
||||
if i == 0 || loc[i-1] == ' ' {
|
||||
loc[i] = unicode.ToUpper(x)
|
||||
} else {
|
||||
loc[i] = unicode.ToLower(x)
|
||||
}
|
||||
}
|
||||
|
||||
return string(loc), res, nil
|
||||
}
|
1
rootpasswordtool.txt
Normal file
1
rootpasswordtool.txt
Normal file
|
@ -0,0 +1 @@
|
|||
https://www.sven.de/kindle/#
|
Reference in a new issue