extractTempHumd.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. <?php
  2. /* provides a table to store interesting information on descriptions submitted by users
  3. *
  4. * to use:
  5. * scanDescription("it rained, 37F and 40% humid -- we had dutch letter ice cream")
  6. *
  7. * it works using a table called "events". The table stores integer values representing various
  8. * observations a user has made while recording radon reading samples. The table includes a column
  9. * "type" that encodes things like rain, or snow, making the "events" table more flexible. Each event
  10. * stored in the table has a start and end time, encoded as seconds since unix epoch.
  11. *
  12. * to make this code useful for end users, these functions are provided to show the users interesting
  13. * insights from the descriptions they provided
  14. *
  15. * getRainPeriods(...): returns array(["start"=>{start}, "end"=>{end}], ...)
  16. * getSnowPeriods(...): returns array(["start"=>{start}, "end"=>{end}], ...)
  17. * getTemperatureData(...): returns array(["time"=>{time}, "reading"=>{reading}, ...)
  18. * getHumidityData(...): returns array(["time"=>{time}, "reading"=>{reading}, ...)
  19. * getRainData(...): returns array(["time"=>{time}, "reading"=>{reading}, ...)
  20. * getSnowData(...): returns array(["time"=>{time}, "reading"=>{reading}, ...)
  21. *
  22. * [?] these functions all take an optional start and end time parameters eg:
  23. * $lastWeekTemperatures = getTemperatureData(time() - strtotime("7 days", 0), time());
  24. *
  25. * to extend this, three functions are modified and optionally a frontend oriented helper function could be
  26. * defined here to retrieve stored data. The functions to be modified are regexParse, which handles
  27. * searching the user description, and it's sister function typeLookup which encodes the type of data, like
  28. * "celcius", "rain", or "snow" as a number. One may want to modify the third function, saveEvents if
  29. * additional processing needs to occur on data as regexParse should only be finding the data of interest
  30. */
  31. /* order 2021 Nov 14 6:30pm
  32. * aroydee 1-515-528-8009
  33. * 14. vermicelli combo
  34. * 29. pad thai chicken
  35. * H4. green curry beef
  36. * vietnamese eggnog (2)
  37. * order will be ready in 25 minutes
  38. */
  39. function scanDescription($text = false, $time = false) {
  40. # connect to mysql database radondb
  41. $db = mysql_connect("localhost", "root", "secom8703");
  42. mysql_select_db("radondb", $db);
  43. # make sure events table exists
  44. if(false === mysql_query("SELECT 1 FROM `events` LIMIT 1", $db)) {
  45. mysql_query("CREATE TABLE `events` (start INTEGER, end INTEGER, type INTEGER, value INTEGER, CONSTRAINT event UNIQUE (start, end, type))", $db);
  46. }
  47. # either perform extraction on entire table, or just user text
  48. if(false /* entire table */ === $text) {
  49. fwrite(STDERR, "scanning radondb for temperature and humidity data" . PHP_EOL);
  50. $result = mysql_query("SELECT time, description FROM locationLog WHERE id = 'note';", $db);
  51. while($row = mysql_fetch_array($result, MYSQL_ASSOC)) {
  52. //printRow($row);
  53. saveEvents($db, regexParse($row["description"]), $row["time"]);
  54. }
  55. mysql_free_result($result);
  56. } else saveEvents($db, regexParse($text), (false === $time) ?time() :$time);
  57. # mysql needs clean-up
  58. mysql_close($db);
  59. }
  60. // returns stats from user descriptions
  61. function regexParse($s) {
  62. $stats = $t = array();
  63. $s = strtolower($s);
  64. // do simple mathing based using NNN{symbol} pattern
  65. $terms = array("farenheit" => "f", "celcius" => "c", "humidity" => "%");
  66. foreach($terms as $k=>$v) if(1 === preg_match("/\d{1,3}(?={$v})/", $s, $t)) $stats[$k] = $t[0];
  67. // check for rain, and other keywords (do snow separately)
  68. $terms = array("rain");
  69. foreach($terms as $v) if(false !== strpos($s, $v)) $stats[$v] = true;
  70. // when checking for snow, ignore if you also find melt
  71. if(false !== strpos($s, "snow")) {
  72. if(false === strpos($s, "melt")) $stats["snow"] = true;
  73. }
  74. return $stats;
  75. }
  76. // the database has a table events schema that includes types, defined in regexParse, the keys are immutable
  77. function typeLookup($s, $getArray = false) {
  78. $types = array(
  79. "farenheit" => 0,
  80. "celcius" => 1,
  81. "humidity" => 2,
  82. "rain" => 3,
  83. "snow" => 4
  84. /* ... no changes to previous types and keys, add new types below */
  85. );
  86. if(!isset($types[$s])) return false;
  87. return $types[$s];
  88. }
  89. // save the stats from a scanned description into the database as events
  90. function saveEvents($db, $stats, $time) {
  91. foreach($stats as $plaintextType=>$value) {
  92. // convert canonical integers to SQL INTEGER
  93. if(ctype_digit($value)) $value = intval($value);
  94. // convert boolean to SQL INTEGER
  95. if(true === $value) $value = 1;
  96. if(false === $value) $value = 0;
  97. // other values should be integers
  98. if(!is_int($value)) {
  99. fwrite(STDERR, "[!] stat {$plaintextType} has value {$value}, not an integer" . PHP_EOL);
  100. continue;
  101. }
  102. // stat type must be valid, also prevents unwanted SQL
  103. if(false === ($type = typeLookup($plaintextType))) {
  104. fwrite(STDERR, "[!] stat type {$plaintextType} is not defined in events schema" . PHP_EOL);
  105. continue;
  106. }
  107. // and also make sure nothing unsafe is coming through time
  108. $start = intval($time);
  109. $end = $start;
  110. // make rain or snow occur over time, 8 hours minimum
  111. if("rain" === $plaintextType || "snow" === $plaintextType) $end += (8/*hours*/ * 60 /*minutes*/ * 60 /*seconds*/);
  112. // save record
  113. if(false === mysql_query("INSERT INTO events (start, end, type, value) VALUES ({$start},{$end},{$type},{$value})", $db)) {
  114. // event previously recorded
  115. } else /* event sucessfully stored */;
  116. }
  117. }
  118. /* sometimes users input time intervals without an end time:
  119. * I might record that it is raining, then go take a six hour nap, upon waking I
  120. * might notice that it is still raining and wait until it stops and then record
  121. * that it stops. This will probably never happen. More likely I will record that
  122. * it is raining and then never say anything again. There is a possibility that
  123. * when I wake up I might record again that it is raining. We just assume that rain
  124. * always lasts at least 8-hours and that whenever there are overlapping mentions
  125. * of rain the intervals are merged, eg: not summed:
  126. *
  127. * |----------- rain 8-hours ------------|
  128. * |----------- rain 8-hours ------------|
  129. *
  130. * |-------------------------- merged --------------------------|
  131. */
  132. function eightHourIntervalMerge($type, $start = false, $end = false) {
  133. if(false === ($rows = helperFunctionTemplate($type, $start, $end))) return false;
  134. $intervals = array();
  135. $start = false;
  136. // rain/snow are added in 8-hour intervals, merge overlapping intervals
  137. foreach($rows as $k=>$row) {
  138. // record the begining of the interval
  139. if(false === $start) $start = $row["start"];
  140. // goto next interval if current interval overlaps
  141. if(isset($rows[$k + 1]) and ($rows[$k + 1]["start"] <= $row["end"])) continue;
  142. // store the interval when there are no more overlaps
  143. $intervals[] = array("start"=>$start, "end"=>$row["end"]);
  144. $start = false;
  145. }
  146. return $intervals;
  147. }
  148. // returns array of ["time"=>{time}, "reading"=>{reading}] from events by type
  149. function getReadings($type, $start = false, $end = false) {
  150. if(false === ($rows = helperFunctionTemplate($type, $start, $end))) return false;
  151. $arr = array( /* [time => ..., reading => ...] */ );
  152. foreach($rows as $row) $arr[] = array("time"=>$row["start"], "reading"=>$row["value"]);
  153. return $arr;
  154. }
  155. // returns merged time intervals of rain, with optional time range (unix epoch)
  156. function getRainPeriods($start = false, $end = false) {
  157. return eightHourIntervalMerge("rain", $start = false, $end = false);
  158. }
  159. // returns merged time intervals of snow, with optional time range (unix epoch)
  160. function getSnowPeriods($start = false, $end = false) {
  161. return eightHourIntervalMerge("snow", $start = false, $end = false);
  162. }
  163. // returns farenheit readings, with optional time range (unix epoch)
  164. function getTemperatureData($start = false, $end = false) {
  165. return getReadings("farenheit", $start, $end);
  166. }
  167. // returns humidity percentage readings, with optional time range (unix epoch)
  168. function getHumidityData($start = false, $end = false) {
  169. return getReadings("humidity", $start, $end);
  170. }
  171. // merged time intervals of snow, sampled and filtered
  172. function getSnowData($start = false, $end = false) {
  173. return intervalsToSamples($start, $end, "snow");
  174. }
  175. // merged time intervals of rain, sampled and filtered
  176. function getRainData($start = false, $end = false) {
  177. return intervalsToSamples($start, $end, "rain");
  178. }
  179. // merged time intervals of rain/snow, sampled and filtered
  180. function intervalsToSamples($start = false, $end = false, $type = "rain") {
  181. // make a set of numbers, between zero and one to use as start and stop data
  182. $points = /* arbitrary number, consider HighCharts - more points, more load */ 8;
  183. $startStopLookup = generateSoftStart($points);
  184. // make up the amount of time for a "soft start"
  185. $softTime = /* fraction of "length of rain" in time */ strtotime("8 hours", 0) / (2 * $points);
  186. // make up the first data point where there is no rain (selected from database manually)
  187. $arr = array(array("time"=> /* SELECT MIN(time) FROM radonLog */ 1631631600, "reading" => 0));
  188. // remaining data points come from the "events" table
  189. $periods = eightHourIntervalMerge($type, $start = false, $end = false);
  190. // generate the graph, use soft start/stop data at beginging/end of intervals
  191. foreach($periods as $a) {
  192. // [?] $a["reading"] value is always one, soft starts are between zero and one
  193. // generate soft start values, separated by a made up amount of time
  194. $start = $a["start"];
  195. for($i = 0;$i<$points; $i++) {
  196. $arr[] = array("time"=>$start, "reading" => $startStopLookup[$i]);
  197. $start += $softTime;
  198. }
  199. // generate soft stop values, separated by a made up amount of time
  200. $stop = $a["end"] - ($points * $softTime);
  201. for($i = $points; $i--;) {
  202. $arr[] = array("time"=>$stop, "reading" => $startStopLookup[$i]);
  203. $stop += $softTime;
  204. }
  205. }
  206. return $arr;
  207. }
  208. /* generate a soft curve to use instead of a harsh edge when displaying data
  209. * that would normally have a jarring on/off transition, user optionally
  210. * provides the number of points used to represent the wave
  211. *
  212. * [?] user should use the returned values in reverse for a "soft-stop"
  213. *
  214. * | .-'``
  215. * | .`
  216. * | ,`
  217. * | * 180 degrees of a sinewave, offset by one
  218. * | ` and a phase shift of PI/2 gives a "soft-
  219. * | .` start" -- this opposes a "sharp" edge
  220. * | * of digital on/off, or a square wave
  221. * | ' (to make values between zero and one, scaled by half)
  222. * | .`
  223. * | ,'
  224. * |,.-'_____________________
  225. */
  226. function generateSoftStart($n = 6) {
  227. $arr = array();
  228. for($i = 0; $i < $n; $i++) $arr[] = (1+sin(($i*2*M_PI_2)/($n-1) - M_PI_2))/2;
  229. return $arr;
  230. }
  231. function helperFunctionTemplate($type, $start = false, $end = false) {
  232. # connect to mysql database radondb
  233. $db = mysql_connect("localhost", "root", "secom8703");
  234. mysql_select_db("radondb", $db);
  235. // prevent injection in time interval input
  236. $start = intval($start);
  237. $end = intval($end);
  238. $and = (/* php zero is false */ $start and $end) ?"start >= {$start} AND end <= {$end} AND" :"";
  239. // add optional time interval to query
  240. $sql = "SELECT start, end, value FROM `events` WHERE {$and}";
  241. // lookup type, make sure query sorts results oldest event to newest
  242. $sql .= "type = " . typeLookup($type) . " ORDER BY start ASC";
  243. if(false === ($result = mysql_query($sql, $db))) {
  244. fwrite(STDERR, "{$type} query failed:" . PHP_EOL . mysql_error($db));
  245. return false;
  246. }
  247. $rows = array();
  248. while($row = mysql_fetch_array($result, MYSQL_ASSOC)) {
  249. $rows[] = $row;
  250. //printRow($row);
  251. }
  252. mysql_free_result($result);
  253. # mysql needs clean-up
  254. mysql_close($db);
  255. return $rows;
  256. }
  257. //scanDescription();
  258. //var_dump(getRainPeriods());
  259. //var_dump(getSnowPeriods());
  260. //var_dump(getHumidityData());
  261. //var_dump(getTemperatureData());
  262. //var_dump(getRainData());
  263. // calling this without parameters will try to extract all data from all records in locationLog
  264. //scanDescription();
  265. /* ------ stuff for debugging ------ */
  266. // row contains ["time" => "...", "description" => "..."]
  267. function printRow($mySQL_assocRow) {
  268. $temp = "";
  269. foreach($mySQL_assocRow as $k=>$v) {
  270. echo "{$v} ";
  271. if("description" !== $k) continue;
  272. $temp = implode(" ", regexParse($v));
  273. }
  274. if("" !== $temp) echo "---- @{$mySQL_assocRow["time"]} {$temp}";
  275. echo PHP_EOL;
  276. }