Show server errors (non-200 code)
[pulsecounter.git] / web / index.html
1 <?xml version="1.0" encoding="utf-8"?>
2 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
3         "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
4 <html xml:lang="en" lang="en" xmlns="http://www.w3.org/1999/xhtml">
5 <head>
6 <script>
7   var dbg, errordiv;
8   var canvas, ctx;
9   var ww, wh;
10   var hmax, hfact;
11   var tmin, tmax, tfact;
12   var xzero = 40, yzero = 48;
13   var cold_d = [], hot_d = [];
14
15   function showdate(utime) {
16     var dt = new Date(utime*1000);
17     return dt.toLocaleDateString() + " " + dt.toLocaleTimeString();
18   }
19
20   function px(x) {
21     return xzero + ((x - tmin) * tfact);
22   }
23
24   function py(y) { return wh - yzero - (y * hfact);
25   }
26
27   const dow = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
28   const mn = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
29               "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
30
31   function getcombx(lo, hi) {
32     var comb = [], lb = [];
33     var trange = tmax - tmin;
34     var inc, inc2, base, t, dt;
35     var label = function(t) { return "<<" + t.toFixed(1) + ">>"; }
36     function tohour(t) { var dt = new Date(t*1000); return dt.getHours(); }
37     function todow(t) { var dt = new Date(t*1000); return dow[dt.getDay()]; }
38     function todom(t) { var dt = new Date(t*1000); return dt.getDate(); }
39     function tomonth(t) { var dt = new Date(t*1000); return mn[dt.getMonth()]; }
40
41     if (trange < 172800) { /* two days -> one hour */
42       inc = 3600;
43       inc2 = 21600;
44       label = tohour;
45     }
46     else if (trange < 864000) { /* 10 days -> six hours */
47       inc = 21600;
48       inc2 = 86400;
49       label = todow;
50     }
51     else if (trange < 2678400) { /* 31 days -> 1 day */
52       inc = 86400;
53       inc2 = 86400;
54       label = todom;
55     }
56     else { /* ~ one month */
57       /* TODO: make this a separate case with loop over months rather than
58          fixed number of seconds. */
59       inc = 2592000;
60       inc2 = 2592000;
61       label = tomonth;
62     }
63
64     dt = new Date(tmin*1000);
65     base = (Math.floor(tmin / inc) + 1) * inc + 60 * dt.getTimezoneOffset();
66     for (t = base; t < tmax; t += inc)
67       comb.push(t);
68     base = (Math.floor(tmin / inc2) + 1) * inc2 + 60 * dt.getTimezoneOffset();
69     for (t = base; t < tmax; t += inc2)
70       lb.push([t, label(t)]);
71     //dbg.innerHTML = "ord=" + ord + "<br>inc=" + inc + "<br>"
72     //              + comb + "<br>" + lb;
73     return [comb, lb];
74   }
75
76   function xaxis() {
77     var comb = getcombx(0, hmax);
78     var i;
79
80     ctx.beginPath();
81     for (i = 0; comb[0][i]; i++) {
82       ctx.moveTo(px(comb[0][i]), py(0) + 5);
83       ctx.lineTo(px(comb[0][i]), py(0));
84     }
85     ctx.strokeStyle = "gray";
86     ctx.stroke();
87
88     ctx.beginPath();
89     ctx.moveTo(px(tmin), py(0));
90     ctx.lineTo(px(tmax), py(0));
91     ctx.strokeStyle = "black";
92     ctx.stroke();
93
94     ctx.fillStyle = "black";
95     ctx.font = "bold 16px Courier";
96     ctx.textAlign = "center";
97     ctx.beginPath();
98     for (i = 0; comb[1][i]; i++) {
99       ctx.fillText(comb[1][i][1], px(comb[1][i][0]), py(0) + 20);
100       ctx.moveTo(px(comb[1][i][0]), py(0) + 5);
101       ctx.lineTo(px(comb[1][i][0]), py(0));
102     }
103     ctx.strokeStyle = "black";
104     ctx.stroke();
105
106     ctx.fillStyle = "black";
107     ctx.textAlign = "left";
108     ctx.fillText(showdate(tmin), px(tmin), py(0) + 40);
109     ctx.textAlign = "right";
110     ctx.fillText(showdate(tmax), px(tmax), py(0) + 40);
111   }
112
113   function getcomby(lo, hi) {
114     var comb = [], lb = [];
115     var d = hi - lo;
116     var ord = Math.pow(10, Math.floor(Math.log10(d)));
117     var scl = Math.floor(d / ord);
118     var inc, inc2, first, x, lb;
119
120     if (scl < 2) { inc = 0.1; inc2 = 0.2; }
121     else if (scl < 5) { inc = 0.1; inc2 = 0.5; }
122     else { inc = 0.5; inc2 = 1; }
123     inc *= ord;
124     inc2 *= ord;
125     first = (Math.floor(lo / inc) + 1) * inc;
126     for (x = 0; x < (d / inc) - 1.2; x++)
127       comb.push(first + inc * x);
128     first = (Math.floor(lo / inc2) + 1) * inc2;
129     for (x = 0; x < (d / inc2) - 1.2; x++)
130       lb.push(first + inc2 * x);
131     //dbg.innerHTML = "ord=" + ord + "<br>inc=" + inc + "<br>"
132     //              + comb + "<br>" + lb;
133     return [comb, lb];
134   }
135
136   function yaxis() {
137     var comb = getcomby(0, hmax);
138     var i;
139
140     ctx.beginPath();
141     for (i = 0; comb[0][i]; i++) {
142       ctx.moveTo(px(tmin) - 5, py(comb[0][i]));
143       ctx.lineTo(px(tmax), py(comb[0][i]));
144     }
145     ctx.strokeStyle = "lightgray";
146     ctx.stroke();
147
148     ctx.beginPath();
149     ctx.moveTo(px(tmin), py(0));
150     ctx.lineTo(px(tmin), py(hmax));
151     ctx.strokeStyle = "black";
152     ctx.stroke();
153
154     ctx.fillStyle = "black";
155     ctx.font = "bold 16px Courier";
156     ctx.textAlign = "right";
157     ctx.fillText(0, px(tmin) - 6, py(0));
158     ctx.beginPath();
159     for (i = 0; comb[1][i]; i++) {
160       ctx.fillText(comb[1][i].toFixed(1), px(tmin) - 6, py(comb[1][i]));
161       ctx.moveTo(px(tmin) - 5, py(comb[1][i]));
162       ctx.lineTo(px(tmax), py(comb[1][i]));
163     }
164     ctx.strokeStyle = "gray";
165     ctx.stroke();
166
167     ctx.textAlign = "left";
168     ctx.fillText("l/min", px(tmin) + 4, py(hmax) + 12);
169   }
170
171   /* @ updates global var `hmax` */
172   function differentiate(times) {
173     var res = [];
174     var dv, dt, v;
175
176     for (i = 0; i < times.length - 1; i++) {
177       dv = times[i+1][1] - times[i][1];
178       dt = times[i+1][0] - times[i][0];
179       if (dt != 0 && dv != 0) {
180         v = (dv / dt) * 600 ; /* Litres per min */
181         if (hmax < v) hmax = v;
182         res.push([times[i][0], v]);
183       }
184     }
185     if (i) res.push([times[i][0], v]);
186
187     return res;
188   }
189
190   function drawplot(data, color) {
191     var i;
192
193     ctx.beginPath();
194     ctx.moveTo(px(data[0][0]), py(data[0][1]));
195     for (i = 1; i < data.length; i++) {
196       ctx.lineTo(px(data[i][0]), py(data[i - 1][1]));
197       ctx.lineTo(px(data[i][0]), py(data[i][1]));
198     }
199     ctx.strokeStyle = color;
200     ctx.stroke();
201   }
202
203   function showloading() {
204     ctx.fillStyle = "green";
205     ctx.font = "bold 16px Courier";
206     ctx.textAlign="center";
207     ctx.fillText("...loading...", (ww / 2) , (wh / 2) + 8);
208   }
209
210   function showempty() {
211     ctx.fillStyle = "red";
212     ctx.font = "bold 24px Courier";
213     ctx.textAlign="center";
214     ctx.fillText("No data for the requested time interval",
215                  (ww / 2) , (wh / 2) + 8);
216   }
217
218   function clearplot() {
219     ctx.clearRect(0, 0, ww, wh);
220   }
221
222   function redraw() {
223     errordiv.style.visibility = "hidden";
224     errordiv.innerHTML = "";
225     clearplot();
226     if (cold_d.length || hot_d.length) {
227       tfact = (ww - xzero) / (tmax - tmin);
228       hfact = (wh - yzero) / hmax;
229       xaxis();
230       yaxis();
231       drawplot(cold_d, "blue");
232       drawplot(hot_d, "red");
233     } else {
234       showempty();
235     }
236   }
237
238   function gotdata(data) {
239     document.getElementById("curcold").innerHTML =
240       (data.current.cold / 100).toFixed(2);
241     document.getElementById("curhot").innerHTML =
242       (data.current.hot / 100).toFixed(2);
243
244     document.getElementById("totcold").innerHTML =
245       ((data.cold[data.cold.length - 1][1] - data.cold[0][1]) * 10);
246     document.getElementById("tothot").innerHTML =
247       ((data.hot[data.hot.length - 1][1] - data.hot[0][1]) * 10);
248
249     tmin = data.range.lo;
250     tmax = data.range.hi;
251     //dbg.innerHTML = "from " + tmin + " to " + tmax
252     //              + "<br>from " + showdate(tmin) + " to " + showdate(tmax);
253     /* differetiate() updates hmax */
254     hmax = 0;
255     cold_d = differentiate(data.cold);
256     hot_d = differentiate(data.hot);
257     //dbg.innerHTML = "hmax=" + hmax + " hfact=" + hfact + "<br>"
258     //              + cold_d + "<br>" + hot_d;
259     redraw();
260   }
261
262   function sendreq(qstr) {
263     var url = "query.cgi" + qstr;
264     var xmlhttp = new XMLHttpRequest();
265
266     //dbg.innerHTML = url;
267     xmlhttp.onreadystatechange = function() {
268       if (xmlhttp.readyState == 4)
269         if (xmlhttp.status == 200) {
270           // dbg.innerHTML = xmlhttp.responseText;
271           var myData = JSON.parse(xmlhttp.responseText);
272           gotdata(myData);
273         } else {
274           errordiv.style.visibility = "visible";
275           errordiv.style.display = "block";
276           errordiv.innerHTML = xmlhttp.responseText;
277         }
278     }
279     xmlhttp.open("GET", url, true);
280     clearplot();
281     showloading();
282     xmlhttp.send();
283   }
284
285   function iso2qu(idate) {
286     return idate.replace("T", "+").replace("0Z", "");
287   }
288
289   function sendquery(lo, hi) {
290     return sendreq("?lo=" + iso2qu(lo) + "&hi=" + iso2qu(hi));
291   }
292
293   function resize() {
294     ww = window.innerWidth - 4;
295     if (ww > window.innerHeight) ww = window.innerHeight;
296     wh = ww / 2;
297     canvas.width = ww;
298     canvas.height = wh;
299     canvas.style.width = ww + "px";
300     canvas.style.height = wh + "px";
301     redraw();
302   }
303
304   function daystart(date) {
305     date.setMilliseconds(0);
306     date.setSeconds(0);
307     date.setMinutes(0);
308     date.setHours(0);
309     return date;
310   }
311
312   function prevweek() {
313     var tdy = daystart(new Date());
314     var dow = tdy.getDay();
315     var wstart, wend;
316
317     wstart = new Date(1*tdy - 86400000 * (dow + 7));
318     wend = new Date(1*wstart + 86400000 * 7);
319     sendquery(wstart.toISOString(), wend.toISOString());
320   }
321
322   function thisweek() {
323     var tdy = daystart(new Date());
324     var dow = tdy.getDay();
325     var wstart, wend;
326
327     wstart = new Date(1*tdy - 86400000 * dow);
328     wend = new Date(1*wstart + 86400000 * 7);
329     sendquery(wstart.toISOString(), wend.toISOString());
330   }
331
332   function beforeyesterday() {
333     var tdy = daystart(new Date());
334     var ytd = new Date(1*tdy - 86400000);
335     var byd = new Date(1*ytd - 86400000);
336     sendquery(byd.toISOString(), ytd.toISOString());
337   }
338
339   function yesterday() {
340     var tdy = daystart(new Date());
341     var ytd = new Date(1*tdy - 86400000);
342     sendquery(ytd.toISOString(), tdy.toISOString());
343   }
344
345   function today() {
346     var tdy = daystart(new Date());
347     var tmr = new Date(1*tdy + 86400000);
348     sendquery(tdy.toISOString(), tmr.toISOString());
349   }
350
351   function initialize() {
352     var qstr = window.location.search;
353
354     dbg = document.getElementById("debug");
355     errordiv = document.getElementById("errormsg");
356     canvas = document.getElementById("plot");
357     ctx = canvas.getContext("2d");
358     resize();
359     if (qstr) sendreq(qstr);
360     else today();
361     
362     document.getElementById("today").onclick = today;
363     document.getElementById("yesterday").onclick = yesterday;
364     document.getElementById("beforeyesterday").onclick = beforeyesterday;
365     document.getElementById("thisweek").onclick = thisweek;
366     document.getElementById("prevweek").onclick = prevweek;
367   }
368
369   /* Set up */
370   window.onload = initialize;
371   window.onresize = resize;
372 </script>
373 <style>
374 @font-face {
375   font-family: PipeDream;
376   src: url('PIPED.TTF') format('truetype');
377   /* Free to use font from http://www.mlink.net/~paterson/jpfonts.htm */
378 }
379 h1 {
380   margin-top: 5px;
381   text-align: center;
382   font-family: PipeDream;
383   font-size: 64px;
384   font-weight: normal;
385   background-color: lightgray;
386 }
387 br {
388   clear: both;
389 }
390 div#currentvals {
391   width: 18em;
392   margin-left: auto;
393   margin-right: auto;
394   margin-bottom: 10px;
395   text-align: center;
396   font-size: 150%;
397 }
398 div#totalvals {
399   width: 18em;
400   margin-left: auto;
401   margin-right: auto;
402   margin-bottom: 10px;
403   text-align: center;
404   font-size: 100%;
405 }
406 div.current {
407   position: relative;
408   padding: 0.2em;
409   border: solid 1px black;
410   margin: 0.2em;
411 }
412 div.cold {
413   float: left;
414   background-color: #d0e0ff;
415 }
416 div.hot {
417   float: right;
418   background-color: #ffd0e0;
419 }
420 canvas#plot {
421   padding: 0px;
422   margin: auto;
423   display: block;
424   width: 640px;
425   height: 320px;
426   /* border: solid 1px black; */
427 }
428 div#errormsg {
429   visibility: hidden;
430   color: red;
431   text-align: center;
432 }
433 div#queries {
434   margin-left: auto;
435   margin-right: auto;
436   margin-bottom: 10px;
437   text-align: center;
438 }
439 div.query {
440   display: inline-block;
441   position: relative;
442   width: 8em;
443   height: 8em;
444   border: solid 1px black;
445   border-radius: 1em;
446   background-color: lightgray;
447   cursor: pointer;
448 }
449 div.label {
450   display: block;
451   width: 100%;
452   position: absolute;
453   top: 50%;
454   transform: translate(0, -50%);
455 }
456 body {
457   margin: 0px;
458 }
459 </style>
460 <title>Water Meters</title>
461 </head><body>
462 <h1>WATER METERS</h1>
463 <div id="currentvals">
464   Current Readings (m<sup>3</sup>)
465   <div class="current cold" id="curcold">cold</div>
466   <div class="current hot" id="curhot">hot</div>
467 </div>
468 <br />
469 <canvas id="plot" width="640" height = "320"></canvas>
470 <br />
471 <div id="errormsg"></div>
472 <br />
473 <div id="totalvals">
474   Total for the period (l)
475   <div class="current cold" id="totcold">cold</div>
476   <div class="current hot" id="tothot">hot</div>
477 </div>
478 <br />
479 <div id="queries">
480  <div class="query" id="prevweek"><div class="label">PREVIOUS WEEK</div></div>
481  <div class="query" id="beforeyesterday"><div class="label">DAY
482    BEFORE YESTERDAY</div></div>
483  <div class="query" id="yesterday"><div class="label">YESTERDAY</div></div>
484  <div class="query" id="today"><div class="label">TODAY</div></div>
485  <div class="query" id="thisweek"><div class="label">THIS WEEK</div></div>
486 </div>
487 <br />
488 <div id="debug"></div>
489 </body>