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