daily pastebin goal
29%
SHARE
TWEET

IT - Weekly report 6

maciejzj Nov 6th, 2018 109 Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. # balloonS
  2.  
  3. A remote control and sensor data visualization for Raspberry Pi based high altitude balloon.
  4.  
  5. [TOC]
  6.  
  7. ## Introduction
  8. ### Brief description
  9. The system provides location, temperature, humidity, pressure and altitude logging with camera streaming and additional GPIO remote controls with a web server hosted on a mobile platform.
  10. ### Detailed description
  11. The goal of this project is to make and build a microsystem that will be then placed inside of capsule carried by a high altitude balloon. The system will be powered by Raspberry Pi and gather information about conditions in different parts of atmosphere. All the data will be presented and accessible via web server hosted by the Pi and backed up evenly.
  12.  
  13. The Internet Technologies part of this program is crucial because of three reasons:
  14.  
  15. 1. The balloon has to be recovered after landing, therefore the system is about to provide location for the landing zone, so the balloon can be easily found. This is achieved by three developements:
  16.  
  17.     * GPS logging and presenting it by Google maps on hosted website
  18.     * Providing a camera livestream
  19.     * Activating buzzer accessed via the hosted website
  20.    
  21. 1. The gathered data should be backed up and remotely accessible in case of the recovery being not viable. In this scenario even if the hardware is lost the data will be gained.
  22. 1. The gathered data should be presented in a readable form, easy to process and analyze. Therefore the hosted server ought to provide user-friendly interface with interactive charts and neatly formatted tables.
  23.  
  24. ## The aim and the scope of the project
  25. ### The initial status of the project during startup
  26. The project has been started before the beginning of the Internet Technologies subject. At the startup the system has fully functioning sensor hardware and contains:
  27.  
  28. * $I^2C$ based humidity and pressure sensors as well as RTC module
  29. * GPS connected via *UART*
  30. * Two thermometers connected via *1-wire*
  31. * Web server based on Apache2, with MySQL user data base
  32. * Webcam streaming video using motion
  33. * Functioning remote GPIO controls accessed by the web page
  34. * Web page presenting the data in a non interactive way, charts are images and logs are row data
  35. * Embedded Google Maps window with route markers
  36.  
  37. Since the sensors hardware and offline back-end (made in Python) are not the subject of Internet Technologies they will not be mentioned again. The progress of this part of the project, not related to web based services, can be tracked on GitHub.
  38.  
  39. ### The enhancements that will be implemented during the Internet Technologies project
  40. The aim of the enhancements is to make web page more interactive and provide easier way to analyze data. The logging system will also be upgraded. During Internet Technologies project we are about to:
  41.  
  42. 1. Create a system for registering new users. A person who will be in need of having an account will be able to register. It will cause sending an e-mail to the administrator who will have to confirm the registration.
  43. 1. Gather the sensor logs in MySQL database rather than inside text files, the tables presenting data should be nicely embedded on the web page.
  44. 1. Generate interactive log charts on client's side by a JavaScript library.
  45. 1. Make the site adaptive to mobile devices.
  46.  
  47. ## Schedule of work
  48. ### Planned schedule of work
  49.  
  50. |    date    | task number |                              planned task description                             |
  51. |:----------:|:-----------:|:---------------------------------------------------------------------------------:|
  52. | 2018-10-10 |      W1     | schedule and goals determination                                                  |
  53. | 2018-10-17 |      W2     | new user registration with admin e-mail confirmation                              |
  54. | 2018-10-24 |      W3     | storing data in both MySQL and text files                                         |
  55. | 2018-10-31 |      W4     | presenting the data in tables embedded on the website (data stored in MySQL only) |
  56. | 2018-11-07 |      W5     | interactive log charts on client's site                                           |
  57. | 2018-11-14 |      W6     | tables, charts CSS styling, desktop version polishing                             |
  58. | 2018-11-21 |      W7     | mobile adaptation - mobile version of the web page 1                              |
  59. | 2018-11-28 |      W8     | mobile adaptation - mobile version of the web page 2                              |
  60. | 2018-12-05 |      W9     | extra time (for unpredicted delays)                                                |
  61.  
  62. ### Schedule realization
  63. #### Week 1
  64. The introductory part of this document was created alongside with planned schedule.
  65. #### Week 2
  66. We managed to split the work what resulted in a rapid progress of work, some goals reaching W4 were achieved.
  67.  
  68. Now the logged data from all sensors is stored in sql database. The communication between database and python is achieved with the `mysql.connector` module. We will show how it is used on the example of pressure logging:
  69. ```python
  70. degrees = sensor.read_temperature()
  71. pascals = sensor.read_pressure()
  72. hectopascals = pascals / 100
  73.  
  74. mydb = mysql.connector.connect(
  75.     host = "localhost",
  76.     user = "root",
  77.     passwd = "balloonSroot",
  78.     database = "balloonS"
  79. )
  80.  
  81. mycursor = mydb.cursor()
  82. sql = "INSERT INTO press_log(log_time, log_val, unit) VALUES (%s, %s, %s)"
  83. val = (time_stamp, hectopascals, "hPa")
  84. mycursor.execute(sql, val)
  85. mydb.commit()
  86. ```
  87. The crucial parts of communication are `connect()` and `execute()` functions. First of them is rather self-explanatory, the second works like `printf`, it creates query based on `sql` string with values inserted from `val` array.
  88.  
  89. To implement this kind of logging temperature reading should be achieved via python script instead using bash with sed. Here is the new temperature reading script:
  90.  
  91. ```python
  92. #!/usr/bin/python
  93. import sys
  94. import re
  95. import mysql.connector
  96. import datetime as dt
  97.    
  98. therm_dev_name = {'ext' : "28-000005945f57", 'int' : "28-00000a418b77"}
  99. therm_addr = {'ext' : "/sys/bus/w1/devices/" + therm_dev_name['ext'] + "/w1_slave",
  100.                 "int" : "/sys/bus/w1/devices/" + therm_dev_name['int'] + "/w1_slave"
  101.                 }
  102. therm_log_name = {'ext': "temp_log", 'int': "int_temp_log"}
  103.  
  104. mydb = mysql.connector.connect(
  105.     host = "localhost",
  106.     user = "root",
  107.     passwd = "balloonSroot",
  108.     database = "balloonS"
  109. )
  110.  
  111. for key in therm_addr.viewkeys() & therm_log_name.viewkeys():
  112.     with open(therm_addr[key], mode = "r") as therm:   
  113.         lines = therm.readlines()
  114.         for line in lines:
  115.             match = re.search(r'(?<=t=)[0-9]*', line)
  116.             if(match):
  117.                 temp = float(match.group()) / 1000
  118.  
  119.     time_stamp = dt.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
  120.        
  121.     mycursor = mydb.cursor()
  122.     sql = "INSERT INTO " + therm_log_name[key] + "(log_time, log_val, unit) VALUES (%s, %s, %s)"
  123.     val = (time_stamp, temp, "C")
  124.     mycursor.execute(sql, val)
  125.     mydb.commit()
  126. ```
  127. The parametrisation of thermometers addresses is preserved using dictionaries, so in the future they will be placed in seprate files. Those files will be easily edited by user to allow him to enter the address of his unique thermometer.
  128.  
  129. To make the setup easier and eventually fully automated the database creation is achieved by sql script:
  130.  
  131. ```sql
  132. DROP USER 'root'@'localhost';
  133. CREATE USER 'root'@'localhost' IDENTIFIED BY 'balloonSroot';
  134. GRANT ALL PRIVILEGES ON *.* TO 'root'@'localhost'
  135.  
  136. CREATE DATABASE balloonS_users;
  137. USE balloonS_users;
  138.  
  139. CREATE TABLE users(id int, login varchar(25), pass varchar(25));
  140.  
  141. CREATE DATABASE balloonS;
  142. USE balloonS;
  143.  
  144. CREATE TABLE press_log(num int NOT NULL AUTO_INCREMENT, log_time timestamp, log_val real, unit varchar(6), PRIMARY KEY (num));
  145.  
  146. CREATE TABLE alt_log(num int NOT NULL AUTO_INCREMENT, log_time timestamp, log_val real, unit varchar(6), PRIMARY KEY (num));
  147.  
  148. CREATE TABLE temp_log(num int NOT NULL AUTO_INCREMENT, log_time timestamp, log_val real, unit varchar(6), PRIMARY KEY (num));
  149.  
  150. CREATE TABLE int_temp_log(num int NOT NULL AUTO_INCREMENT, log_time timestamp, log_val real, unit varchar(6), PRIMARY KEY (num));
  151.  
  152. CREATE TABLE hum_log(num int NOT NULL AUTO_INCREMENT, log_time timestamp, log_val real, unit varchar(6), PRIMARY KEY (num));
  153.  
  154. CREATE TABLE loc_log(num int NOT NULL AUTO_INCREMENT, log_time timestamp, latitude real, longitude real, velocity real, course real, PRIMARY KEY (num));
  155. ```
  156. In the future the password should be treated as variable and entered by user.
  157.  
  158. The whole system of logging data works, which can be checked by selecting everything from correct tables. Here is the example of humidity log:
  159. ```
  160. MariaDB [balloonS]> select * from hum_log;
  161. +------+---------------------+---------+------+
  162. | num  | log_time            | log_val | unit |
  163. +------+---------------------+---------+------+
  164. |    1 | 2018-10-15 20:29:02 |    61.5 | hPa  |
  165. |    2 | 2018-10-15 20:30:01 |    61.7 | hPa  |
  166. |    3 | 2018-10-15 20:31:03 |    61.7 | hPa  |
  167. |    4 | 2018-10-15 20:32:01 |    61.8 | hPa  |
  168. |    5 | 2018-10-15 20:33:02 |    61.9 | hPa  |
  169. |    6 | 2018-10-15 20:34:01 |    61.9 | hPa  |
  170. |    7 | 2018-10-15 20:35:02 |    61.8 | hPa  |
  171. |    8 | 2018-10-15 20:36:01 |    61.6 | hPa  |
  172. ```
  173. Now the data should be gained by php when `main_panel.php` is opened and placed in html tables. Since we have to create a few of these tables the way of achieving this should be universal. Therefore we should create a function that can create any of these tables given the sql table name. The function looks like this:
  174. ```php
  175. <?php
  176. function makeTable($connection, $tableName) {
  177.     if ($connection->connect_errno != 0) {
  178.             echo "Error: ".$connection->connect_errno;
  179.     } else {
  180.         try {
  181.             $query = "SELECT * FROM " . $tableName;
  182.             print "<table>";
  183.             $result = $connection->query($query);
  184.             // We want the first row for col names
  185.             $row = $result->fetch_assoc();
  186.             print " <tr>";
  187.             foreach ($row as $field => $value){
  188.                 print " <th>$field</th>";
  189.             }
  190.             print " </tr>";
  191.  
  192.             // Print actual data
  193.             foreach($result as $row){
  194.                 print " <tr>";
  195.                 foreach ($row as $name=>$value){
  196.                     print " <td>$value</td>";
  197.                 }
  198.                 print " </tr>";
  199.             }
  200.             print "</table>";
  201.         } catch(PDOException $e) {
  202.          echo 'ERROR: ' . $e->getMessage();
  203.         }
  204.     }
  205. }
  206. ?>
  207. ```
  208. It is stored in a separate file to maintain order and clearance. The connection to the database is established in main file:
  209. ```php
  210. <?php
  211. require_once "connect.php";
  212. require_once "make_log_table.php";
  213. $connection = @new mysqli($host, $db_user, $db_password, $db_name_logs);
  214. $tableLogNames = array(
  215.     "temperature" => "temp_log",
  216.     "internal_temperature" => "int_temp_log",
  217.     "pressure" => "press_log",
  218.     "altitude" => "alt_log",
  219.     "humidity" => "hum_log",
  220.     "location" => "loc_log",
  221. );
  222. ?>
  223. ```
  224. The connection data is also stored in separate file `connect.php`. This is how the function is called inside the main file:
  225. ```php
  226. <div class="log_table">
  227.     <?php
  228.         makeTable($connection, $tableLogNames["location"]);
  229.     ?>
  230. </div>
  231. ```
  232. To make coding more convinient the names of the tables in the sql database are stored in a dictionary that is accessible to the person reading the main file.
  233.  
  234. Finally we can check the results of the work. The creation works for all of the logs' tables, regardless of their dimensions. Here is an example of the temperature log:
  235. ![html_table](https://i.imgur.com/wOYeaGC.jpg)
  236.  
  237. The registration system is under construction, it now features passwords hashing, however the email confirmation is still to be finished. This is due to the time consuming setup process on second computer, which had to be done before starting the main part od the work.
  238.  
  239. #### Week 3
  240. During previous week we managed to get the log data from the SQL database. Now we have to pass it to javascript to make a chart from it. In the first step we decided to separate extraction of the data from drawing the HTML table. In order to do so the functions were modified in this way:
  241. ```php
  242. <?php
  243. function makeTable($connection, $tableName) {
  244.     if ($connection->connect_errno != 0) {
  245.             echo "Error: ".$connection->connect_errno;
  246.     } else {
  247.         try {
  248.             $query = "SELECT * FROM " . $tableName;
  249.             $html_table_log = "<table>";
  250.             $result = $connection->query($query);
  251.             // We want the first row for col names
  252.             $row = $result->fetch_assoc();
  253.             $html_table_log .= " <tr>";
  254.             foreach ($row as $field => $value){
  255.                 $html_table_log .= " <th>$field</th>";
  256.             }
  257.             $html_table_log .= " </tr>";
  258.  
  259.             // Print actual data
  260.             foreach($result as $row){
  261.                 $html_table_log .= " <tr>";
  262.                 $array_table_log[] = $row;
  263.                 foreach ($row as $name=>$value){
  264.                     $html_table_log .= " <td>$value</td>";
  265.                 }
  266.                 $html_table_log .= " </tr>";
  267.             }
  268.             $html_table_log .= "</table>";
  269.             return array($html_table_log, $array_table_log);
  270.         } catch(PDOException $e) {
  271.          echo 'ERROR: ' . $e->getMessage();
  272.         }
  273.     }
  274. }
  275. ?>
  276. ```
  277. As you can see, now we are creating a `string $html_table_log` variable that holds the HTML table text. This string is then returned. The function call now looks like this:
  278. ```php
  279. <?php
  280. foreach ($db_table_names as $log_name => $db_table_name){
  281.     list($html_table_log, $array_table_log) = makeTable($connection, $db_table_name);
  282.    
  283.     $html_table_logs[$log_name] = $html_table_log;
  284.     $array_table_logs[$log_name] = $array_table_log;
  285. }
  286. ?>
  287. ```
  288. And then we can deploy the table by simply calling:
  289. `<?php print($html_table_logs["temperature"]); ?>`.
  290.  
  291. You have probably noticed that alongside the creation of the HTML table the function also prepares and returns a php array containing the data extracted from SQL database table. Notice how the function returns two values by passing an array. Now we will focus on passing the returned array of data to javascript. It is achieved by using JSON encoding:
  292. ```javasript
  293. <script type="text/javascript">
  294.     var temp_log = <?php echo json_encode($array_table_logs["temperature"], JSON_PRETTY_PRINT) ?>;
  295.     var int_temp_log = <?php echo json_encode($array_table_logs["internal_temperature"], JSON_PRETTY_PRINT) ?>;
  296.     var loc_log = <?php echo json_encode($array_table_logs["location"], JSON_PRETTY_PRINT) ?>;
  297.     var press_log = <?php echo json_encode($array_table_logs["pressure"], JSON_PRETTY_PRINT) ?>;
  298.     var alt_log = <?php echo json_encode($array_table_logs["altitude"], JSON_PRETTY_PRINT) ?>;
  299.     var hum_log = <?php echo json_encode($array_table_logs["humidity"], JSON_PRETTY_PRINT) ?>;
  300. </script>
  301. ```
  302. We decided not to do this by `for` statement, since it would require simultaneous looping through two languages with a shared key.
  303.  
  304. Now the javascript has all the data, but we are still not ready to plot it. We have to decode the JSON objects and extract proper fields from it. For the clearance we will do all od this in separate file and dedicated functions.
  305.  
  306. The crucial function for out task is:
  307. ```javascript
  308. function makeChart(chartID, chartLabel, log_data) {
  309.   [xData, yData] = extractData(log_data);
  310.   drawChart(chartID, chartLabel, xData, yData);
  311. }
  312. ```
  313. It contains calls to two other functions: data extracting and chart drawing. Let\`s look at `extractData()`:
  314. ```jsavascript
  315. function extractData(data) {
  316.   var dataArray = [];
  317.   var timeArray = [];
  318.  
  319.  
  320.   for (var i = 0; i < data.length; i++) {
  321.     dataArray.push(JSON.parse(data[i].log_val));
  322.     timeArray.push(JSON.stringify(data[i].log_time).replace(/['"]+/g, ''));
  323.   }
  324.  
  325.   return [timeArray, dataArray];
  326. }
  327. ```
  328. It gets the JSON object and loops thorough it extracting timestamps and log data. Then both of them are returned.
  329.  
  330. Now we can call the chart drawing function:
  331. ```javascript
  332. unction drawChart(chartID, chartLabel, xData, yData) {
  333.  
  334.   const myChart = document.getElementById(chartID).getContext('2d');
  335.  
  336.   let chart1 = new Chart(myChart, {
  337.     type:'line', //bar, horizontalBar, pie, line, doughnut, radar, polarArea
  338.     data:{
  339.       labels:xData,
  340.       datasets:[
  341.         {
  342.         label: chartLabel,
  343.         fill:false,
  344.         borderColor:'428bca',
  345.         lineTension:0.2,
  346.         data: yData}
  347.       ],
  348.     },
  349.     options:{
  350.       title:{
  351.         display:true,
  352.         text:(chartLabel + " log"),
  353.         fontSize:25,
  354.         fontColor:'#000'
  355.       },
  356.       elements: {
  357.         point:{
  358.           radius: 0
  359.         }
  360.       },
  361.       legend:{
  362.         display:true,
  363.         position:'top'
  364.       },
  365.       layout:{
  366.         padding:{
  367.           left:50,
  368.           right:50,
  369.           bottom:50,
  370.           top:50
  371.         }
  372.       }
  373.     }
  374.   });
  375. }
  376. ```
  377. And feed it with the data kept in the arrays. To plot the data versus time we should keep in mind correct date string format, which is `YYYY-MM-DD HH:MM:SS`.
  378.  
  379. Finally we can call the `makeChart()` function in our main panel:
  380. ```
  381. <canvas id="hum_log_chart"></canvas>
  382.                 <script>makeChart("hum_log_chart", "Humidity", hum_log)</script>
  383. ```
  384. If we keep the correct naming which is ensured by the associative array at the beginning of the file we can reuse this function for every chart we want to draw.
  385.  
  386. Here\`s the generated chart:
  387. ![chart_example](https://i.imgur.com/tCvHGVi.png)
  388.  
  389. Another feature we can now call finished is registration system. Inquisitive people can fill a form on the site, then administrator will receive an e-mail informing him about the request.
  390. Here is how the registration form looks right now:
  391. ![alt text](https://i.imgur.com/BNeCa8d.png "registration_system_look")
  392. The small question marks next to "Nickname:" and "Password:" show requirements that have to be fulfilled after being hovered. The `.tooltip` class that provides showing that tooltips is made in CSS language.
  393. ```css
  394. .tooltip
  395. {
  396.     position: relative;
  397.     z-index: 20;
  398. }
  399.  
  400. .tooltip span
  401. {
  402.     display: none;
  403. }
  404.  
  405. .tooltip:hover
  406. {
  407.     z-index: 21;
  408. }
  409.  
  410. .tooltip:hover span
  411. {
  412.     display: block;
  413.     width: 290px;
  414.     padding: 5px;
  415.     color: #FFF;
  416.     background: #535663;
  417.     text-decoration: none;
  418.     position: absolute;
  419.     border-radius: 6px;
  420.     margin-left: auto;
  421.     left: auto;
  422.     top: 25px;
  423. }
  424. ```
  425. And used in php file:
  426. ```php
  427. <a href="#" class="tooltip"><img style="float:right" src="./data/img/question_mark.png" width="6%" height="6%"><span>3-24 characters long, only alphanumeric characters, username has to be unique</span></a>
  428. ```
  429.  
  430. Valid form checks:
  431. * length of nickname
  432. * if nickname is unique
  433. * if nickname has only alphanumeric characters
  434. * length of password
  435. * if password has at least one digit, one lowercase and one uppercase
  436. * compatibility of password typed two times
  437. * e-mail format
  438. * if e-mail is unique
  439. * if the checkbox with Terms of Use acceptation was marked
  440. * if the user is a human (Google reCAPTCHA)
  441.  
  442. If the requirements are not fulfilled the info about it is being displayed under form inputs:
  443. ![text alt](https://i.imgur.com/oHPUhd7.png "registration_system_look2")
  444. Nickname and e-mail address, when input correctly, are stored in `$_SESSION` variable so if new user does not fulfill correctly rest of the requirements, he does not have to type them again.
  445. ```php
  446. <input type="text" name="nickname" placeholder="nickname" onfocus="this.placeholder=''" onblur="this.placeholder='nickname'" value="<?php echo $_SESSION['nickname']?>"/>
  447. ```
  448.  
  449. Every new user needs to be confirmed by one of administrators. It's provided by sending an activation link.
  450. ```php
  451. <?php
  452. $message = 'Confirm that $nickname with e-mail adress: $email is allowed to join.
  453.             Click the link:
  454.             http://localhost/email_confirmation.php?username='.$nickname.'&code='.$auth_code;
  455.             mail($admin_email,"$nickname email confimation",$message,"From: DoNotReply@TheBalloonS.com");
  456. ?>
  457. ```
  458.  
  459. Activation link changes confirmation flag stored in database from `0` to `1` with query `UPDATE users SET confirmed='1' WHERE login='$nickname'`. Then site sends an e-mail to new user in which he/she is being informed about acceptance of his/her request.
  460.  
  461. #### Week 4
  462. During this week we focused on three goals. The first of them was to apply the new way of making charts to all of the logs. Since the way of creating them is as universal as possible nearly all of them required only calling a function with a proprer argument. Hovever we decided to draw both external and internal temperature logs on the same graph for easier comparison. This required some fairly straightforward modifications to the code. We also added some styling to the graphs. Here are the results:
  463. ![Imgur](https://i.imgur.com/lcgPQrB.png)
  464. ![Imgur](https://i.imgur.com/msj6HXR.png)
  465. Second thing was to take care of location logging, it was still done in the old way, passing text files to feed Goolge Maps data. At the sime time it was prone to erros, the loss of GPS singal resulted in a record that later was to broke Google Maps completely. To address this problems we implemented checking correct format of logged loaction data, and cared more about singal location, communication errors and data corruption. Since the code is rather long we will not post it here, you can visit our GitHub to check it out: [GPS class source code](https://github.com/MaciejZj/balloonS/blob/master/gps.py). Then we had to feed Google Maps data from SQL database instead of text file, it did it in similar way as before, with a slightly modified code.
  466. ```javascript
  467. function extractLocation(data) {
  468.     var timeArray = [];
  469.     var latArray = [];
  470.     var lngArray = [];
  471.    
  472.     for (var i = 0; i < data.length; i++) {
  473.         if(JSON.stringify(data[i].status).replace(/['"]+/g, '') == 'Correct') {
  474.             timeArray.push(JSON.stringify(data[i].log_time).replace(/['"]+/g, ''));
  475.             latArray.push(JSON.parse(data[i].latitude));
  476.             lngArray.push(JSON.parse(data[i].longitude));
  477.         }
  478.     }
  479.     return [timeArray, latArray, lngArray];
  480. }
  481.  
  482. function makeGoogleMaps(locLogData) {
  483.     [timeData, latData, lngData] = extractLocation(locLogData);
  484.     initialize(timeData, latData, lngData);
  485. }
  486.  
  487. function initialize(timeData, latData, lngData) {
  488.     var mapLat = latData[latData.length - 1]
  489.     var mapLng = lngData[lngData.length - 1]
  490.  
  491.     var mapOptions = {
  492.         zoom: 15,
  493.         center: {lat: mapLat, lng: mapLng}
  494.     };
  495.     map = new google.maps.Map(document.getElementById('map'),
  496.             mapOptions);
  497.    
  498.     for (i = 0; i < latData.length; i++) {
  499.         var markerLat = latData[i];
  500.         var markerLng = lngData[i];
  501.        
  502.         var marker = new google.maps.Marker({
  503.             position: {lat: markerLat, lng: markerLng},
  504.             map: map
  505.         });
  506.  
  507.         var content = "<b>" + timeData[i] + "</b>" + " (" + markerLat + ", " + markerLng + ")";
  508.         var infowindow = new google.maps.InfoWindow()
  509.         google.maps.event.addListener(marker,'click', (function(marker,content,infowindow){
  510.             return function() {
  511.                 infowindow.setContent(content);
  512.                 infowindow.open(map,marker);
  513.             };
  514.         })(marker,content,infowindow));  
  515.     }
  516.     google.maps.event.trigger(map, 'resize');
  517. }
  518. ```
  519. Now all logs are supplied by the SQL database and we can take care of their design. We focused on the log tables, the main goal was to make their headers fixed. To achieve this quickly we decided to use **less** framework for css. It uses styles.less file and is included using:
  520. ```html
  521. <link rel="stylesheet/less" type="text/css" href="styles.less">
  522. <script src="//cdnjs.cloudflare.com/ajax/libs/less.js/3.7.1/less.min.js" ></script>
  523. ```
  524. With this framework fixed table header and styling can be achieved in this way (example of location log table):
  525. ```css
  526. .loc_log {
  527.     width: 900px;
  528.     table-layout: fixed;
  529.     border-collapse: collapse;
  530.  
  531.     th, td {
  532.         padding: 5px;
  533.         text-align: center;
  534.         width: 100%;
  535.     }
  536.    
  537.     td:nth-child(1), th:nth-child(1) { min-width: 20; }
  538.     td:nth-child(2), th:nth-child(2) { min-width: 150; }
  539.     td:nth-child(3), th:nth-child(3) { min-width: 150; }
  540.     td:nth-child(4), th:nth-child(4) { min-width: 100; }
  541.     td:nth-child(5), th:nth-child(5) { min-width: 100; }
  542.     td:nth-child(6), th:nth-child(6) { min-width: 100; }
  543.     td:nth-child(7), th:nth-child(7) { min-width: 80; }
  544.  
  545.     thead {
  546.         background-color: #6C7DDD;
  547.         color: #FDFDFD;
  548.         tr {
  549.             display: block;
  550.             position: relative;
  551.         }
  552.     }
  553.     tbody {
  554.         display: block;
  555.         overflow: auto;
  556.         width: 100%;
  557.         height: 300px;
  558.         tr:nth-child(even) {
  559.             background-color: #DDD;
  560.         }
  561.     }
  562. }
  563.  
  564. ```
  565. with wrapper:
  566. ```css
  567.     margin: 10px;
  568.     border-radius: 8px;
  569.     width: 900px;
  570.     height: 328px;
  571.     -webkit-box-shadow: 0px 10px 41px -11px rgba(0,0,0,0.68);
  572.     -moz-box-shadow: 0px 10px 41px -11px rgba(0,0,0,0.68);
  573.     box-shadow: 0px 10px 41px -11px rgba(0,0,0,0.68);
  574.  
  575. ```
  576. It is curucial to bear in mind that wrapper height must be exactly mathing height of the table. If not so the fixed header will break. The results are presented below, note that the record with 'Signal lost' status doesn't break down Google Maps panel.
  577. ![Imgur](https://i.imgur.com/2V57znY.png)
RAW Paste Data
We use cookies for various purposes including analytics. By continuing to use Pastebin, you agree to our use of cookies as described in the Cookies Policy. OK, I Understand
 
Top