2025-02-18 14:21:51 +01:00
package render
import (
"fmt"
"hayai/config"
"hayai/jmaeew"
"hayai/utils"
"hayai/waves"
"image/color"
"log"
"math"
2025-02-18 14:56:16 +01:00
"os"
2025-02-18 14:21:51 +01:00
"time"
"github.com/disintegration/imaging"
"github.com/fogleman/gg"
"github.com/golang/freetype/truetype"
"github.com/mmcloughlin/globe"
"github.com/pymaxion/geographiclib-go/geodesic"
"golang.org/x/image/font"
"github.com/hajimehoshi/ebiten/v2"
_ "embed"
)
var gameData * Game
type Game struct {
2025-02-18 14:45:03 +01:00
Event jmaeew . JMAEEW
ImpactTime float64
2025-02-18 14:21:51 +01:00
}
var Displaying bool
func ( g * Game ) Update ( ) error {
if ebiten . IsWindowBeingClosed ( ) {
2025-02-18 14:56:16 +01:00
os . Exit ( 0 )
2025-02-18 14:21:51 +01:00
}
return nil // Add kill after timer TODO
}
2025-02-18 14:45:03 +01:00
func ComputeLoveImpactTime ( event jmaeew . JMAEEW ) float64 {
wgs84 := geodesic . WGS84
line := wgs84 . InverseLine ( event . Latitude , event . Longitude , config . Config . Latitude , config . Config . Longitude )
return waves . PredictLoveWaveImpactTime ( line . Arc ( ) )
}
2025-02-18 14:21:51 +01:00
func ( g * Game ) Draw ( screen * ebiten . Image ) {
jst := time . FixedZone ( "JST" , 9 * 3600 )
// fake time here for testing
2025-02-18 14:45:03 +01:00
// g.Event.OriginTime = "2025/02/18 22:45:30"
2025-02-18 14:21:51 +01:00
originalTimeParsed , _ := time . ParseInLocation ( "2006/01/02 15:04:05" , g . Event . OriginTime , jst )
2025-02-18 14:45:03 +01:00
currentTime := float64 ( time . Now ( ) . Sub ( originalTimeParsed ) . Nanoseconds ( ) ) / 1e9
2025-02-18 14:21:51 +01:00
gl := globe . New ( )
gl . DrawGraticule ( 10.0 )
gl . DrawLandBoundaries ( )
green := color . NRGBA { 0x00 , 0x64 , 0x3c , 192 }
red := color . NRGBA { 0xff , 0x42 , 0x3c , 192 }
2025-02-18 16:41:22 +01:00
gl . DrawDot ( config . Config . Latitude , config . Config . Longitude , 0.05 , globe . Color ( green ) )
2025-02-18 14:21:51 +01:00
gl . DrawDot ( g . Event . Latitude , g . Event . Longitude , 0.05 , globe . Color ( red ) )
// Draw waves
for _ , wave := range waves . WaveList {
travel , travelA , travelB , double , exists := waves . TimeToTravel ( * wave , currentTime )
if exists {
if double {
DrawCircle ( gl , g . Event . Latitude , g . Event . Longitude , travelA , globe . Color ( wave . Color ) )
DrawCircle ( gl , g . Event . Latitude , g . Event . Longitude , travelB , globe . Color ( wave . Color ) )
} else {
DrawCircle ( gl , g . Event . Latitude , g . Event . Longitude , travel , globe . Color ( wave . Color ) )
}
}
}
gl . CenterOn ( g . Event . Latitude , g . Event . Longitude )
renderedImage := gl . Image ( config . Config . RealtimeVisRenderSize )
inverted := imaging . Invert ( renderedImage )
ctx := gg . NewContextForImage ( inverted )
ctx . SetRGBA ( 1 , 0 , 0 , 1 )
ctx . SetFontFace ( FontFace )
ctx . DrawString ( fmt . Sprintf ( "Detected a M%0.1f earthquake in %s" , g . Event . Magunitude , g . Event . Title ) , config . Config . RealtimeVisTextPadding , config . Config . RealtimeVisTextPadding + config . Config . RealtimeVisFontSize )
ctx . DrawString ( fmt . Sprintf ( "%0.4f, %0.4f @ %d km" , g . Event . Latitude , g . Event . Longitude , g . Event . Depth ) , config . Config . RealtimeVisTextPadding , config . Config . RealtimeVisTextPadding + 2 * config . Config . RealtimeVisFontSize )
2025-02-18 14:45:03 +01:00
ctx . DrawString ( fmt . Sprintf ( "Love waves impact in %0.1fs" , g . ImpactTime - currentTime ) , config . Config . RealtimeVisTextPadding , config . Config . RealtimeVisTextPadding + 3 * config . Config . RealtimeVisFontSize )
2025-02-18 14:21:51 +01:00
for i , wave := range waves . WaveList {
travel , travelA , travelB , double , exists := waves . TimeToTravel ( * wave , currentTime )
if exists {
ctx . SetRGBA ( float64 ( wave . Color . R ) / 255 , float64 ( wave . Color . G ) / 255 , float64 ( wave . Color . B ) / 255 , 1 )
if double {
angle := 2 * math . Pi * float64 ( i ) / float64 ( len ( waves . WaveList ) )
x := math . Cos ( - angle )
y := math . Sin ( - angle )
if travelA < 90 {
ctx . DrawStringAnchored ( wave . Label , float64 ( config . Config . RealtimeVisRenderSize ) / 2 + x * 0.99 * float64 ( config . Config . RealtimeVisRenderSize ) / 2 * math . Sin ( travelA / 180 * math . Pi ) , float64 ( config . Config . RealtimeVisRenderSize ) / 2 + y * 0.99 * float64 ( config . Config . RealtimeVisRenderSize ) / 2 * math . Sin ( travel / 180 * math . Pi ) , x , y )
}
if travelB < 90 {
ctx . DrawStringAnchored ( wave . Label , float64 ( config . Config . RealtimeVisRenderSize ) / 2 - x * 0.99 * float64 ( config . Config . RealtimeVisRenderSize ) / 2 * math . Sin ( travelB / 180 * math . Pi ) , float64 ( config . Config . RealtimeVisRenderSize ) / 2 - y * 0.99 * float64 ( config . Config . RealtimeVisRenderSize ) / 2 * math . Sin ( travel / 180 * math . Pi ) , - x , - y )
}
} else {
if travel < 90 {
angle := 2 * math . Pi * float64 ( i ) / float64 ( len ( waves . WaveList ) )
x := math . Cos ( - angle )
y := math . Sin ( - angle )
ctx . DrawStringAnchored ( wave . Label , float64 ( config . Config . RealtimeVisRenderSize ) / 2 + x * 0.99 * float64 ( config . Config . RealtimeVisRenderSize ) / 2 * math . Sin ( travel / 180 * math . Pi ) , float64 ( config . Config . RealtimeVisRenderSize ) / 2 + y * 0.99 * float64 ( config . Config . RealtimeVisRenderSize ) / 2 * math . Sin ( travel / 180 * math . Pi ) , x , y )
}
}
ctx . Fill ( )
}
}
screen . DrawImage ( ebiten . NewImageFromImage ( ctx . Image ( ) ) , & ebiten . DrawImageOptions { } )
}
func DrawCircle ( g * globe . Globe , lat , lon float64 , radius float64 , color globe . Option ) {
//fmt.Println(radius)
wgs84 := geodesic . WGS84
pointLats := [ ] float64 { }
pointLons := [ ] float64 { }
for i := 0 ; i < config . Config . RealtimeVisCircleResolution ; i ++ {
2025-02-18 17:35:45 +01:00
gData := wgs84 . Direct ( lat , lon , float64 ( i ) / float64 ( config . Config . RealtimeVisCircleResolution ) * 360. , radius / 180 * math . Pi * wgs84 . EquatorialRadius ( ) )
2025-02-18 14:21:51 +01:00
pointLats = append ( pointLats , gData . Lat2 )
pointLons = append ( pointLons , gData . Lon2 )
}
for i := 0 ; i < config . Config . RealtimeVisCircleResolution ; i ++ {
if i == 0 {
g . DrawLine ( pointLats [ config . Config . RealtimeVisCircleResolution - 1 ] , pointLons [ config . Config . RealtimeVisCircleResolution - 1 ] , pointLats [ 0 ] , pointLons [ 0 ] , color )
} else {
g . DrawLine ( pointLats [ i - 1 ] , pointLons [ i - 1 ] , pointLats [ i ] , pointLons [ i ] , color )
//fmt.Println(pointLats[i-1], pointLons[i-1])
}
}
}
//go:embed assets/NotoSansJP-VariableFont_wght.ttf
var fontBytes [ ] byte
var FontFace font . Face
func Init ( ) {
font , err := truetype . Parse ( fontBytes )
utils . CheckError ( err )
FontFace = truetype . NewFace ( font , & truetype . Options { Size : config . Config . RealtimeVisFontSize } )
}
func ( g * Game ) Layout ( outsideWidth , outsideHeight int ) ( int , int ) {
return config . Config . RealtimeVisRenderSize , config . Config . RealtimeVisRenderSize
}
func Render ( event jmaeew . JMAEEW ) {
2025-02-18 16:10:16 +01:00
if config . Config . TestWarning {
t := time . Now ( )
_ , offset := t . Zone ( )
event . OriginTime = time . Now ( ) . Add ( time . Second * time . Duration ( 9 * 3600 - offset ) ) . Format ( "2006/01/02 15:04:05" )
}
2025-02-18 14:45:03 +01:00
gameData = & Game { event , ComputeLoveImpactTime ( event ) }
2025-02-18 14:21:51 +01:00
if ! Displaying {
Displaying = true
//ebiten.SetWindowIcon([]image.Image{icons.WindowIcon48, icons.WindowIcon32, icons.WindowIcon16})
ebiten . SetWindowSize ( config . Config . RealtimeVisRenderSize , config . Config . RealtimeVisRenderSize )
ebiten . SetWindowTitle ( "hayai" )
ebiten . SetWindowMousePassthrough ( true )
ebiten . SetTPS ( config . Config . RealtimeVisFPS ) // target FPS todo
ebiten . SetWindowDecorated ( true )
screenW , screenH := ebiten . Monitor ( ) . Size ( )
ebiten . SetWindowPosition ( screenW / 2 - config . Config . RealtimeVisRenderSize / 2 , screenH / 2 - config . Config . RealtimeVisRenderSize / 2 )
ebiten . SetVsyncEnabled ( true )
ebiten . SetWindowClosingHandled ( true )
2025-02-18 14:56:16 +01:00
if err := ebiten . RunGameWithOptions ( gameData , & ebiten . RunGameOptions { } ) ; err != nil {
2025-02-18 14:21:51 +01:00
log . Fatal ( err )
}
}
}