|
<!doctype html> |
|
<head> |
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> |
|
<meta name="viewport" content="width=device-width, initial-scale=1"> |
|
<link rel="manifest" href="/manifest.webmanifest" /> |
|
<script src="gif.js?v=3"></script> |
|
<style> |
|
body { |
|
text-align: center; |
|
font: .8rem Inconsolata, monospace; |
|
margin: 0px; |
|
padding: 0px; |
|
} |
|
#animation { |
|
height: 84vh; |
|
} |
|
#animation img |
|
{ |
|
max-width: 100%; |
|
max-height: 100%; |
|
height: 100%; |
|
margin: auto; |
|
display: block; |
|
} |
|
body {background-color: #A9A9A9; |
|
margin-top: 1px; |
|
margin-bottom: 1px; |
|
} |
|
.dark-mode { |
|
background-color: black; |
|
color: white; |
|
} |
|
|
|
#elem_frame_rate, #elem_frames_to_skip { |
|
width: 2em; |
|
} |
|
#rangeText{ |
|
position: relative; |
|
bottom: 7px; |
|
} |
|
#info{ |
|
position: relative; |
|
width: 150px; |
|
display: inline-block; |
|
} |
|
#bottom{ |
|
display: block; |
|
} |
|
.container { |
|
display: flex; |
|
flex-wrap: wrap; |
|
justify-content: center; |
|
} |
|
.left { |
|
float:left; |
|
display: inline-block; |
|
} |
|
.right { |
|
float:right; |
|
display: inline-block; |
|
} |
|
|
|
</style> |
|
|
|
</head> |
|
<body> |
|
<div id="animation"> |
|
<img id = 'image'/> |
|
</div> |
|
<div id=top><div id="info"></div> |
|
<div style="display: inline-block;"> |
|
<input type="radio" id="radio-latest" name="radio-display" value="latest" |
|
checked onclick="setDisplayMode('latest');"> |
|
<label for="latest">Latest</label> |
|
</div> |
|
<div style="display: inline-block;"> |
|
<input type="radio" id="radio-display" name="radio-display" value="query" onclick="elem_display_changed_to_query();"> |
|
<label for="query">Date Range</label> |
|
</div> |
|
</div> |
|
<div id="datediv" style="display: hidden;"> |
|
<label for="start-time">Start</label> |
|
<input type="datetime-local" id="start-time" |
|
name="start-time" onchange="query_start_date = this.value"> |
|
<label for="end-time">End</label> |
|
<input type="datetime-local" id="end-time" |
|
name="end-time" onchange="query_end_date = this.value"> |
|
</div> |
|
<div id="rangediv"> |
|
<input type="range" id="range" name="range" |
|
min="0" max="5" oninput="range_changed(parseInt(this.value))" > |
|
<label id="rangeText"></label> |
|
</div> |
|
<div id="bottom"> |
|
<select id="elem_image_type" name="elem_image_type" onchange=elem_image_type_changed(this.value)> |
|
<option>keypoints</option> |
|
<option selected>objectKey</option> |
|
</select> |
|
<select id="elem_station" name="elem_station" onchange=elem_station_changed(this.value)> |
|
<option>palmer</option> |
|
<option selected>talkeetna</option> |
|
<option>paradise</option> |
|
<option>bartlett</option> |
|
</select> |
|
<select id="elem_items_to_show" name="elem_items_to_show" onchange=elem_items_to_show_changed(this.value)> |
|
<option selected>5</option> |
|
<option>10</option> |
|
<option>25</option> |
|
<option>50</option> |
|
<option>100</option> |
|
<option>200</option> |
|
<option>300</option> |
|
</select> |
|
<input type="button" value="OK" onclick="okClicked();" /> |
|
<input type="checkbox" id="loop" name="loop" onclick="loop=!loop;"> |
|
<label for="loop">Loop</label> |
|
<div class="container"> |
|
<div class="left"><fieldset> |
|
<input type="number" placeholder="6" step=".1" min="0" max="24" value="6" id="elem_frame_rate" onchange=elem_frame_rate_changed(this.value)> |
|
<label id="">frames/sec</label> |
|
</fieldset> |
|
</div> |
|
<div class="right"><fieldset> |
|
<input type="number" placeholder="1" step="1" min="1" max="60" value="1" id="elem_frames_to_skip" onchange=elem_frames_to_skip_changed(this.value)> |
|
<label id="">mins/frame</label> |
|
</fieldset> |
|
</div> |
|
<input type="button" name="gif_button" value="GIF" onclick="gifClicked();" /> |
|
<input type="button" name="theme_button" value="🔆" onclick="toggleTheme();" /> |
|
</div> |
|
</div> |
|
<script> |
|
|
|
const url_pfx = 'https://dvfvh7sumgfb0.cloudfront.net/'; |
|
const image_types = ['objectKey', 'keypoints']; |
|
const stations_url = 'https://api.af7ti.com/peakog'; |
|
var data; |
|
var data_frames; |
|
var station; |
|
var stations; |
|
var image_type; |
|
var loop=false; |
|
var items_to_show = document.getElementById("elem_items_to_show").value; |
|
var i = parseInt(items_to_show); |
|
var frameRate = 6; //frames per second |
|
var frames_to_skip = 1; |
|
var displayMode; //strings 'latest' or 'query' (date range) |
|
|
|
function toggleTheme() { |
|
var element = document.body; |
|
element.classList.toggle("dark-mode"); |
|
} |
|
|
|
function elem_frames_to_skip_changed(val) { |
|
frames_to_skip = Number(val); |
|
} |
|
|
|
function elem_display_changed_to_query() { |
|
setDisplayMode('query'); |
|
document.getElementById('bottom').scrollIntoView(); |
|
} |
|
function elem_frame_rate_changed(val) { |
|
frameRate = Number(document.getElementById("elem_frame_rate").value); |
|
clearInterval(interval); |
|
} |
|
function updateRange(val) { |
|
console.log(`updateRange: val is ${val}`); |
|
document.getElementById("range").value = val; |
|
i = val; |
|
range_changed(val); |
|
} |
|
|
|
function update_elem_image_type(operator) { |
|
//console.log(`update_elem_image_type: val is ${operator}`); |
|
let idx = image_types.indexOf(image_type); |
|
if (operator == '+') { |
|
console.log(`update_elem_image_type: incrementing`); |
|
idx < image_types.length -1 ? idx = idx + 1 : idx = 0; |
|
} |
|
if (operator == '-') { |
|
console.log(`update_elem_image_type: decrementing`); |
|
idx >= 1 ? idx = idx - 1 : idx = image_types.length - 1; |
|
} |
|
val = image_types[idx]; |
|
console.log(`update_elem_image_type val is ${val}`); |
|
document.getElementById("elem_image_type").value = val; |
|
elem_image_type_changed(val); |
|
} |
|
|
|
function updateLabel(i) { |
|
document.getElementById("rangeText").innerHTML = new Date(data[i].updated).toUTCString(); |
|
document.getElementById("rangeText").innerHTML = applyTimeOffset(station, data[i].updated) |
|
} |
|
|
|
function range_changed(val) { |
|
//console.log(`range_changed: val is ${val}`); |
|
loop = false; |
|
clearInterval(interval); |
|
document.getElementById("loop").checked = false; |
|
i = val; |
|
changeImage(val); |
|
updateLabel(val); |
|
document.getElementById("info").innerHTML = getWxText(i); |
|
} |
|
function elem_items_to_show_changed(val) { |
|
console.log(`elem_items_to_show_changed fired`); |
|
items_to_show = val; |
|
setRangeMax(); |
|
update(); |
|
} |
|
function elem_image_type_changed(val) { |
|
console.log(`elem_image_type_changed: val is ${val}`); |
|
clearInterval(interval); |
|
let idx = image_types.indexOf(image_type); |
|
console.log(`elem_image_type_changed: idx is ${idx}`); |
|
image_type = val; |
|
console.log(`elem_image_type_changed: image_type is ${image_type}`); |
|
console.log(`elem_image_type_changed: i is ${i}`); |
|
changeImage(i); |
|
if (loop === true) fetch_loop(); |
|
} |
|
function elem_station_changed(val) { |
|
clearInterval(interval); |
|
station = val; |
|
initializeDates(); |
|
//getdata(); |
|
update(); |
|
} |
|
function setRangeMax() { |
|
document.getElementById("range").max = items_to_show -1; |
|
} |
|
|
|
function startAnimation(data) { |
|
typeof interval != "undefined" ? clearInterval(interval) : ''; |
|
//let data_frames = data.map(d => d[image_type]); //; |
|
//console.log(`startAnimation: i is ${i}`); |
|
var frameCount = data_frames.length; |
|
i = 0; |
|
interval = setInterval(function (d) { |
|
if (i > frameCount-1) { |
|
clearInterval(interval); |
|
if (loop == false) return; |
|
if (loop == true) startAnimation(data); |
|
} |
|
if (i < frameCount) { |
|
document.getElementById("image").src = `${url_pfx}${data_frames[i]}`; |
|
document.getElementById("range").value = i; |
|
document.getElementById("info").innerHTML = getWxText(i); |
|
updateLabel(i); |
|
if (i < frameCount ) i = i + 1; |
|
if (i == frameCount) return; |
|
} |
|
}, 1000/frameRate); |
|
} |
|
|
|
async function getdata() { |
|
|
|
all_data = []; |
|
data = []; |
|
json = []; |
|
|
|
all_data.length = 0; |
|
data.length = 0; |
|
json.length = 0; |
|
|
|
if (displayMode == 'latest') url=`https://api.af7ti.com/peakog/query?stationId=${station}&limit=${items_to_show}`; |
|
|
|
if (displayMode == 'query') url=`https://api.af7ti.com/peakog/search-between?stationId=${station}&start=${applyTimeOffsetPos(station, query_start_date)}&end=${applyTimeOffsetPos(station,query_end_date)}`; |
|
|
|
//console.log(`getdata displayMode is: ${displayMode} url is ${url}`); |
|
|
|
fetch(url) |
|
.then(response => response.json()) |
|
.then(json => json.reverse()) |
|
.then(all_data => { |
|
data = skipFrames(all_data.slice()); |
|
document.getElementById("range").max = data.length -1; |
|
fetch_loop() |
|
}) |
|
//.then(console.log('getdata done')); |
|
.catch((error) => { |
|
console.log(`getdata error ${error}`); |
|
}); |
|
} |
|
|
|
function skipFrames(arr) { |
|
let result = arr.slice().filter(function (value, index, ar) { |
|
return (index % frames_to_skip == 0); |
|
} ); |
|
return result; |
|
} |
|
|
|
function changeImage(val) { |
|
document.getElementById("image").src = `${url_pfx}${data.map(d => d[image_type])[val]}`; |
|
} |
|
|
|
async function fetch_loop(i=0) { |
|
if (data.length == 0) return; |
|
//console.log(`fetch_loop: i is ${i}`); |
|
data_frames = data.map(d => d[image_type]); |
|
let l = data.length; |
|
let url = url_pfx + data_frames[i]; |
|
//console.log(`fetch_loop start ${i} url is ${url} data length is ${data.length}`); |
|
if (i+1 < l) document.getElementById("info").innerHTML = `Loading ${i}/${l}`; |
|
if (i+1 == l) document.getElementById("info").innerHTML = ``; |
|
if (typeof i == "undefined") return; |
|
fetch(url) |
|
.then(response => response.blob()) |
|
//.then(response => console.log(`${i} done`)) |
|
.then(response => { |
|
if (i == l -1) { |
|
//sleep(1000); |
|
startAnimation(data); |
|
return; |
|
} |
|
fetch_loop(i+1) |
|
}) |
|
.catch((error) => { |
|
console.log(`fetch_loop error ${error}`); |
|
if (i == l -1) { |
|
//sleep(1000); |
|
startAnimation(data); |
|
return; |
|
} |
|
fetch_loop(i+1) |
|
}); |
|
} |
|
|
|
function sleep(ms) { |
|
return new Promise(resolve => setTimeout(resolve, ms)); |
|
} |
|
|
|
document.onkeydown = checkKey; |
|
|
|
function checkKey(e) { |
|
|
|
//make sure nothing like date picker is in focus |
|
if (document.activeElement.tagName != "BODY") return; |
|
|
|
e = e || window.event; |
|
|
|
if (e.keyCode === 38) { |
|
// up arrow |
|
update_elem_image_type('+') |
|
} |
|
else if (e.keyCode === 40) { |
|
// down arrow |
|
update_elem_image_type('-') |
|
} |
|
else if (e.keyCode === 37) { |
|
// left arrow |
|
i >= 1 ? i = i-1 : i = data.length - 1; |
|
updateRange(i); |
|
} |
|
else if (e.keyCode === 39) { |
|
// right arrow |
|
i < data.length -1 ? i = i+1 : i = 0; |
|
updateRange(i); |
|
} |
|
} |
|
|
|
function getdates(){ |
|
query_start_date = document.getElementById("start-time").value; |
|
query_end_date = document.getElementById("end-time").value; |
|
} |
|
function update() { |
|
clearInterval(interval); |
|
getdates(); |
|
getdata(); |
|
} |
|
|
|
|
|
function setDisplayMode(val){ |
|
clearInterval(interval); |
|
console.log(`setDisplayMode clicked: val is ${val}`); |
|
displayMode = val; |
|
if (val == 'latest'){ |
|
document.getElementById("datediv").style.display="none"; |
|
document.getElementById("elem_items_to_show").style.display=""; |
|
items_to_show = document.getElementById("elem_items_to_show").value; |
|
} |
|
if (val == 'query'){ |
|
document.getElementById("datediv").style.display=""; |
|
document.getElementById("elem_items_to_show").style.display="none"; |
|
} |
|
|
|
} |
|
|
|
async function start() { |
|
loadStations() |
|
.then(res => initializeDates()); |
|
document.getElementById("datediv").style.display="none"; |
|
displayMode = 'latest'; |
|
station = 'talkeetna'; |
|
image_type = 'objectKey'; |
|
setRangeMax(); |
|
getdata(); |
|
//initializeDates(); |
|
} |
|
|
|
function okClicked() { |
|
console.log(`ok clicked`); |
|
update(); |
|
} |
|
|
|
function gifClicked() { |
|
console.log(`gif clicked`); |
|
newgif(); |
|
} |
|
|
|
|
|
function initializeDates() { |
|
initialstart = new Date(new Date().getTime() - (1 * 60 * 60 * 1000)).toISOString().slice(0,19); |
|
initialend = new Date(new Date().getTime()).toISOString().slice(0,19) |
|
initialstart = applyTimeOffset(station, initialstart); |
|
initialend = applyTimeOffset(station, initialend); |
|
document.getElementById('start-time').value = initialstart; |
|
document.getElementById('end-time').value = initialend; |
|
//document.getElementById('end-time').max = new Date(Date.now()).getTime(); |
|
document.getElementById('start-time').min = new Date(new Date().getTime() - (25 * 60 * 60 * 1000)).toISOString().slice(0,19); |
|
} |
|
|
|
async function loadStations() { |
|
let response = await fetch(stations_url); |
|
stations = await response.json(); |
|
return stations; |
|
} |
|
|
|
function calcTimeOffset(val) { |
|
let result = parseInt(stations.find(d => d.stationId == val).timezone); |
|
return result; |
|
} |
|
|
|
function applyTimeOffset(station, val){ //given a station and gmt time return local time |
|
let offset = Math.abs(calcTimeOffset(station)) + new Date().getTimezoneOffset()/60; |
|
let dateA = new Date(val).getTime() |
|
let result = new Date(dateA - (offset * 60 * 60 * 1000)).toISOString().slice(0,19); |
|
return result; |
|
} |
|
|
|
function applyTimeOffsetPos(station, val){ //given a station and gmt time return local time |
|
let offset = Math.abs(calcTimeOffset(station)) - new Date().getTimezoneOffset()/60; |
|
let dateA = new Date(val).getTime() |
|
let result = new Date(dateA + (offset * 60 * 60 * 1000)).toISOString().slice(0,19); |
|
return result; |
|
} |
|
|
|
function getWxText(val) { |
|
//console.log(`getWxText: val is ${val}`); |
|
temp = ""; |
|
wind_dir = ""; |
|
wind_speed = ""; |
|
fractalIndex = ""; |
|
if (!isNaN(data[val].wx_temperature)) temp = `${Math.round(data[val].wx_temperature * 9/5 + 32 )}°F`; |
|
if (!isNaN(Number(data[val].wx_windDirection))) wind_dir = windDirection(data[val].wx_windDirection); |
|
if (!isNaN(data[val].wx_windSpeed)) wind_speed = `${Math.round(data[val].wx_windSpeed * 0.621371)}mph`; |
|
if (!isNaN(data[val].fractalIndex)) fractalIndex = `${data[val].fractalIndex}`; |
|
|
|
//typeof wind_dir == "undefined" ? wind_dir = "" : ""; |
|
//if (isNaN(wind_speed)) wind_speed = ""; |
|
//console.log(`temp is ${temp} wind_dir ${wind_dir} is wind_speed is ${wind_speed} `) |
|
return `${temp} ${wind_dir} ${wind_speed} ${fractalIndex}`; |
|
} |
|
|
|
function windDirection(val) { |
|
if (typeof val == "undefined" || val == "None") result = ""; |
|
if (0.1 <= val && val <= 22) result = '↓'; |
|
if (22 <= val && val <= 67) result = '↙'; |
|
if (67 <= val && val <= 107) result = '←'; |
|
if (107 <= val && val <= 147) result = '↖'; |
|
if (147 <= val && val <= 190) result = '↑'; |
|
if (190 <= val && val <= 230) result = '↗'; |
|
if (230 <= val && val <= 280) result = '→'; |
|
if (280 <= val && val <= 320) result = '↘'; |
|
if (320 <= val && val <= 360) result = '↓'; |
|
if (val == 0) result = '-'; |
|
|
|
if (typeof result == "undefined" ) result = ""; |
|
|
|
return result; |
|
} |
|
|
|
function imagesizes() { |
|
let w = document.getElementById('image').naturalWidth; |
|
let h = document.getElementById('image').naturalHeight; |
|
return [w,h]; |
|
}; |
|
|
|
appConfig = { |
|
quality: 50 |
|
}; |
|
|
|
async function newgif() { |
|
|
|
images = data_frames.map(d => url_pfx + d); |
|
gif_frames = []; |
|
gif = new GIF({ |
|
//debug: true, |
|
quality: appConfig.quality, |
|
workers: 4, |
|
//delay: 1000/frameRate |
|
width: imagesizes()[0], |
|
height: imagesizes()[1] |
|
|
|
}); |
|
|
|
gif.on('progress', function(p) { |
|
document.getElementById("info").innerHTML = "GIF " + Math.round(p * 100) + "%" + " Complete"; |
|
}); |
|
|
|
gif.on('finished', function(blob) { |
|
window.open(URL.createObjectURL(blob)); |
|
}); |
|
|
|
images.forEach(function(d) { |
|
let img = new Image(); |
|
img.onload=function(){ |
|
} |
|
img.crossOrigin = "anonymous"; |
|
img.src = d; |
|
gif_frames.push(img); |
|
}); |
|
gif_frames.forEach(d => gif.addFrame(d, {delay: 1000/frameRate})); |
|
await sleep(500); |
|
try { gif.render(); } |
|
catch(error) { |
|
console.log(error); |
|
} |
|
} |
|
|
|
start(); |
|
|
|
//Registering your worker |
|
|
|
if ('serviceWorker' in navigator) { |
|
navigator.serviceWorker.register('./serviceworker.js', {scope: '/'}) |
|
.then((reg) => { |
|
// registration worked |
|
console.log('Registration succeeded. Scope is ' + reg.scope); |
|
}).catch((error) => { |
|
// registration failed |
|
console.log('Registration failed with ' + error); |
|
}); |
|
} |
|
|
|
</script> |
|
</body> |
|
</html> |