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
|
module git.tdpain.net/codemicro/kindle-dashboard
|
||||||
|
|
||||||
go 1.21.4
|
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