diff --git a/app/widget_maker.go b/app/widget_maker.go index 72a52a39..fc86484f 100644 --- a/app/widget_maker.go +++ b/app/widget_maker.go @@ -41,6 +41,7 @@ import ( "github.com/wtfutil/wtf/modules/ipaddresses/ipinfo" "github.com/wtfutil/wtf/modules/jenkins" "github.com/wtfutil/wtf/modules/jira" + "github.com/wtfutil/wtf/modules/krisinformation" "github.com/wtfutil/wtf/modules/kubernetes" "github.com/wtfutil/wtf/modules/logger" "github.com/wtfutil/wtf/modules/mercurial" @@ -214,6 +215,9 @@ func MakeWidget( case "kubernetes": settings := kubernetes.NewSettingsFromYAML(moduleName, moduleConfig, config) widget = kubernetes.NewWidget(app, settings) + case "krisinformation": + settings := krisinformation.NewSettingsFromYAML(moduleName, moduleConfig, config) + widget = krisinformation.NewWidget(app, settings) case "logger": settings := logger.NewSettingsFromYAML(moduleName, moduleConfig, config) widget = logger.NewWidget(app, settings) diff --git a/modules/krisinformation/client.go b/modules/krisinformation/client.go new file mode 100644 index 00000000..2de5638b --- /dev/null +++ b/modules/krisinformation/client.go @@ -0,0 +1,193 @@ +package krisinformation + +import ( + "fmt" + "math" + "net/http" + "strconv" + "strings" + "time" + + "github.com/wtfutil/wtf/logger" + "github.com/wtfutil/wtf/utils" +) + +const ( + krisinformationAPI = "https://api.krisinformation.se/v2/feed?format=json" +) + +type Krisinformation []struct { + Identifier string `json:"Identifier"` + PushMessage string `json:"PushMessage"` + Updated time.Time `json:"Updated"` + Published time.Time `json:"Published"` + Headline string `json:"Headline"` + Preamble string `json:"Preamble"` + BodyText string `json:"BodyText"` + Area []struct { + Type string `json:"Type"` + Description string `json:"Description"` + Coordinate string `json:"Coordinate"` + GeometryInformation interface{} `json:"GeometryInformation"` + } `json:"Area"` + Web string `json:"Web"` + Language string `json:"Language"` + Event string `json:"Event"` + SenderName string `json:"SenderName"` + Push bool `json:"Push"` + BodyLinks []interface{} `json:"BodyLinks"` + SourceID int `json:"SourceID"` + IsVma bool `json:"IsVma"` + IsTestVma bool `json:"IsTestVma"` +} + +// Client holds or configuration +type Client struct { + latitude float64 + longitude float64 + radius int + county string + country bool +} + +// Item holds the interesting parts +type Item struct { + PushMessage string + HeadLine string + SenderName string + Country bool + County bool + Distance float64 + Updated time.Time +} + +//NewClient returns a new Client +func NewClient(latitude, longitude float64, radius int, county string, country bool) *Client { + return &Client{ + latitude: latitude, + longitude: longitude, + radius: radius, + county: county, + country: country, + } + +} + +// getKrisinformation - return items that match either country, county or a radius +// Priority: +// - Country +// - County +// - Region +func (c *Client) getKrisinformation() (items []Item, err error) { + resp, err := http.Get(krisinformationAPI) + if err != nil { + return nil, err + } + defer func() { _ = resp.Body.Close() }() + + var data Krisinformation + err = utils.ParseJSON(&data, resp.Body) + if err != nil { + return nil, err + } + + for i := range data { + for a := range data[i].Area { + // Country wide events + if c.country && data[i].Area[a].Type == "Country" { + item := Item{ + PushMessage: data[i].PushMessage, + HeadLine: data[i].Headline, + SenderName: data[i].SenderName, + Country: true, + Updated: data[i].Updated, + } + items = append(items, item) + continue + } + + // County specific events + if c.county != "" && data[i].Area[a].Type == "County" { + // We look for county in description + if strings.Contains( + strings.ToLower(data[i].Area[a].Description), + strings.ToLower(c.county), + ) { + item := Item{ + PushMessage: data[i].PushMessage, + HeadLine: data[i].Headline, + SenderName: data[i].SenderName, + County: true, + Updated: data[i].Updated, + } + items = append(items, item) + continue + } + } + + if c.radius != -1 { + coords := data[i].Area[a].Coordinate + if coords == "" { + continue + } + buf := strings.Split(coords, " ") + latlon := strings.Split(buf[0], ",") + kris_latitude, err := strconv.ParseFloat(latlon[0], 32) + if err != nil { + return nil, err + } + + kris_longitude, err := strconv.ParseFloat(latlon[1], 32) + if err != nil { + return nil, err + } + + distance := DistanceInMeters(kris_latitude, kris_longitude, c.latitude, c.longitude) + logger.Log(fmt.Sprintf("Distance: %f", distance/1000)) // KM + if distance < float64(c.radius) { + item := Item{ + PushMessage: data[i].PushMessage, + HeadLine: data[i].Headline, + SenderName: data[i].SenderName, + Distance: distance, + Updated: data[i].Updated, + } + items = append(items, item) + } + + } + } + } + + return items, nil +} + +// haversin(θ) function +func hsin(theta float64) float64 { + return math.Pow(math.Sin(theta/2), 2) +} + +// Distance function returns the distance (in meters) between two points of +// a given longitude and latitude relatively accurately (using a spherical +// approximation of the Earth) through the Haversin Distance Formula for +// great arc distance on a sphere with accuracy for small distances +// +// point coordinates are supplied in degrees and converted into rad. in the func +// +// http://en.wikipedia.org/wiki/Haversine_formula +func DistanceInMeters(lat1, lon1, lat2, lon2 float64) float64 { + // convert to radians + // must cast radius as float to multiply later + var la1, lo1, la2, lo2, r float64 + la1 = lat1 * math.Pi / 180 + lo1 = lon1 * math.Pi / 180 + la2 = lat2 * math.Pi / 180 + lo2 = lon2 * math.Pi / 180 + + r = 6378100 // Earth radius in METERS + + // calculate + h := hsin(la2-la1) + math.Cos(la1)*math.Cos(la2)*hsin(lo2-lo1) + + return 2 * r * math.Asin(math.Sqrt(h)) +} diff --git a/modules/krisinformation/settings.go b/modules/krisinformation/settings.go new file mode 100644 index 00000000..209d143a --- /dev/null +++ b/modules/krisinformation/settings.go @@ -0,0 +1,44 @@ +package krisinformation + +import ( + "github.com/olebedev/config" + "github.com/wtfutil/wtf/cfg" +) + +const ( + defaultFocusable = false + defaultTitle = "Krisinformation" + defaultRadius = -1 + defaultCountry = true + defaultCounty = "" + defaultMaxItems = -1 + defaultMaxAge = 720 +) + +// Settings defines the configuration properties for this module +type Settings struct { + common *cfg.Common + latitude float64 `help:"The latitude of the position from which the widget should look for messages." optional:"true"` + longitude float64 `help:"The longitude of the position from which the widget should look for messages." optional:"true"` + radius int `help:"The radius in km from your position that the widget should look for messages. need latitude/longitude setting,Default 10" optional:"true"` + county string `help:"The county from where to display messages" optional:"true"` + country bool `help:"Only display country wide messages" optional:"true"` + maxitems int `help:"Only display X number of latest messages" optional:"true"` + maxage int `help:"Only show messages younger than maxage" optional:"true"` +} + +// NewSettingsFromYAML creates a new settings instance from a YAML config block +func NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings { + settings := Settings{ + common: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig), + latitude: ymlConfig.UFloat64("latitude", -1), + longitude: ymlConfig.UFloat64("longitude", -1), + radius: ymlConfig.UInt("radius", defaultRadius), + country: ymlConfig.UBool("country", defaultCountry), + county: ymlConfig.UString("county", defaultCounty), + maxitems: ymlConfig.UInt("maxitems", defaultMaxItems), + maxage: ymlConfig.UInt("maxages", defaultMaxAge), + } + + return &settings +} diff --git a/modules/krisinformation/widget.go b/modules/krisinformation/widget.go new file mode 100644 index 00000000..fc260489 --- /dev/null +++ b/modules/krisinformation/widget.go @@ -0,0 +1,90 @@ +package krisinformation + +import ( + "fmt" + "time" + + "github.com/rivo/tview" + "github.com/wtfutil/wtf/view" +) + +// Widget is the container for your module's data +type Widget struct { + view.TextWidget + + app *tview.Application + settings *Settings + err error + client *Client +} + +// NewWidget creates and returns an instance of Widget +func NewWidget(app *tview.Application, settings *Settings) *Widget { + widget := Widget{ + TextWidget: view.NewTextWidget(app, nil, settings.common), + app: app, + settings: settings, + client: NewClient( + settings.latitude, + settings.longitude, + settings.radius, + settings.county, + settings.country), + } + + return &widget +} + +/* -------------------- Exported Functions -------------------- */ + +// Refresh updates the onscreen contents of the widget +func (widget *Widget) Refresh() { + if widget.Disabled() { + return + } + // The last call should always be to the display function + widget.display() +} + +/* -------------------- Unexported Functions -------------------- */ + +func (widget *Widget) content() (string, string, bool) { + var title = defaultTitle + if widget.CommonSettings().Title != "" { + title = widget.CommonSettings().Title + } + now := time.Now() + kriser, err := widget.client.getKrisinformation() + if err != nil { + handleError(widget, err) + } + + var str string + i := 0 + for k := range kriser { + diff := now.Sub(kriser[k].Updated) + if widget.settings.maxage != -1 { + // Skip if message is too old + if int(diff.Hours()) > widget.settings.maxage { + //logger.Log(fmt.Sprintf("Article to old: (%s) Days: %d", kriser[k].HeadLine, int(diff.Hours()))) + continue + } + } + i++ + if i > widget.settings.maxitems && widget.settings.maxitems != -1 { + break + } + str += fmt.Sprintf("- %s\n", kriser[k].HeadLine) + } + return title, str, true +} + +func (widget *Widget) display() { + widget.Redraw(func() (string, string, bool) { + return widget.content() + }) +} + +func handleError(widget *Widget, err error) { + widget.err = err +}