Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
548 lines (505 sloc) 14.5 KB
// twh: time, weather, headlines
package main
import (
"encoding/json"
"flag"
"fmt"
"image"
_ "image/jpeg"
"io"
"math"
"math/rand"
"net/http"
"os"
"os/signal"
"strings"
"time"
"github.com/ajstarks/openvg"
"github.com/disintegration/gift"
)
// Forecast is weather information from forecast.io
type Forecast struct {
Lat float64 `json:"latitude"`
Long float64 `json:"longitude"`
Currently current `json:"currently"`
}
type current struct {
Summary string `json:"summary"`
Icon string `json:"icon"`
PrecipProb float64 `json:"precipProbability"`
Temperature float64 `json:"temperature"`
FeelsLike float64 `json:"apparentTemperature"`
}
// NYTHeadlines is the headline info from the New York Times
type NYTHeadlines struct {
Status string `json:"status"`
Copyright string `json:"copyright"`
NumResults int `json:"num_results"`
Results []result `json:"results"`
}
type result struct {
Section string `json:"section"`
Subsection string `json:"subsection"`
Title string `json:"title"`
Abstract string `json:"abstract"`
Thumbnail string `json:"thumbnail_standard"`
}
// HNtop and HNitem are Hacker News top stories list and items
type HNTop []int
type HNitem struct {
By string `json:"by"`
Title string `json:"title"`
}
// display is the dispaly context
type display struct {
width, height openvg.VGfloat
bgcolor, textcolor string
}
// dimen define a rectangular region
type dimen struct {
x, y, width, height openvg.VGfloat
}
const (
weatherfmt = "https://api.forecast.io/forecast/%s/%s/?exclude=hourly,daily,minutely,flags"
NYTfmt = "http://api.nytimes.com/svc/news/v3/content/all/%s/.json?api-key=%s&limit=5"
HNTopURL = "https://hacker-news.firebaseio.com/v0/topstories.json"
HNItemfmt = "https://hacker-news.firebaseio.com/v0/item/%d.json"
weatherAPIkey = "-API-Key-"
NYTAPIkey = "-API-Key-"
)
var fromHTML = strings.NewReplacer(
"", "'",
"", "'",
"", "--",
"‘", "'",
"’", "'",
"“", `"`,
"”", `"`,
"‘", "'",
"’", "'",
"–", "-",
"—", "--",
"–", "-",
"—", "--",
"…", "...",
"&", "&")
// show the current time, weather and headlines
func main() {
var (
section = flag.String("h", "u.s.", "headline type (arts, health, sports, science, technology, u.s., world, hn)")
location = flag.String("loc", "40.6213,-74.4395", "lat,long for weather")
bgcolor = flag.String("bg", "slateblue", "background color")
textcolor = flag.String("tc", "white", "text color")
width = flag.Int("width", 0, "screen width")
height = flag.Int("height", 0, "screen height")
smartcolor = flag.Bool("sc", false, "smart colors")
thumb = flag.Bool("tn", false, "show thumbnails")
)
flag.Parse()
// initial display
dw, dh := openvg.Init()
if *width > 0 && *height > 0 {
dw, dh = *width, *height
}
openvg.Start(dw, dh)
canvas := display{
width: openvg.VGfloat(dw),
height: openvg.VGfloat(dh),
bgcolor: *bgcolor,
textcolor: *textcolor,
}
canvas.countdown()
openvg.End()
canvas.clock(*smartcolor)
canvas.weather(*location)
canvas.headlines(*section, *thumb)
// update on specific intervals, shutdown on interrupt
dateticker := time.NewTicker(1 * time.Minute)
weatherticker := time.NewTicker(5 * time.Minute)
headticker := time.NewTicker(10 * time.Minute)
sigint := make(chan os.Signal, 1)
signal.Notify(sigint, os.Interrupt)
for {
select {
case <-dateticker.C:
canvas.clock(*smartcolor)
case <-weatherticker.C:
canvas.weather(*location)
case <-headticker.C:
canvas.headlines(*section, *thumb)
case <-sigint:
openvg.End()
openvg.Finish()
os.Exit(0)
}
}
}
// clock displays the current time
func (d *display) clock(smartcolor bool) {
if smartcolor {
d.bgcolor = daycolor()
}
cdim := dimen{x: d.width / 2, y: d.height / 2, width: d.width / 2, height: d.height / 2}
cdim.regionFill(d.bgcolor, d.textcolor)
clocksize := d.width / 20
cs := int(clocksize)
x := d.width * 0.95
y := d.height * 0.70
now := time.Now()
openvg.TextEnd(x, y, now.Format("3:04 pm"), "sans", cs)
openvg.TextEnd(x, y+(clocksize*2), now.Format("Monday January _2"), "sans", cs/2)
openvg.End()
}
// weather retrieves data from the forecast.io API, decodes and displays it.
func (d *display) weather(latlong string) {
wdim := dimen{x: 0, y: d.height / 2, width: d.width / 2, height: d.height / 2}
r, err := netread(fmt.Sprintf(weatherfmt, weatherAPIkey, latlong))
if err != nil {
fmt.Fprintf(os.Stderr, "Weather read error: %v\n", err)
wdim.gerror(d.bgcolor, d.textcolor, "no weather")
return
}
defer r.Close()
var data Forecast
err = json.NewDecoder(r).Decode(&data)
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
wdim.gerror(d.bgcolor, d.textcolor, "no weather")
return
}
x := d.width * 0.05
y := d.height * 0.70
wsize := d.width / 20
spacing := wsize * 2.0
w1 := int(wsize)
w2 := w1 / 2
w3 := w1 / 4
c := data.Currently
temp := fmt.Sprintf("%0.f°", c.Temperature)
tw := openvg.TextWidth(temp, "sans", w1)
wdim.regionFill(d.bgcolor, d.textcolor)
openvg.Text(x, y, temp, "sans", w1)
if c.Temperature-c.FeelsLike > 1 {
openvg.Text(x, y-(spacing/3),
fmt.Sprintf("(feels like %0.f°)", c.FeelsLike), "sans", w3)
}
openvg.Text(x, y+spacing, c.Summary, "sans", w2)
if c.PrecipProb > 0 {
openvg.Text(x, y-(spacing*.6),
fmt.Sprintf("%0.f%% Chance of precipitation", c.PrecipProb*100), "sans", w3)
}
ic := dimen{
x: x + tw + d.width*0.01,
y: d.height * 0.67,
width: d.width / 10,
height: d.width / 10,
}
switch c.Icon {
case "clear-day":
ic.sun("orange")
case "clear-night":
ic.moon(d.bgcolor, d.textcolor)
case "rain":
ic.rain("skyblue")
case "snow":
ic.snow(d.textcolor)
case "wind":
ic.wind(d.bgcolor, d.textcolor)
case "fog":
ic.fog(d.textcolor)
case "cloudy":
ic.cloud(d.textcolor)
case "partly-cloudy-day":
ic.pcloud(d.textcolor)
case "partly-cloudy-night":
ic.npcloud("darkgray", d.textcolor)
}
openvg.End()
}
// headlines shows hacker news or NYT headlines
func (d *display) headlines(headlinetype string, thumb bool) {
if headlinetype == "hn" {
d.hackernews(5)
} else {
d.nytheadlines(headlinetype, thumb)
}
}
// hackernews shows the top n articles from Hackernews
func (d *display) hackernews(n int) {
hdim := dimen{x: 0, y: 0, width: d.width, height: d.height / 2}
r, err := netread(HNTopURL)
if err != nil {
fmt.Fprintf(os.Stderr, "headline read error: %v\n", err)
hdim.gerror(d.bgcolor, d.textcolor, "no headlines")
return
}
var hnid HNTop
err = json.NewDecoder(r).Decode(&hnid)
if err != nil {
fmt.Fprintf(os.Stderr, "decode: %v\n", err)
hdim.gerror(d.bgcolor, d.textcolor, "no headlines")
r.Close()
return
}
r.Close()
var item HNitem
x := d.width / 2
y := d.height * 0.10
headsize := d.width / 80
spacing := headsize * 2.0
hdim.regionFill(d.bgcolor, d.textcolor)
for i := n - 1; i >= 0; i-- {
hnr, err := netread(fmt.Sprintf(HNItemfmt, hnid[i]))
if err != nil {
fmt.Fprintf(os.Stderr, "%v: getting id %d\n", err, hnid[i])
hnr.Close()
continue
}
err = json.NewDecoder(hnr).Decode(&item)
if err != nil {
fmt.Fprintf(os.Stderr, "%v: decoding id %d\n", err, hnid[i])
hnr.Close()
continue
}
openvg.TextMid(x, y, item.Title, "serif", int(headsize))
y += spacing
hnr.Close()
}
openvg.Image(d.width*0.05, 15, 32, 32, "hn.png")
openvg.End()
}
// nytheadlines retrieves data from the New York Times API, decodes and displays it.
func (d *display) nytheadlines(section string, thumb bool) {
hdim := dimen{x: 0, y: 0, width: d.width, height: d.height / 2}
r, err := netread(fmt.Sprintf(NYTfmt, section, NYTAPIkey))
if err != nil {
fmt.Fprintf(os.Stderr, "headline read error: %v\n", err)
hdim.gerror(d.bgcolor, d.textcolor, "no headlines")
return
}
defer r.Close()
var data NYTHeadlines
err = json.NewDecoder(r).Decode(&data)
if err != nil {
fmt.Fprintf(os.Stderr, "decode: %v\n", err)
hdim.gerror(d.bgcolor, d.textcolor, "no headlines")
return
}
x := d.width * 0.10
y := d.height * 0.10
thumbsize := int(d.height * 0.05)
hdim.regionFill(d.bgcolor, d.textcolor)
headsize := d.width / 80
spacing := openvg.VGfloat(thumbsize) + 3
if !thumb {
x -= 100
}
for i := len(data.Results) - 1; i >= 0; i-- {
openvg.Text(x, y, fromHTML.Replace(data.Results[i].Title), "serif", int(headsize))
if len(data.Results[i].Thumbnail) > 0 && thumb {
img, imerr := netimage(data.Results[i].Thumbnail)
if imerr != nil {
continue
}
g := gift.New()
g.Add(gift.Resize(0, thumbsize, gift.LanczosResampling))
gift.Resize(thumbsize, thumbsize, gift.BoxResampling)
resized := image.NewRGBA(g.Bounds(img.Bounds()))
g.Draw(resized, img)
openvg.Img(x-100, y-(spacing*0.25), resized)
}
y = y + spacing
}
openvg.Image(d.width*0.05, 15, 30, 30, "poweredby_nytimes_30a.png")
openvg.End()
}
// netimage reads an image from a URL
func netimage(url string) (image.Image, error) {
r, err := netread(url)
defer r.Close()
if err != nil {
return nil, fmt.Errorf("Unable to read the image from %s", url)
}
im, _, err := image.Decode(r)
return im, err
}
// netread derefernces a URL, returning the Reader, with an error
func netread(url string) (io.ReadCloser, error) {
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Get(url)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unable to get network data for %s (%s)", url, resp.Status)
}
return resp.Body, nil
}
// countdown shows a countdown display to the top of minute
func (d *display) countdown() {
tick := time.NewTicker(1 * time.Second)
ty := d.height / 2
th := d.height / 20
size := d.width / 70
for delay := 60 - time.Now().Second(); delay > 0; delay-- {
select {
case <-tick.C:
tx := d.width * (openvg.VGfloat(60-delay) / 60)
openvg.BackgroundColor(d.bgcolor)
openvg.FillColor("black")
openvg.Rect(0, ty, d.width, th)
openvg.FillColor("white")
openvg.TextEnd(tx, ty+(th/4), fmt.Sprintf("start in %d ", delay), "sans", int(size))
openvg.Rect(tx, ty, d.width-tx, th)
openvg.End()
}
}
openvg.BackgroundColor(d.bgcolor)
}
// regionFill colors a rectangular region, and sets the fill color for subsequent text
func (d *dimen) regionFill(bgcolor, textcolor string) {
openvg.FillColor(bgcolor)
openvg.Rect(d.x, d.y, d.width, d.height)
openvg.FillColor(textcolor)
}
// gerror makes a graphical error message
func (d *dimen) gerror(bgcolor, textcolor, message string) {
d.regionFill(bgcolor, textcolor)
openvg.TextMid(d.x+d.width/2, d.y+d.height/2, message, "sans", int(d.width/20))
openvg.End()
}
// fog shows the fog icon
func (d *dimen) fog(color string) {
x, y, w, h := d.x, d.y, d.width, d.height
radius := d.width / 3
r2 := radius * 1.8
openvg.FillColor(color, 0.5)
openvg.Circle(x+w*0.25, y+h*0.25, radius)
openvg.Circle(x+w*0.30, y+h*0.45, radius)
openvg.Circle(x+w*0.60, y+h*0.40, r2)
}
// cloud shows the cloudy icon
func (d *dimen) cloud(color string) {
x, y, w, h := d.x, d.y, d.width, d.height
radius := d.width / 3
r2 := radius * 1.8
openvg.FillColor(color)
openvg.Circle(x+w*0.25, y+h*0.25, radius)
openvg.Circle(x+w*0.30, y+h*0.45, radius)
openvg.Circle(x+w*0.60, y+h*0.40, r2)
}
// drop shows the raindrop icon
func (d *dimen) drop(color string) {
x, y, w, h := d.x, d.y, d.width, d.height
openvg.FillColor(color)
openvg.Ellipse(x+(w/2), y+(h*0.40), w*0.52, h*0.65)
xp := []openvg.VGfloat{x + (w / 2), x + (w * 0.25), x + (w * 0.75)}
yp := []openvg.VGfloat{y + h, y + (h / 2), y + (h / 2)}
openvg.Polygon(xp, yp)
}
// rain shows raindrops
func (d *dimen) rain(color string) {
dd := dimen{x: 0, y: 0, width: d.width / 6, height: d.height / 6}
for i := 0; i < 20; i++ {
dd.x = d.x + d.width*openvg.VGfloat(rand.Float64())
dd.y = d.y + d.height*openvg.VGfloat(rand.Float64())
dd.drop(color)
}
}
// flake shows the snowflake icon
func (d *dimen) flake(color string) {
x, y, w, h := d.x, d.y, d.width, d.height
cx := x + (w / 2)
cy := y + (h / 2)
r := w * 0.30
openvg.StrokeColor(color)
openvg.StrokeWidth(w / 20)
for t := 0.0; t < 2*math.Pi; t += math.Pi / 4 {
c := openvg.VGfloat(math.Cos(t))
s := openvg.VGfloat(math.Sin(t))
x1 := (r * c) + cx
y1 := (r * s) + cy
openvg.Line(cx, cy, x1, y1)
}
openvg.StrokeWidth(0)
}
// snow shows the snow icon
func (d *dimen) snow(color string) {
df := dimen{x: 0, y: 0, width: d.width / 6, height: d.height / 6}
for i := 0; i < 20; i++ {
df.x = d.x + d.width*openvg.VGfloat(rand.Float64())
df.y = d.y + d.height*openvg.VGfloat(rand.Float64())
df.flake(color)
}
}
// sun shows the icon for clear weather
func (d *dimen) sun(color string) {
x, y, w, h := d.x, d.y, d.width, d.height
cx := x + (w / 2)
cy := y + (h / 2)
r0 := w * 0.50
r1 := w * 0.45
r2 := w * 0.30
openvg.FillColor(color)
openvg.Circle(cx, cy, r0)
openvg.StrokeColor(color)
openvg.StrokeWidth(w / 30)
for t := 0.0; t < 2*math.Pi; t += math.Pi / 6 {
c := openvg.VGfloat(math.Cos(t))
s := openvg.VGfloat(math.Sin(t))
x1 := (r1 * c) + cx
y1 := (r1 * s) + cy
x2 := (r2 * c) + cx
y2 := (r2 * s) + cy
openvg.Line(x1, y1, x2, y2)
}
openvg.StrokeWidth(0)
}
// moon shows the icon for clear weather at night
func (d *dimen) moon(bg, fg string) {
x, y, w, h := d.x, d.y, d.width, d.height
cx := x + w/2
cy := y + h/2
w2 := w / 2
openvg.FillColor(fg)
openvg.Circle(cx, cy, w2)
openvg.FillColor(bg)
openvg.Circle(x+w*0.65, cy, w2)
}
// pcloud shows the icon for partly cloudy
func (d *dimen) pcloud(color string) {
sd := dimen{x: d.x + d.width*.2, y: d.y + d.height*.33, width: d.width * .7, height: d.height * .7}
sd.sun("orange")
d.cloud(color)
}
// npcloud shows the partly cloudy icon at night
func (d *dimen) npcloud(ccolor, mcolor string) {
d.cloud(ccolor)
md := dimen{x: d.x + d.width*.2, y: d.y + d.height*0.05, width: d.width * .7, height: d.height * .7}
md.moon(ccolor, mcolor)
}
// wind shows the windy icon
func (d *dimen) wind(bg, color string) {
x, y, w, h := d.x, d.y, d.width, d.height
openvg.FillColor(bg, 0)
openvg.StrokeWidth(w / 25)
openvg.StrokeColor(color)
openvg.Qbezier(x+w*0.10, y+h*0.8, x+w*0.50, y+h*0.60, x+w*0.9, y+h*0.85)
openvg.Qbezier(x+w*0.10, y+h*0.5, x+w*0.55, y+h*0.30, x+w*0.9, y+h*0.55)
openvg.Qbezier(x+w*0.10, y+h*0.2, x+w*0.60, y+h*0.10, x+w*0.9, y+h*0.35)
openvg.StrokeWidth(0)
}
// daycolor returns a color appropriate for the hour of the day
func daycolor() string {
hour := time.Now().Hour()
switch {
case hour <= 11 && hour >= 6:
return "steelblue"
case hour <= 17 && hour >= 12:
return "blue"
case hour <= 19 && hour >= 18:
return "slategray"
default:
return "midnightblue"
}
}
You can’t perform that action at this time.