Created
December 4, 2025 22:39
-
-
Save vadim0x60/65ee294d4563b47b8c15f12b58e07dcb to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| const hour_ms = 3600 * 1000 | |
| function average(arr) { | |
| sum = null | |
| for (const elem of arr) { | |
| if (elem != null) { | |
| sum += elem | |
| } | |
| } | |
| if (sum != null) { | |
| return sum / arr.length | |
| } | |
| else { | |
| return null | |
| } | |
| } | |
| function close_enough(warning, event) { | |
| let too_late = warning['start'].getTime() - event.getEndTime().getTime() > event_spillover | |
| let too_early = event.getStartTime().getTime() - warning['end'].getTime() > event_spillover | |
| return !(too_late || too_early) | |
| } | |
| function time_filter(events, start, end) { | |
| let filtered = [] | |
| for (const event of events) { | |
| let startstamp = event['start'] ? event['start'].getTime() : Number.NEGATIVE_INFINITY | |
| let endstamp = event['end'] ? event['end'].getTime() : Number.POSITIVE_INFINITY | |
| startstamp = Math.max(start.getTime(), startstamp) | |
| endstamp = Math.min(end.getTime(), endstamp) | |
| if (endstamp >= startstamp) { | |
| let copy = {...event} | |
| copy['start'] = new Date(startstamp), | |
| copy['end'] = new Date(endstamp) | |
| filtered.push(copy) | |
| } | |
| } | |
| return filtered | |
| } | |
| function event_time(event) { | |
| return event.getStartTime().getTime() + event.getEndTime().getTime() | |
| } | |
| function sort_events(events) { | |
| return events.sort((a, b) => (a['start'].getTime() + a['end'].getTime()) - (b['start'].getTime() + b['end'].getTime())) | |
| } | |
| function clear_calendar(calendar, start, end) { | |
| for (let event of calendar.getEvents(start, end)) { | |
| event.deleteEvent() | |
| } | |
| } | |
| function populate_calendar(calendar, events) { | |
| for (let event of events) { | |
| if (event != null) { | |
| calendar.createEvent(event['description'], event['start'], event['end']) | |
| } | |
| } | |
| } | |
| function provision_calendar(calendar_name) { | |
| let calendars = CalendarApp.getCalendarsByName(calendar_name) | |
| let the_calendar | |
| if (calendars.length == 0) { | |
| the_calendar = CalendarApp.createCalendar(calendar_name) | |
| } | |
| else { | |
| the_calendar = calendars[0] | |
| } | |
| return the_calendar | |
| } | |
| function getDaysArray(start, end) { | |
| days = [] | |
| d = new Date(start) | |
| d.setDate(d.getDate() - 1) | |
| days.push(new Date(d)) | |
| while(d <= end) { | |
| d.setDate(d.getDate() + 1) | |
| days.push(new Date(d)) | |
| } | |
| return days | |
| } | |
| function zip_longest(args) { | |
| result = [] | |
| length = Math.max(...args.map(a => a.length)) | |
| for (i = 0; i < length; i++) { | |
| result.push(args.map(a => a[i] || null)) | |
| } | |
| return result | |
| } | |
| function smart_append(events, event) { | |
| if (event == null) return | |
| latest_event = events.at(-1) | |
| if (events.length > 0 && latest_event['description'] == event['description'] && latest_event['end'] >= event['start']) { | |
| latest_event['start'] = new Date(Math.min(latest_event['start'], event['start'])) | |
| latest_event['end'] = new Date(Math.max(latest_event['end'], event['end'])) | |
| } | |
| else { | |
| events.push(event) | |
| } | |
| } | |
| const loc_regex = new RegExp('\-?([0-9\.]+);\-?([0-9\.]+)') | |
| const geocode_horizon = 5 // days | |
| const geocoder = Maps.newGeocoder() | |
| function geocode_location(location_description) { | |
| let latlon, lat, lon | |
| if (!location_description) { | |
| return [null, null] | |
| } | |
| let match = loc_regex.exec(location_description) | |
| if (match) { | |
| [latlon, lat, lon] = match | |
| } | |
| if (lat && lon) { | |
| return [lat, lon] | |
| } | |
| else { | |
| console.log(location_description) | |
| for (const result of geocoder.geocode(location_description).results) { | |
| lat = result.geometry.location.lat | |
| lon = result.geometry.location.lng | |
| return [lat, lon] | |
| } | |
| } | |
| return [null, null] | |
| } | |
| function geocode_events(recheck) { | |
| let now = new Date() | |
| let horizon_end = new Date() | |
| horizon_end.setDate(now.getDate() + 5) | |
| for (let calendar of CalendarApp.getAllOwnedCalendars()) { | |
| for (let event of calendar.getEvents(now, horizon_end)) { | |
| if (!recheck && event.getTag('lat') && event.getTag('lon')) { | |
| continue | |
| } | |
| let [lat, lon] = geocode_location(event.getLocation()) | |
| if (lat && lon) { | |
| event.setTag('lat', lat.toString()) | |
| event.setTag('lon', lon.toString()) | |
| } | |
| } | |
| } | |
| } | |
| function* all_locations(horizon_start, horizon_end) { | |
| var warnings = [] | |
| var location_events = [] | |
| var locations_left = location_lookahead | |
| for (calendar_name of location_calendars) { | |
| calendar = CalendarApp.getCalendarsByName(calendar_name)[0] | |
| location_events.push(...calendar.getEvents(horizon_start, horizon_end)) | |
| location_events = location_events.sort((a, b) => event_time(a) - event_time(b)) | |
| } | |
| for (let location of location_events) { | |
| if (location.getTag('lat') && location.getTag('lon')) { | |
| yield location | |
| locations_left -= 1 | |
| } | |
| if (locations_left == 0) break | |
| } | |
| } | |
| function assign_geocodes() { | |
| geocode_events(false) | |
| } | |
| function review_geocodes() { | |
| geocode_events(true) | |
| } | |
| const weather_api_key = 'nice try' | |
| const weather_lookahead = 3 * 24 * 60 * 60 * 1000 // 3 days | |
| const event_spillover = 4 * 60 * 60 * 1000 // 4 hours | |
| // weather_requests_per_day = 800 | |
| const location_lookahead = 10 | |
| const weather_trigger_minutes = 15 //roundup((24 * 60 * location_lookahead) / (2 * weather_requests_per_day)) | |
| // My weather perferences | |
| const too_hot = 27 | |
| const max_pop = 0.75 // pop - probability of precipitation | |
| const temperature_format = new Intl.NumberFormat("en-GB", { style: "decimal", signDisplay: 'always' }); | |
| const weather_calendar = provision_calendar('#weather') | |
| const location_calendars = ['uncommited', 'commited', 'crucial', 'log', 'stays'] | |
| function as_warning(forecast, duration, location) { | |
| problem = '' | |
| temperature = null | |
| description = null | |
| if ('feels_like' in forecast) { | |
| temperature = Number(forecast['feels_like']) || average(Object.values(forecast['feels_like'])) | |
| if (temperature > too_hot) { | |
| problem += 'hot' | |
| } | |
| } | |
| pop = forecast['pop'] || (forecast['precipitation'] > 0) | |
| if (pop > max_pop) { | |
| problem += 'rain' | |
| } | |
| if (problem) { | |
| if ('weather' in forecast) { | |
| description = forecast['weather'][0]['description'] | |
| } | |
| description = (description || problem) + ' @' + location | |
| if (temperature) { | |
| description = temperature_format.format(Math.round(temperature)) + ' ' + description | |
| } | |
| return { | |
| 'start': new Date(forecast.dt * 1000), | |
| 'end': new Date((forecast.dt + duration) * 1000), | |
| 'description': description | |
| } | |
| } | |
| else { | |
| return null | |
| } | |
| } | |
| function fetch_local_warnings(lat, lon, loc) { | |
| var warnings = [] | |
| const api_call = 'https://api.openweathermap.org/data/3.0/onecall?lat=' + lat + '&lon=' + lon + '&appid=' + weather_api_key + '&units=metric' | |
| const weather = JSON.parse(UrlFetchApp.fetch(api_call)); | |
| if ('minutely' in weather) { | |
| for (forecast of weather['minutely']) { | |
| smart_append(warnings, as_warning(forecast, 60, loc)) | |
| } | |
| } | |
| for (forecast of weather['hourly'].slice(1)) { | |
| smart_append(warnings, as_warning(forecast, 60 * 60, loc)) | |
| } | |
| return warnings | |
| } | |
| function update_weather() { | |
| now = new Date() | |
| horizon_end = new Date(now.getTime() + weather_lookahead) | |
| warnings = [] | |
| for (location of all_locations(now, horizon_end)) { | |
| local_warnings = fetch_local_warnings(location.getTag('lat'), location.getTag('lon'), location.getTitle()) | |
| local_warnings = local_warnings.filter(w => w && close_enough(w, location)) | |
| warnings.push(...local_warnings) | |
| } | |
| now.setMinutes(0,0,0) // calendar.getEvents() filters by start of event instead of its full span | |
| clear_calendar(weather_calendar, now, horizon_end) | |
| populate_calendar(weather_calendar, warnings) | |
| } | |
| function deleteAllTriggers(){ | |
| let triggers = ScriptApp.getProjectTriggers(); | |
| for (let trigger of triggers) { | |
| ScriptApp.deleteTrigger(trigger); | |
| } | |
| } | |
| function install() { | |
| //Delete any already existing triggers so we don't create excessive triggers | |
| deleteAllTriggers(); | |
| //Schedule sync routine | |
| ScriptApp.newTrigger("update_weather").timeBased().everyMinutes(weather_trigger_minutes).create() | |
| ScriptApp.newTrigger("assign_geocodes").timeBased().everyMinutes(10).create() | |
| ScriptApp.newTrigger("review_geocodes").timeBased().everyHours(12).create() | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment