diff --git a/apps.json b/apps.json index 80dc1e2b1..b6192b2e1 100644 --- a/apps.json +++ b/apps.json @@ -265,6 +265,31 @@ {"name":"slidingtext.dtfmt.js","url":"slidingtext.dtfmt.js"} ] }, + { "id": "solarclock", + "name": "Solar Clock", + "icon": "solar_clock.png", + "version":"0.01", + "description": "The solar clock will use your current or chosen location to work out the the current sun phase of the day", + "tags": "clock", + "type":"clock", + "allow_emulator":false, + "readme": "README.md", + "storage": [ + {"name":"solarclock.app.js","url":"solar_clock.js"}, + {"name":"solarclock.img","url":"solar_clock-icon.js","evaluate":true}, + {"name":"solar_colors.js","url":"solar_colors.js"}, + {"name":"solar_controller.js","url":"solar_controller.js"}, + {"name":"solar_date_utils.js","url":"solar_date_utils.js"}, + {"name":"solar_graphic_utils.js","url":"solar_graphic_utils.js"}, + {"name":"solar_location.js","url":"solar_location.js"}, + {"name":"solar_math_utils.js","url":"solar_math_utils.js"}, + {"name":"solar_loc.Iceland.json","url":"solar_loc.Iceland.json"}, + {"name":"solar_loc.Kauai.json","url":"solar_loc.Kauai.json"}, + {"name":"solar_loc.Tokyo.json","url":"solar_loc.Tokyo.json"}, + {"name":"solar_loc.local.json","url":"solar_loc.local.json"} + {"name":"solar_locations.json","url":"solar_locations.json"} + ] + }, { "id": "sweepclock", "name": "Sweep Clock", "icon": "sweepclock.png", diff --git a/apps/solarclock/ChangeLog b/apps/solarclock/ChangeLog new file mode 100644 index 000000000..d53df991b --- /dev/null +++ b/apps/solarclock/ChangeLog @@ -0,0 +1 @@ +0.01: Initial Release diff --git a/apps/solarclock/README.md b/apps/solarclock/README.md new file mode 100644 index 000000000..51c95139b --- /dev/null +++ b/apps/solarclock/README.md @@ -0,0 +1 @@ +# Solar Clock diff --git a/apps/solarclock/app.png b/apps/solarclock/app.png new file mode 100644 index 000000000..763c00b1d Binary files /dev/null and b/apps/solarclock/app.png differ diff --git a/apps/solarclock/solar_clock-icon.js b/apps/solarclock/solar_clock-icon.js new file mode 100644 index 000000000..d9bdd8c65 --- /dev/null +++ b/apps/solarclock/solar_clock-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("lEowkA/4AGmYIHABHzmVCCaE0kUin4TPmUimQTQ+UzmcvJ6EjCaP/kYABCaEymYTl+Q7SMgITTmQTQPAK0RMgITm+QTS+ciPCcikQpPY4MjmYTO+czmcyHh4TCmcvJ54nCPCBjBJx4oECc8zJ6ATTn48RE4YTTHh4SDH4ImRFBwTGFBgTGFBgSGFBYmHUgITRmcyFBASFAoUjE5PzkQLBHJxiDAQP/GxAA==")) diff --git a/apps/solarclock/solar_clock.js b/apps/solarclock/solar_clock.js new file mode 100644 index 000000000..d1154e110 --- /dev/null +++ b/apps/solarclock/solar_clock.js @@ -0,0 +1,466 @@ +const DateUtils = require("solar_date_utils.js"); +const Math2 = require("solar_math_utils.js"); +const GraphicUtils = require("solar_graphic_utils.js"); +const Colors = require("solar_colors.js"); +const LocationUtils = require("solar_location.js"); + +var screen_info = { + screen_width : g.getWidth(), + screen_start_x : 0, + screen_centre_x: g.getWidth()/2, + screen_height : (g.getHeight()-100), + screen_start_y : 100, + screen_centre_y : 90 + (g.getHeight()-100)/2, + screen_bg_color : Colors.BLACK, + sun_radius: 8, + sun_x : null, + sun_y : null, + sunrise_y : null, +} +const img_width=40; +const img_height=30; +var img_buffer = Graphics.createArrayBuffer(img_width,img_height,8); +var img = {width:img_width,height:img_height,bpp:8,transparent:0,buffer:img_buffer.buffer}; +var img_info = { + x: null, + y: null, + img: img, + img_buffer: img_buffer +} +const COSINE_COLOUR= Colors.GREY; +const HORIZON_COLOUR = Colors.GREY; +const SolarController = require("solar_controller.js"); +var controller = new SolarController(); +var curr_mode = null; +var last_sun_draw_time = null; +var draw_full_cosine = true; + +function draw_sun(now, day_info) { + + var now_fraction = (now.getTime() - day_info.day_start.getTime())/DateUtils.DAY_MILLIS; + var now_x = now_fraction * screen_info.screen_width; + if(screen_info.sun_x != null && Math.abs(now_x- screen_info.sun_x) < 1){ + console.log("no sun movement"); + return false; + } + // now calculate thew new sun coordinates + var now_radians = Math2.TWO_PI *(now_x - screen_info.screen_centre_x)/screen_info.screen_width; + var now_y = screen_info.screen_centre_y - (screen_info.screen_height * Math.cos(now_radians) / 2); + if(Math.abs(now_x - screen_info.sun_x) > 5){ + clear_sun(); + } + // update the screen info with the new sun info + screen_info.sun_x = now_x; + screen_info.sun_y = now_y; + last_sun_draw_time = now; + + if(draw_full_cosine){ + //console.log("drawing full cosine"); + GraphicUtils.draw_cosine(screen_info.screen_start_x, + screen_info.screen_width, + COSINE_COLOUR, + screen_info); + draw_full_cosine = false; + } + if(curr_mode == null) { + GraphicUtils.draw_sunrise_line(HORIZON_COLOUR, day_info, screen_info); + } + // decide on the new sun drawing mode and draw + curr_mode = controller.mode(now,day_info,screen_info); + img_info.img_buffer.clear(); + img_info.img_buffer.setColor(screen_info.screen_bg_color[0], + screen_info.screen_bg_color[1], + screen_info.screen_bg_color[2], + ); + img_info.img_buffer.fillRect(0,0,img_width, img_height); + img_info.x = screen_info.sun_x - img_info.img.width/2; + img_info.y = screen_info.sun_y - img_info.img.height/2; + + var cosine_dist = screen_info.sun_radius/Math.sqrt(2); + GraphicUtils.draw_cosine(img_info.x, + screen_info.sun_x - cosine_dist, + COSINE_COLOUR, + screen_info, + img_info); + GraphicUtils.draw_cosine(screen_info.sun_x + cosine_dist, + screen_info.sun_x + img_width, + COSINE_COLOUR, + screen_info, + img_info); + + curr_mode.draw(now,day_info,screen_info,img_info); + + var sunrise_dist = Math.abs(screen_info.sunrise_y-screen_info.sun_y); + if( sunrise_dist <= img_height) { + GraphicUtils.draw_sunrise_line(HORIZON_COLOUR, day_info, screen_info,img_info); + } else if(sunrise_dist <= img_height*2.5) { + GraphicUtils.draw_sunrise_line(HORIZON_COLOUR, day_info, screen_info); + } + // we draw a blank where the image is going to be drawn to clear out the area + g.setColor(screen_info.screen_bg_color[0],screen_info.screen_bg_color[1],screen_info.screen_bg_color[2]); + g.fillRect(img_info.x,img_info.y-2,img_info.x+img_width,img_info.y + img_height + 2); + g.drawImage(img,img_info.x,img_info.y); + // paint the cosine curve back to the normal color where it just came from + GraphicUtils.draw_cosine(img_info.x - 3, + img_info.x, + COSINE_COLOUR, + screen_info); + GraphicUtils.draw_cosine(img_info.x + img_width, + img_info.x + img_width + 3, + COSINE_COLOUR, + screen_info); + return true; +} + +function clear_sun(){ + g.setColor(screen_info.screen_bg_color[0],screen_info.screen_bg_color[1],screen_info.screen_bg_color[2]); + g.fillRect(img_info.x,img_info.y,img_info.x+img_width,img_info.y + img_width); + GraphicUtils.draw_cosine(img_info.x - 4, + img_info.x + img_width + 4, + COSINE_COLOUR, + screen_info); + GraphicUtils.draw_sunrise_line(HORIZON_COLOUR, day_info, screen_info); + screen_info.sun_x = null; + screen_info.sun_y = null; +} + +var last_time = null; +var last_offset = null; +var last_date = null; + +const time_color = Colors.WHITE; +const date_color = Colors.YELLOW; +const DATE_Y_COORD = 35; +const DATE_X_COORD = 10; +const TIME_X_COORD = 140; +const TIME_Y_COORD = 35; +const OFFSET_Y_COORD = 70; +const LOCATION_Y_COORD = 55; + +function write_date(now){ + var new_date = require('locale').dow(now,1) + " " + Math2.format00(now.getDate()); + //console.log("writing date:" + new_date) + g.setFont("Vector",15); + g.setFontAlign(-1,-1,0); + if(last_date != null){ + if(new_date == last_date){ + return; + } + g.setColor(screen_info.screen_bg_color[0], + screen_info.screen_bg_color[1], + screen_info.screen_bg_color[2]); + g.drawString(last_date, DATE_X_COORD,DATE_Y_COORD); + } + g.setColor(date_color[0],date_color[1],date_color[2]); + g.drawString(new_date, DATE_X_COORD,DATE_Y_COORD); + last_date = new_date; +} + +var last_status_msg = "" +var last_gps_coords_msg_n = ""; +var last_gps_coords_msg_e = ""; +const GPS_MSG_X_COORD = 70; +const GPS_MSG_Y = 220; +const GPS_MSG_COORDS_Y_E = 80; +const GPS_MSG_COORDS_Y_N = 90; + +function write_GPS_status(){ + var gps_coords = location.getCoordinates(); + var gps_coords_msg_n; + var gps_coords_msg_e; + if(gps_coords != null){ + gps_coords_msg_n = "N: " + gps_coords[0]; + gps_coords_msg_n = gps_coords_msg_n.substr(0,Math.min(gps_coords_msg_n.length - 1,10)); + gps_coords_msg_e = "E: " + gps_coords[1]; + gps_coords_msg_e = gps_coords_msg_e.substr(0,Math.min(gps_coords_msg_e.length - 1,10)); + } else { + gps_coords_msg_n = ""; + gps_coords_msg_e = ""; + } + var status_msg = ""; + if(location.isGPSLocation()) { + if(gps_coords == null) { + if (location.getGPSPower() > 0) { + status_msg = "Finding GPS Position"; + } else { + status_msg = "ERROR GPS Position not found"; + } + } else { + if (location.getGPSPower() > 0) { + status_msg = "Updating GPS Position"; + } + } + } + g.setFont("Vector",11); + g.setFontAlign(-1,-1,0); + if(last_status_msg != status_msg) { + g.setColor(screen_info.screen_bg_color[0], + screen_info.screen_bg_color[1], + screen_info.screen_bg_color[2]); + g.drawString(last_status_msg, GPS_MSG_X_COORD, GPS_MSG_Y); + g.setColor(Colors.YELLOW[0],Colors.YELLOW[1],Colors.YELLOW[2]); + g.drawString(status_msg, GPS_MSG_X_COORD, GPS_MSG_Y); + last_status_msg = status_msg; + } + + if(last_gps_coords_msg_e != gps_coords_msg_e) { + g.setColor(screen_info.screen_bg_color[0], + screen_info.screen_bg_color[1], + screen_info.screen_bg_color[2]); + g.drawString(last_gps_coords_msg_e, DATE_X_COORD, GPS_MSG_COORDS_Y_E); + g.drawString(last_gps_coords_msg_n, DATE_X_COORD, GPS_MSG_COORDS_Y_N); + g.setColor(Colors.WHITE[0],Colors.WHITE[1],Colors.WHITE[2]); + + g.drawString(gps_coords_msg_e, DATE_X_COORD, GPS_MSG_COORDS_Y_E); + g.drawString(gps_coords_msg_n, DATE_X_COORD, GPS_MSG_COORDS_Y_N); + last_gps_coords_msg_e = gps_coords_msg_e; + last_gps_coords_msg_n = gps_coords_msg_n; + } +} + +function write_time(now){ + var new_time = format_time(now); + g.setFont("Vector",35); + g.setFontAlign(-1,-1,0); + if(last_time != null){ + g.setColor(screen_info.screen_bg_color[0],screen_info.screen_bg_color[1],screen_info.screen_bg_color[2]); + g.drawString(last_time, TIME_X_COORD,TIME_Y_COORD); + } + g.setColor(time_color[0],time_color[1],time_color[2]); + g.drawString(new_time, TIME_X_COORD,TIME_Y_COORD); + last_time = new_time; +} + +function format_time(now){ + var time = new Date(now.getTime() - time_offset); + var hours = time.getHours() % 12; + if(hours < 1){ + hours = 12; + } + return Math2.format00(hours) + ":" + Math2.format00(time.getMinutes()); +} + +function write_offset(){ + var new_offset = format_offset(); + g.setFont("Vector",15); + g.setFontAlign(-1,-1,0); + if(last_offset != null){ + g.setColor(screen_info.screen_bg_color[0],screen_info.screen_bg_color[1],screen_info.screen_bg_color[2]); + g.drawString(last_offset, TIME_X_COORD,OFFSET_Y_COORD); + } + g.setColor(time_color[0],time_color[1],time_color[2]); + g.drawString(new_offset, TIME_X_COORD,OFFSET_Y_COORD); + last_offset = new_offset; +} + +function format_offset(){ + if(time_offset == 0) + return ""; + + var hours_offset = Math.abs(time_offset) / DateUtils.HOUR_MILLIS; + var mins_offset = Math.abs(time_offset) / DateUtils.MIN_MILLIS; + var mins_offset_from_hour = mins_offset % 60; + //console.log("mins offset=" + mins_offset + " mins_offset_from_hour=" + mins_offset_from_hour); + var sign = "+"; + if(time_offset < 0) + sign = "-"; + + return sign + Math2.format00(hours_offset) + ":" + Math2.format00(mins_offset_from_hour); +} + +let time_offset = 0; +let last_draw_time = null; +var day_info = null; +var location = LocationUtils.load_locations(); +var last_location_name = null; + +function write_location_name() { + var new_location_name = location.getName(); + g.setFont("Vector", 20); + g.setFontAlign(-1, -1, 0); + if (last_location_name != null) { + g.setColor(screen_info.screen_bg_color[0], screen_info.screen_bg_color[1], screen_info.screen_bg_color[2]); + g.drawString(last_location_name, DATE_X_COORD, LOCATION_Y_COORD); + } + g.setColor(time_color[0], time_color[1], time_color[2]); + if (new_location_name != "local") { + g.drawString(new_location_name, DATE_X_COORD, LOCATION_Y_COORD); + } + last_location_name = new_location_name; +} + +location.addUpdateListener( + (loc)=>{ + console.log("location update:" + JSON.stringify(loc)); + clear_sun(); + GraphicUtils.draw_sunrise_line(screen_info.screen_bg_color, day_info, screen_info); + day_info = null; + screen_info.sunrise_y = null; + curr_mode = null; + draw_clock(); + } +); + + + +function dayInfo(now) { + if (day_info == null || now > day_info.day_end) { + var coords = location.getCoordinates(); + if(coords != null) { + day_info = DateUtils.sunrise_sunset(now, coords[0], coords[1], location.getUTCOffset()); + //console.log("day info:" + JSON.stringify(day_info)); + } else { + day_info = null; + } + } + return day_info; +} + +function time_now() { + var timezone_offset_hours = location.getUTCOffset(); + if(timezone_offset_hours != null) { + var local_offset_hours = -new Date().getTimezoneOffset()/60; + var timezone_offset_millis = + (timezone_offset_hours - local_offset_hours) * DateUtils.HOUR_MILLIS; + return new Date(Date.now() + time_offset + timezone_offset_millis); + } else { + return new Date(Date.now() + time_offset); + } +} + + +function draw_clock(){ + var start_time = Date.now(); + var now = time_now(); + + var day_info = dayInfo(now); + if(day_info != null) { + draw_sun(now, day_info); + } + write_time(now); + write_date(now); + write_offset(); + write_location_name(); + write_GPS_status(); + last_draw_time = now; + log_memory_used(); + var time_taken = Date.now() - start_time; + console.log("drawing clock:" + now.toISOString() + " time taken:" + time_taken ); +} + +function log_memory_used() { + var memory = process.memory(); + console.log("memory used:" + memory.usage + + " total:" + memory.total + "->" + + " ->" + memory.usage/memory.total + ); +} + +function button1pressed(){ + console.log("button 1 pressed"); + time_offset = 0; + clear_sun(); + day_info = null; + draw_clock(); +} + +function button3pressed(){ + console.log("button 3 pressed"); + time_offset = 0; + location.nextLocation(); +} + +function button4pressed(){ + time_offset -= DateUtils.HOUR_MILLIS/4; + draw_clock(); + setTimeout(()=>{ + if(BTN4.read()){ + button4pressed(); + } + }, + 50 + ) +} + +function button5pressed(){ + time_offset += DateUtils.HOUR_MILLIS/4; + draw_clock(); + setTimeout(()=>{ + if(BTN5.read()){ + button5pressed(); + } + }, + 50 + ) +} + +// The interval reference for updating the clock +let interval_ref = null; +function clear_timers(){ + if(interval_ref != null) { + clearInterval(interval_ref); + interval_ref = null; + } +} + +function start_timers(){ + var date = new Date(); + var secs = date.getSeconds(); + var nextMinuteStart = 60 - secs; + setTimeout(schedule_draw_clock,nextMinuteStart * 1000); + draw_clock(); +} +function schedule_draw_clock(){ + clear_timers(); + if (Bangle.isLCDOn()) { + interval_ref = setInterval(() => { + if (!Bangle.isLCDOn()) { + console.log("draw clock callback - skipped redraw"); + } else { + draw_clock(); + } + }, DateUtils.MIN_MILLIS + ); + draw_clock(); + } else { + console.log("scheduleDrawClock - skipped not visible"); + } +} + +Bangle.on('lcdPower', (on) => { + if (on) { + console.log("lcdPower: on"); + draw_clock(); + start_timers(); + } else { + console.log("lcdPower: off"); + clear_timers(); + } +}); + +Bangle.on('faceUp',function(up){ + if (up && !Bangle.isLCDOn()) { + clear_timers(); + Bangle.setLCDPower(true); + } +}); + +g.clear(); +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +start_timers(); + +function button2pressed(){ + controller = null; + + location.shutdown(); + location = null; + + Bangle.showLauncher(); +} +setWatch(button2pressed, BTN2,{repeat:false,edge:"falling"}); +setWatch(button1pressed, BTN1,{repeat:true,edge:"falling"}); +setWatch(button3pressed, BTN3,{repeat:true,edge:"falling"}); +setWatch(button4pressed, BTN4,{repeat:true,edge:"rising"}); +setWatch(button5pressed, BTN5,{repeat:true,edge:"rising"}); \ No newline at end of file diff --git a/apps/solarclock/solar_clock.png b/apps/solarclock/solar_clock.png new file mode 100644 index 000000000..70a1cd532 Binary files /dev/null and b/apps/solarclock/solar_clock.png differ diff --git a/apps/solarclock/solar_colors.js b/apps/solarclock/solar_colors.js new file mode 100644 index 000000000..a048cf308 --- /dev/null +++ b/apps/solarclock/solar_colors.js @@ -0,0 +1,14 @@ +const _BLACK= [0.0,0.0,0.0]; +const _GREY= [0.5,0.5,0.5]; +const _WHITE= [1.0,1.0,1.0]; +const _YELLOW= [1.0,1.0,0.0]; +const _RED= [1.0,0.0,0.0]; + +const Colors = { + BLACK: _BLACK, + GREY: _GREY, + WHITE: _WHITE, + YELLOW: _YELLOW, + RED: _RED +}; +module.exports = Colors; \ No newline at end of file diff --git a/apps/solarclock/solar_controller.js b/apps/solarclock/solar_controller.js new file mode 100644 index 000000000..12a2933de --- /dev/null +++ b/apps/solarclock/solar_controller.js @@ -0,0 +1,247 @@ +const Math2 = require("solar_math_utils.js"); +const DateUtils = require("solar_date_utils.js"); +const GraphicUtils = require("solar_graphic_utils.js"); +const Colors = require("solar_colors.js"); + +const CORONA_GAP = 2; +const CORONA_MIN_LENGTH = 2; +const CORONA_LINES = 12; +const CORONA_RADIUS = 14; +const SUNSET_START_HEIGHT = 10; +const SUNSET_COLOUR = Colors.RED; +const SUNRISE_COLOUR = [1,0.6,0]; +const MIDDAY_COLOUR = [1,1,0.7]; +const NIGHT_COLOUR = Colors.BLACK; + +function daytime_sun_color(now,day_info){ + var now_fraction_of_day =DateUtils.now_fraction_of_day(now,day_info); + if(now > day_info.sunset_date){ + return SUNSET_COLOUR; + } else if(now < day_info.sunrise_date){ + return SUNRISE_COLOUR; + } else if(now < day_info.solar_noon) { + var sunrise_fraction = (day_info.sunrise_date - day_info.day_start) / DateUtils.DAY_MILLIS; + var rise_to_midday_fraction = (now_fraction_of_day - sunrise_fraction) / (0.5 - sunrise_fraction); + return Math2.interpolate(SUNRISE_COLOUR, MIDDAY_COLOUR, rise_to_midday_fraction); + } else { + var sunset_fraction = (day_info.day_end - day_info.sunset_date) / DateUtils.DAY_MILLIS; + var midday_to_sunset_fraction = (now_fraction_of_day - 0.5)/(0.5 - sunset_fraction); + //console.log("sunset_fraction=" + sunset_fraction + " midday_to_sunset_fraction=" + midday_to_sunset_fraction) + return Math2.interpolate(MIDDAY_COLOUR,SUNSET_COLOUR,midday_to_sunset_fraction); + } +} + +function draw_night_sun(sun_x,sun_y,sun_radius,img_info){ + var draw_info = GraphicUtils.draw_info(img_info); + draw_info.buff.setColor(Colors.WHITE[0],Colors.WHITE[1],Colors.WHITE[2]); + draw_info.buff.fillCircle(sun_x - draw_info.offset_x, + sun_y - draw_info.offset_y, + sun_radius); + draw_info.buff.setColor(NIGHT_COLOUR[0],NIGHT_COLOUR[1],NIGHT_COLOUR[2]); + draw_info.buff.fillCircle(sun_x - draw_info.offset_x, + sun_y - draw_info.offset_y, + sun_radius-1); +} + +function draw_partial_sun(time, day_info, screen_info,img_info){ + var sun_height = screen_info.sunrise_y - screen_info.sun_y; + if(sun_height > screen_info.sun_radius){ + var sun_color = daytime_sun_color(time,day_info); + var draw_info = GraphicUtils.draw_info(img_info); + draw_info.buff.setColor(sun_color[0],sun_color[1],sun_color[2]); + draw_info.buff.fillCircle(screen_info.sun_x - draw_info.offset_x, + screen_info.sun_y - draw_info.offset_y, + screen_info.sun_radius + ); + } else if(sun_height < -screen_info.sun_radius){ + draw_night_sun(screen_info.sun_x,screen_info.sun_y,screen_info.sun_radius, img_info); + } else { + var draw_info = GraphicUtils.draw_info(img_info); + draw_info.buff.setColor(NIGHT_COLOUR[0],NIGHT_COLOUR[1],NIGHT_COLOUR[2]); + draw_info.buff.fillCircle(screen_info.sun_x - draw_info.offset_x, + screen_info.sun_y - draw_info.offset_y, + screen_info.sun_radius-1); + var sun_color = daytime_sun_color(time,day_info); + draw_info.buff.setColor(sun_color[0],sun_color[1],sun_color[2]); + draw_info.buff.drawCircle(screen_info.sun_x - draw_info.offset_x, + screen_info.sun_y - draw_info.offset_y, + screen_info.sun_radius); + GraphicUtils.fill_circle_partial_y(screen_info.sun_x, + screen_info.sun_y, + screen_info.sun_radius, + screen_info.sun_y - screen_info.sun_radius, + screen_info.sunrise_y, + img_info + ); + } +} +function draw_random_background(screen_info, + img_info, + rgb_init, + rgb_step + ){ + var draw_info = GraphicUtils.draw_info(img_info); + var rgb = rgb_init; + var sky_to = Math.min(screen_info.sunrise_y-1, img_info.y + img_info.img.height - 3); + for(var sky_y=img_info.y+3;sky_y0) + rgb[i] = Math2.random_walk(rgb[i],rgb_step[i],1,0); + } + draw_info.buff.setColor(rgb[0],rgb[1],rgb[2]); + draw_info.buff.moveTo(screen_info.sun_x + + Math.random()*img_info.img.width/8 - + 0.4*img_info.img.width - + draw_info.offset_x, + sky_y - draw_info.offset_y); + draw_info.buff.lineTo(screen_info.sun_x + + 0.4*img_info.img.width - + Math.random()*img_info.img.width/8 - + draw_info.offset_x, + sky_y - draw_info.offset_y); + } + draw_info.buff.setColor(NIGHT_COLOUR[0],NIGHT_COLOUR[1],NIGHT_COLOUR[2]); + draw_info.buff.fillCircle(screen_info.sun_x - draw_info.offset_x, + screen_info.sun_y - draw_info.offset_y, + screen_info.sun_radius+1); +} +class SolarMode { + test(time, day_info, screen_info){ throw "test undefined";} + draw(time, day_info, screen_info, img_buffer_info){ + throw "sun drawing undefined"; + } +} +class NightMode extends SolarMode { + toString(){return "NightMode";} + test(time, day_info, screen_info, img_info) { + return (time < day_info.sunrise_date || time > day_info.sunset_date); + } + draw(time, day_info, screen_info, img_info){ + draw_night_sun(screen_info.sun_x,screen_info.sun_y,screen_info.sun_radius, img_info); + } +} +class DayLightMode extends SolarMode { + toString(){ + return "DayLightMode"; + } + test(time, day_info, screen_info){ + var sun_height = screen_info.sunrise_y - screen_info.sun_y; + /*console.log("DayLightMode " + + "time=" + time.toISOString() + + " sunset_date=" + day_info.sunset_date.toISOString() + + " sunrise_date=" + day_info.sunrise_date.toISOString() + );*/ + return time < day_info.sunset_date && + time > day_info.sunrise_date && + sun_height >= screen_info.sun_radius * 2 + SUNSET_START_HEIGHT; + } + _calc_corona_radius(now, day_info){ + if(now < day_info.sunset_date && + now > day_info.sunrise_date){ + var now_fraction_of_day =DateUtils.now_fraction_of_day(now,day_info); + var sunset_fraction = (day_info.sunset_date.getTime() - day_info.day_start.getTime())/DateUtils.DAY_MILLIS; + var now_fraction_from_midday = + 1 - Math.abs(now_fraction_of_day-0.5)/(sunset_fraction-0.5); + return CORONA_RADIUS * now_fraction_from_midday; + } else { + return 0; + } + } + _drawCorona(corona_radius,sun_x,sun_y,sun_radius, draw_info){ + var thickness_rads = (Math2.TWO_PI/CORONA_LINES)/3; + var from_radius = sun_radius + CORONA_GAP; + if(corona_radius > from_radius + CORONA_MIN_LENGTH) { + for (var i = 0; i < CORONA_LINES; i++) { + var to_x1 = sun_x - draw_info.offset_x + from_radius * Math.cos(i * Math2.TWO_PI / CORONA_LINES + thickness_rads); + var to_y1 = sun_y - draw_info.offset_y + from_radius * Math.sin(i * Math2.TWO_PI / CORONA_LINES + thickness_rads); + var to_x2 = sun_x - draw_info.offset_x + from_radius * Math.cos(i * Math2.TWO_PI / CORONA_LINES - thickness_rads); + var to_y2 = sun_y - draw_info.offset_y + from_radius * Math.sin(i * Math2.TWO_PI / CORONA_LINES - thickness_rads); + var to_x3 = sun_x - draw_info.offset_x + corona_radius * Math.cos(i * Math2.TWO_PI / CORONA_LINES); + var to_y3 = sun_y - draw_info.offset_y + corona_radius * Math.sin(i * Math2.TWO_PI / CORONA_LINES); + draw_info.buff.fillPoly([to_x1, to_y1, to_x2, to_y2, to_x3, to_y3]); + } + } + } + draw(now, day_info, screen_info, img_info){ + var sun_color = daytime_sun_color(now,day_info); + var corona_radius = this._calc_corona_radius(now, day_info); + var draw_info = GraphicUtils.draw_info(img_info); + draw_info.buff.setColor(sun_color[0],sun_color[1],sun_color[2]); + if(corona_radius > screen_info.sun_radius){ + this._drawCorona(corona_radius, + screen_info.sun_x, + screen_info.sun_y, + screen_info.sun_radius, + draw_info); + } + draw_info.buff.fillCircle(screen_info.sun_x - draw_info.offset_x, + screen_info.sun_y - draw_info.offset_y, + screen_info.sun_radius); + } +} +class TwiLightMode extends SolarMode { + toString(){ + return "TwilightMode"; + } + test(time, day_info, screen_info){ + if(screen_info.sunrise_y == null) { + console.log("warning no sunrise_defined"); + return false; + } + var sun_height = screen_info.sunrise_y - screen_info.sun_y; + /*console.log("TwilightMode " + + "time=" + time.toISOString() + + " sun_height=" + sun_height + + " sun_radius=" + screen_info.sun_radius + );*/ + if(sun_height > -screen_info.sun_radius && + sun_height < screen_info.sun_radius * 2 + SUNSET_START_HEIGHT + ){ + //console.log("selected TwilightMode"); + return true; + } + return false; + } + draw(time, day_info, screen_info, img_info){ + if(time < day_info.solar_noon) { + draw_random_background(screen_info, + img_info, + [0,0.8,1], + [0.05,0.05,0.0]); + } else { + draw_random_background(screen_info, + img_info, + [1,0.75,Math.random()], + [0,0.05,0.05]); + } + draw_partial_sun(time,day_info,screen_info,img_info); + } +} +class SolarControllerImpl { + constructor(){ + this.solar_modes = [new TwiLightMode(), new DayLightMode()]; + this.default_mode = new NightMode(); + this.last = null; + } + toString(){ + return "SolarControllerImpl"; + } + mode(time, day_info, screen_info){ + if(this.last != null){ + if(this.last.test(time,day_info,screen_info)){ + return this.last; + } + } + for(var i=0; i" + solar_noon_datetime.toISOString()); + + var sunrise_time_LST = (solar_noon*1440-HA_sunrise_degrees*4)/1440; + var sunrise_time_LST_datetime = _to_time(now,sunrise_time_LST); + console.log("sunrise_time_LST=" + sunrise_time_LST + + "->" + sunrise_time_LST_datetime.toISOString()); + + var sunset_time_LST =(solar_noon*1440+HA_sunrise_degrees*4)/1440; + var sunset_time_LST_datetime = _to_time(now,sunset_time_LST); + console.log("sunset_time_LST=" + sunset_time_LST + + "->" + sunset_time_LST_datetime.toISOString()); + return { + day_start: new Date(solar_noon_datetime.getTime() - _DAY_MILLIS / 2), + sunrise_date: sunrise_time_LST_datetime, + //sunrise_fraction: sunrise_time_LST, + sunset_date: sunset_time_LST_datetime, + //sunset_fraction: sunset_time_LST, + solar_noon: solar_noon_datetime, + day_end: new Date(solar_noon_datetime.getTime() + _DAY_MILLIS / 2) + }; + }, + now_fraction_of_day: (now,day_info)=>{ + return (now.getTime() - day_info.day_start.getTime())/_DAY_MILLIS; + }, +} +module.exports = DateUtils; \ No newline at end of file diff --git a/apps/solarclock/solar_graphic_utils.js b/apps/solarclock/solar_graphic_utils.js new file mode 100644 index 000000000..3c2afec88 --- /dev/null +++ b/apps/solarclock/solar_graphic_utils.js @@ -0,0 +1,94 @@ +var DateUtils = require("solar_date_utils.js"); +var Math2 = require("solar_math_utils.js"); + +function _draw_info(img_info){ + if (img_info == null) { + return { + buff: g, + offset_x: 0, + offset_y: 0 + }; + } else { + return { + buff: img_info.img_buffer, + offset_x: img_info.x, + offset_y: img_info.y + }; + } + +} +const GraphicUtils = { + draw_info : (img_info)=>_draw_info(img_info), + draw_cosine : (from_x,to_x, line_colour, screen_info, img_info)=>{ + //console.log("draw_cosine from_x=" + from_x + " to_x=" + to_x); + var draw_info = _draw_info(img_info); + + draw_info.buff.reset(); + draw_info.buff.setColor(line_colour[0],line_colour[1],line_colour[2]); + first = true; + for(var x=from_x; x { + if (!this.in_use) + return; + + if (g.fix) { + var loc_info = { + last_update: new Date(), + coordinates: [g.lon, g.lat] + }; + console.log("Received gps fixing:" + JSON.stringify(loc_info)); + storage.writeJSON("solar_loc.local.json", this.location_info); + if(this.isGPSLocation()){ + this.location_info = loc_info; + this.notifyUpdate(); + } + this.setGPSPower(0); + } + }); + } + isGPSLocation(){return this.getName() == 'local';} + addUpdateListener(listener){this.listeners.push(listener);} + nextLocation() { + if(this.locations.length > 1) { + this.idx += 1; + this.idx = this.idx % this.locations.length; + console.log("location now:" + this.getName()); + this.init(); + this.notifyUpdate(); + } else { + console.log("no extra locations found"); + } + } + notifyUpdate(){ + for(var i=0; i{ + var locations = storage.readJSON(LOCATIONS_FILE); + console.log("loaded locations:" + locations); + var mgr = new LocationManager(locations); + mgr.init(); + return mgr; + } +} +module.exports = LocationUtils; \ No newline at end of file diff --git a/apps/solarclock/solar_locations.json b/apps/solarclock/solar_locations.json new file mode 100644 index 000000000..750b43206 --- /dev/null +++ b/apps/solarclock/solar_locations.json @@ -0,0 +1 @@ +["local","Tokyo","Iceland","Kauai"] \ No newline at end of file diff --git a/apps/solarclock/solar_math_utils.js b/apps/solarclock/solar_math_utils.js new file mode 100644 index 000000000..c88ba9961 --- /dev/null +++ b/apps/solarclock/solar_math_utils.js @@ -0,0 +1,34 @@ +const _TWO_PI = 2 * Math.PI; +const Maths2 = { + TWO_PI: _TWO_PI, + to_radians: (degrees)=> _TWO_PI * degrees / 360, + to_degrees: (radians)=> 360 * radians/ (_TWO_PI), + interpolate: (vector1, vector2, fraction)=>{ + var result = []; + for(var i=0; i< vector1.length; i++){ + var value = vector1[i] + (vector2[i] - vector1[i]) * fraction; + result.push(value); + } + return result; + }, + format00: (num)=>{ + var value = (num | 0); + if(value > 99 || value < 0) + throw "must be between in range 0-99"; + if(value < 10) + return "0" + value.toString(); + else + return value.toString(); + }, + random_walk: (value,step,max,min)=>{ + if(Math.random()>0.5){ + value -= step; + } else { + value += step; + } + value = Math.min(value,max); + value = Math.max(value,min); + return value; + } +} +module.exports = Maths2; \ No newline at end of file