Raspberry Piのセンサー情報をJavaでグラフ化し、Webブラウザーで確認する

#はじめに
Raspberry Pi 3 に接続した温度・湿度センサー、および気圧センサーの情報を継続してデータベースに記録し、それをグラフ化して他の PC やスマホの Web ブラウザー上に表示させてみました。プログラミング言語は Java を使用しています。

![History Graph](https://qiita-image-store.s3.amazonaws.com/0/242156/0c9b5a6d-acf7-1ce9-f934-292091d8b726.png)

温度・湿度センサーおよび気圧センサーは、DHT11、BMP180を使用しました。

|DHT11<br>（温度・湿度センサー）|BMP180<br>（気圧・温度センサー）|
|:-:|:-:|
|![DHT11](https://qiita-image-store.s3.amazonaws.com/0/242156/d2e1b723-c198-67db-aa91-a0aa54846af9.jpeg)|![BMP180](https://qiita-image-store.s3.amazonaws.com/0/242156/0278c079-1650-ea5d-f958-ede8630dd0e9.jpeg)|


#準備
ハードウェア、およびソフトウェア環境は、以下の記事で紹介しているものを使用しています。

> [Raspberry Piのセンサー情報をグラフ化し、Webブラウザーで確認できる環境を準備する](https://qiita.com/uchino-tama/items/bdc5e546822decd9bbef)

環境の準備が整ったら、以下の配線図のようにセンサーを取り付けます。抵抗はデータシート上では 5KΩ が指定されていましたが、手持ちになかったので 10KΩ で代用ました（特に問題はないようです）。

![配線図](https://qiita-image-store.s3.amazonaws.com/0/242156/a29c7d5d-a439-f5c5-e349-5a8977e1604c.png)

（参考資料）[DHT11データシート](http://osoyoo.com/driver/DHT11-datasheet.pdf) ／ [BMP180データシート](https://cdn-shop.adafruit.com/datasheets/BST-BMP180-DS000-09.pdf)

#データベースの作成
以下の操作は、すべて pi ユーザーから実施しています。

まず始めに、温湿度および気圧のデータを記録していくためのデータベースを作成します。データベースは上のリンク先の記事でインストールした MySQL を使用しています。

以下の例ではデータベース名として raspberrypi を指定していますが、任意の名前を指定することができます（出力の一部は省略しています）。

```
$ mysql -p
Enter password:

MariaDB [(none)]> CREATE DATABASE raspberrypi CHARACTER SET utf8;
MariaDB [(none)]> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
| raspberrypi        |
+--------------------+
```

データベースが作成されたら、その中にテーブルを作成します。以下の例ではテーブル atmosphere を作成し、カラムとして id、datetime（日時）、temperature（温度）、humidity（湿度）、pressure（気圧）を作成しています。こちらも分かりやすい任意の名前を指定することができます。

```
MariaDB [(none)]> use raspberrypi
MariaDB [raspberrypi]> CREATE TABLE atmosphere( id INT(10) NOT NULL AUTO_INCREMENT PRIMARY KEY, datetime DATETIME NOT NULL, temperature FLOAT(4,1), humidity FLOAT(4,1), pressure FLOAT(5,1) );

MariaDB [raspberrypi]> show tables;
+-----------------------+
| Tables_in_raspberrypi |
+-----------------------+
| atmosphere            |
+-----------------------+

MariaDB [raspberrypi]> desc atmosphere;
+-------------+------------+------+-----+---------+----------------+
| Field       | Type       | Null | Key | Default | Extra          |
+-------------+------------+------+-----+---------+----------------+
| id          | int(10)    | NO   | PRI | NULL    | auto_increment |
| datetime    | datetime   | NO   |     | NULL    |                |
| temperature | float(4,1) | YES  |     | NULL    |                |
| humidity    | float(4,1) | YES  |     | NULL    |                |
| pressure    | float(5,1) | YES  |     | NULL    |                |
+-------------+------------+------+-----+---------+----------------+

MariaDB [raspberrypi]> exit
```

#データの記録
Tomcat の webapps ディレクトリ配下に、データを記録するためのプログラムや、サーブレットを配置するためのディレクトリを作成します。

```
mkdir -p ~/apache-tomcat-9.0.6/webapps/atmosphere/WEB-INF/classes
```

センサーから情報を読み取り、データベースに記録していくための以下のソースコードを、この classes ディレクトリーに作成します。

* RecordingTask.java
* DHT11.java
* DHT11Result.java
* BMP180.java

4 つのファイルのうち下の 3 つのファイルは、各センサーから情報を読み取るためのものです。これらについては、それぞれ以下の紹介記事からコピーして下さい。

**DHT11.java / DHT11Result.java**
[Raspberry Pi 3 & DHT11からJavaで温度・湿度を読み取る](https://qiita.com/uchino-tama/items/d729855d34dfbb491957)

**BMP180.java**
[Raspberry Pi 3 & BMP180からJavaで気圧・温度を読み取る](https://qiita.com/uchino-tama/items/efccfe871ab3abc782f3)</dd>

```java:RecordingTask.java
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.Files;
import java.nio.charset.StandardCharsets;
import java.text.MessageFormat;
import java.util.Properties;

import java.sql.DriverManager;
import java.sql.Connection;
import java.sql.Statement;
import java.sql.SQLException;

public class RecordingTask {

    private static String sqlDataBase      ;
    private static String sqlTable         ;
    private static String sqlColDateTime   ;
    private static String sqlColTemperature;
    private static String sqlColHumidity   ;
    private static String sqlColPressure   ;

    private static String sqlUser    ;
    private static String sqlPassword;

    // {0} table name
    // {1} column name(datetime)
    // {2} column name(temperature)
    // {3} column name(humidity)
    // {4} column name(pressuer)
    // {5} temperature
    // {6} humidity
    // {7} puressure
    private static final String sqlTemplate =
        "INSERT INTO {0} ( {1}, {2}, {3}, {4} ) VALUES ( NOW(), {5,number,#0.0}, {6,number,#0.0}, {7,number,#0.00} );";

    public static void main( String... args ) {

        getProperties();

        Connection conn = null;
        String url = "jdbc:mysql://localhost/" + sqlDataBase;

        try {
            Class.forName( "com.mysql.jdbc.Driver" ).newInstance();
            conn = DriverManager.getConnection( url, sqlUser, sqlPassword );

            Statement stmt = conn.createStatement();

            // DHT11 ---

            DHT11 dht11 = new DHT11( 15 ); // use GPIO pin 15

            DHT11Result result;
            while ( true ) {
                result = dht11.read();

                if ( result.isValid() ) {
                    // System.out.printf("Temperature: %.1f C\n" , result.temperature );
                    // System.out.printf("Humidity:    %.1f %%\n", result.humidity);
                    break;
                }
            }

            // BMP180 ---

            BMP180 bmp180 = new BMP180( BMP180.ULTRAHIGHRES );

            double pressure = bmp180.readPressure();
            // System.out.printf( "Pressure   : %.2f hPa\n", pressure );

            // write data to database
            String sql = MessageFormat.format( sqlTemplate,
                             sqlTable, sqlColDateTime, sqlColTemperature, sqlColHumidity, sqlColPressure,
                             result.temperature, result.humidity, pressure );
            // System.out.println( sql );

            int num = stmt.executeUpdate( sql );

            stmt.close();
        } catch ( ClassNotFoundException e ) {
            e.printStackTrace();
        } catch ( SQLException e ) {
            e.printStackTrace();
        } catch ( Exception e ) {
            e.printStackTrace();
        } finally {
            try {
                if ( conn != null ) {
                    conn.close();
                }
            } catch ( SQLException e ) {
                e.printStackTrace();
            }
        }
    }

    static void getProperties() {
        Path path = Paths.get( "atmosphere.properties" );
        try ( BufferedReader reader = Files.newBufferedReader( path, StandardCharsets.UTF_8 ) ) {
            Properties properties = new Properties();
            properties.load( reader );

            sqlDataBase       = properties.getProperty( "mysql.db" );
            sqlTable          = properties.getProperty( "mysql.db.table" );
            sqlColDateTime    = properties.getProperty( "mysql.db.table.datetime" );
            sqlColTemperature = properties.getProperty( "mysql.db.table.temperature" );
            sqlColHumidity    = properties.getProperty( "mysql.db.table.humidity" );
            sqlColPressure    = properties.getProperty( "mysql.db.table.puressure" );
            sqlUser           = properties.getProperty( "mysql.user" );
            sqlPassword       = properties.getProperty( "mysql.password" );
        } catch ( IOException e ) {
            e.printStackTrace();
        }
    }
}
```

データベースへ書き込む値の指定は、以下の部分で行っています。他のセンサーを使用する場合は、読み取った値を result.temperature、result.humidity、pressure の部分に double 型で指定すれば OK です。

```java
            String sql = MessageFormat.format( sqlTemplate,
                             sqlTable, sqlColDateTime, sqlColTemperature, sqlColHumidity, sqlColPressure,
                             result.temperature, result.humidity, pressure );
```


また、同じ場所にプロパティファイル atmosphere.properties を作成しておきます。このファイルには、作成したデータベース、テーブル、およびカラムに指定した名前を登録します。あわせて MySQL にアクセスするためのユーザーおよびパスワードも指定しておきます。

```java:atmosphere.properties
mysql.db=raspberrypi
mysql.db.table=atmosphere
mysql.db.table.datetime=datetime
mysql.db.table.temperature=temperature
mysql.db.table.humidity=humidity
mysql.db.table.puressure=pressure
mysql.user=pi
mysql.password=raspberry
jfreechart.chartfile=/home/pi/apache-tomcat-9.0.6/webapps/atmosphere/atmosphere.png
```

最後の jfreechart.chartfile= は、サーブレットが作成するグラフイメージを置く場所とファイル名を指定しています。

ここまで準備ができたら、ソースコードをコンパイルして実行します。

```
$ cd ~/apache-tomcat-9.0.6/webapps/atmosphere/WEB-INF/classes
$ javac RecordingTask.java
$ sudo java -classpath $CLASSPATH RecordingTask
```

プログラムの実行後、センサーから取得された情報がデータベースに書き込まれていることを確認します。

```
$ mysql -p
Enter password:

MariaDB [(none)]> use raspberrypi

MariaDB [raspberrypi]> select * from atmosphere;
+----+---------------------+-------------+----------+----------+
| id | datetime            | temperature | humidity | pressure |
+----+---------------------+-------------+----------+----------+
|  1 | 2018-04-07 19:26:15 |        24.2 |     50.0 |    994.4 |
+----+---------------------+-------------+----------+----------+

MariaDB [raspberrypi]> exit
```

なおテストで作成したレコードを削除したい場合や、テーブル、データベースそのものを削除したい場合は、以下のSQL文を使用します。削除は特に確認もなくサクッと削除されますので注意して下さい。

|機能|SQL文|
|:--|:--|
|テーブル内のレコードをすべて削除|USE database_name<br>TRUNCATE TABLE table_name;|
|テーブルの削除|USE database_name<br>DROP TABLE table_name;|
|データベースの削除|DROP DATABASE database_name;|

cron から定期的にこのプログラムを実行して、センサー情報をデータベースに蓄積します。ディレクトリ ~/apache-tomcat-9.0.6/webapps/atmosphere に以下のシェルスクリプト atmosphere.sh を作成し、アクセス権を設定しておきます。

```bash:atmosphere.sh
#!/bin/sh

CATALINA_HOME="/home/pi/apache-tomcat-9.0.6"
export CLASSPATH=.:/opt/pi4j/lib/*:$CATALINA_HOME/lib/*

cd $CATALINA_HOME/webapps/atmosphere/WEB-INF/classes
sudo java -classpath $CLASSPATH RecordingTask
```

```
$ cd ~/apache-tomcat-9.0.6/webapps/atmosphere
$ chmod 755 atmosphere.sh
```

このシェルスクリプトを crontab で設定すれば準備完了です。crontab を最初に起動した場合使用するエディタを聞かれますが、特に理由がない限り nano がお勧めです。

```
$ crontab -e
no crontab for pi - using an empty one

Select an editor.  To change later, run 'select-editor'.
  1. /bin/ed
  2. /bin/nano        <---- easiest
  3. /usr/bin/vim.tiny

Choose 1-3 [2]: 2
```

エディタで開かれたファイルの最後に以下を追加します。この設定は毎時 0 分にシェルスクリプトを起動します。今回作成したプログラムは、毎時 0 分にデータが記録される前提で作成されています。

```
0 * * * * /bin/bash /home/pi/apache-tomcat-9.0.6/webapps/atmosphere/atmosphere.sh
```

設定した内容は `crontab -l` で確認できます。

#グラフの作成
再び classes ディレクトリに戻って、データベースからデータを読み込み、グラフイメージを作成するサーブレット AtmosphereChart.java を作成します。

```java:AtmosphereChart.java
import java.io.File;
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.Files;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Properties;
import java.text.MessageFormat;
import java.awt.BasicStroke;
import java.awt.Font;
import java.awt.Color;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.RequestDispatcher;

import java.sql.DriverManager;
import java.sql.Connection;
import java.sql.Statement;
import java.sql.ResultSet;
import java.sql.SQLException;

import org.jfree.chart.ChartFactory;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.ChartUtilities;
import org.jfree.chart.title.TextTitle;
import org.jfree.chart.plot.CategoryPlot;
import org.jfree.chart.plot.CategoryMarker;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.axis.CategoryAxis;
import org.jfree.chart.axis.CategoryLabelPositions;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.renderer.category.LineAndShapeRenderer;
import org.jfree.data.category.DefaultCategoryDataset;
import org.jfree.ui.Layer;

public class AtmosphereChart extends HttpServlet {

    private String sqlDataBase      ;
    private String sqlTable         ;
    private String sqlColDateTime   ;
    private String sqlColTemperature;
    private String sqlColHumidity   ;
    private String sqlColPressure   ;

    private String sqlUser    ;
    private String sqlPassword;

    private String chartFile;

    private static final String PERIOD_1_DAY    = "1";
    private static final String PERIOD_2_DAYS   = "2";
    private static final String PERIOD_3_DAYS   = "3";
    private static final String PERIOD_7_DAYS   = "7";
    private static final String PERIOD_30_DAYS  = "30";
    private static final String PERIOD_90_DAYS  = "90";
    private static final String PERIOD_180_DAYS = "180";
    private static final String PERIOD_360_DAYS = "360";

    // {0} table name {1} column name(datetime) {2} period(minute)
    private static final String sqlTemplate =
        "SELECT * FROM {0} WHERE {1} > ( SELECT MAX( {1} ) FROM {0} ) - INTERVAL {2,number,#} - 1 MINUTE ";

    private static final HashMap<String, String> sqlConditions;
    static {
        sqlConditions = new HashMap<>();
        sqlConditions.put( PERIOD_1_DAY   , ";" );
        sqlConditions.put( PERIOD_2_DAYS  , ";" );
        sqlConditions.put( PERIOD_3_DAYS  , "AND MOD( HOUR( {1} ), 2 ) = 0;" );
        sqlConditions.put( PERIOD_7_DAYS  , "AND MOD( HOUR( {1} ), 3 ) = 0;" );
        sqlConditions.put( PERIOD_30_DAYS , "AND HOUR( {1} ) = 12;" );
        sqlConditions.put( PERIOD_90_DAYS , "AND MOD( DAYOFMONTH( {1} ), 2 ) = 1 AND HOUR( {1} ) = 12;" );
        sqlConditions.put( PERIOD_180_DAYS, "AND MOD( DAYOFMONTH( {1} ), 4 ) = 1 AND HOUR( {1} ) = 12;" );
        sqlConditions.put( PERIOD_360_DAYS, "AND MOD( DAYOFMONTH( {1} ), 8 ) = 1 AND HOUR( {1} ) = 12;" );
    }

    private static final String path = Thread.currentThread().getContextClassLoader().getResource( "" ).getPath();
    private static final Font labelFont     = new Font( Font.SANS_SERIF, Font.PLAIN, 12 );
    private static final Font tickLabelFont = new Font( Font.SANS_SERIF, Font.PLAIN, 10 );

    private java.util.logging.Logger logger;

    @Override
    protected void doGet( HttpServletRequest request, HttpServletResponse response )
                   throws ServletException, IOException {
        /*
            logger = java.util.logging.Logger.getLogger( "debug" );
            try {
                java.util.logging.FileHandler fh = new java.util.logging.FileHandler( "/home/pi/debug.log" );
                fh.setFormatter( new java.util.logging.SimpleFormatter() );
                logger.addHandler( fh );
            } catch ( IOException e ) {
            }
        */

        String[] datetime    = null;
        double[] temperature = null;
        double[] humidity    = null;
        double[] pressure    = null;
        ArrayList<String> markerLabels = new ArrayList<>();

        int size = 0;

        String period = request.getParameter( "period" );
        if ( period == null ) period = PERIOD_1_DAY;

        getProperties();

        // Read data from database

        Connection conn = null;
        String url = "jdbc:mysql://localhost/" + sqlDataBase;

        try {
            Class.forName( "com.mysql.jdbc.Driver" ).newInstance();
            conn = DriverManager.getConnection( url, sqlUser, sqlPassword );

            Statement stmt = conn.createStatement();

            String sql = makeSQLStatement( period );
            // logger.log( java.util.logging.Level.INFO, "SQL statement : " + sql );

            ResultSet rs = stmt.executeQuery( sql );

            rs.last();
            size = rs.getRow();
            rs.beforeFirst();

            datetime    = new String[ size ];
            temperature = new double[ size ];
            humidity    = new double[ size ];
            pressure    = new double[ size ];

            int i = 0;
            while( rs.next() ) {
                datetime   [ i ] = MessageFormat.format(
                                       "{0,date,MM-dd} {1,time,HH:mm}",
                                       rs.getDate( sqlColDateTime ), rs.getTime( sqlColDateTime ) );
                temperature[ i ] = rs.getDouble( sqlColTemperature );
                humidity   [ i ] = rs.getDouble( sqlColHumidity );
                pressure   [ i ] = rs.getDouble( sqlColPressure );

                if ( isMarkerLabel( period, datetime[ i ] ) ) {
                    markerLabels.add( datetime[ i ] );
                }

                i ++ ;
            }

            rs.close();
            stmt.close();
        } catch ( ClassNotFoundException e ) {
            logger.log( java.util.logging.Level.WARNING, "", e );
        } catch ( SQLException e ) {
            logger.log( java.util.logging.Level.WARNING, "", e );
        } catch ( Exception e ) {
            logger.log( java.util.logging.Level.WARNING, "", e );
        } finally {
            try {
                if ( conn != null ) {
                    conn.close();
                }
            } catch ( SQLException e ) {
                logger.log( java.util.logging.Level.WARNING, "", e );
            }
        }

        // Draw Chart

        LineAndShapeRenderer renderer;
        CategoryAxis cAxis;
        NumberAxis   nAxis;

        DefaultCategoryDataset dataT = new DefaultCategoryDataset();
        DefaultCategoryDataset dataH = new DefaultCategoryDataset();
        DefaultCategoryDataset dataP = new DefaultCategoryDataset();

        for( int i = 0; i < size; i ++ ) {
            dataT.addValue( ( Number )temperature[ i ], "Temperature", datetime[ i ] );
            dataH.addValue( ( Number )humidity   [ i ], "Humidity"   , datetime[ i ] );
            dataP.addValue( ( Number )pressure   [ i ], "Pressure"   , datetime[ i ] );
        }

        // Temperature
        JFreeChart chart = ChartFactory.createLineChart(
            "Sensor Data History Graph - " + period + ( period.equals( "1" ) ? " day" : " days" ),
            "Time",
            "Temperature (C)",
            dataT,
            PlotOrientation.VERTICAL,
            true,
            false,
            false
        );
        chart.setBackgroundPaint( Color.WHITE );
        TextTitle subtitle = new TextTitle(
            size != 0 ?
            MessageFormat.format( "Last data({0})  Temperature: {1} C  Humidity: {2} %  Pressure: {3} hPa",
                                  datetime   [ size - 1 ],
                                  temperature[ size - 1 ],
                                  humidity   [ size - 1 ],
                                  pressure   [ size - 1 ] ) :
            "No data to plot on the graph."
        );
        chart.addSubtitle( subtitle );

        CategoryPlot plot = chart.getCategoryPlot();

        cAxis = plot.getDomainAxis();
        cAxis.setCategoryLabelPositions( CategoryLabelPositions.UP_90 );
        cAxis.setLabelFont( labelFont );
        cAxis.setTickLabelFont( tickLabelFont );

        renderer = ( LineAndShapeRenderer )plot.getRenderer();
        nAxis = ( NumberAxis )plot.getRangeAxis();

        customizeChart( renderer, nAxis, 0.0, 50.0, Color.RED );

        // Add Humidity
        renderer = new LineAndShapeRenderer();
        nAxis = new NumberAxis( "Humidity (%)" );

        customizeChart( renderer, nAxis, 0.0, 100.0, Color.BLUE );

        plot.setRenderer( 1, renderer );    // renderer #1
        plot.setDataset( 1, dataH );        // dataset  #1
        plot.setRangeAxis( 1, nAxis );      // range axis #1
        plot.mapDatasetToRangeAxis( 1, 1 ); // map dataset #1 to range axis #1

        // Add Pressure
        renderer = new LineAndShapeRenderer();
        nAxis = new NumberAxis( "Pressure (hPa)" );

        customizeChart( renderer, nAxis, 950.0, 1050.0, Color.GREEN );

        plot.setRenderer( 2, renderer );    // renderer #2
        plot.setDataset( 2, dataP );        // dataset  #2
        plot.setRangeAxis( 2, nAxis );      // range axis #2
        plot.mapDatasetToRangeAxis( 2, 2 ); // map dataset #2 to range axis #2

        for ( String markerLabel : markerLabels ) {
            CategoryMarker marker = new CategoryMarker( markerLabel );
            marker.setPaint( Color.WHITE );
            marker.setDrawAsLine( true );
            marker.setStroke( new BasicStroke( 1.0f ) );
            plot.addDomainMarker( marker, Layer.BACKGROUND );
        }

        try {
            ChartUtilities.saveChartAsPNG( new File( chartFile ), chart, 800, 600 );
        } catch ( IOException e ) {
            logger.log( java.util.logging.Level.WARNING, "", e );
        }

        RequestDispatcher disp = request.getRequestDispatcher( "/result.html" );
        disp.forward( request, response );
    }

    void getProperties() {
        Path propPath = Paths.get( path + "atmosphere.properties" );
        try ( BufferedReader reader = Files.newBufferedReader( propPath, StandardCharsets.UTF_8 ) ) {
            Properties properties = new Properties();
            properties.load( reader );

            sqlDataBase       = properties.getProperty( "mysql.db" );
            sqlTable          = properties.getProperty( "mysql.db.table" );
            sqlColDateTime    = properties.getProperty( "mysql.db.table.datetime" );
            sqlColTemperature = properties.getProperty( "mysql.db.table.temperature" );
            sqlColHumidity    = properties.getProperty( "mysql.db.table.humidity" );
            sqlColPressure    = properties.getProperty( "mysql.db.table.puressure" );
            sqlUser           = properties.getProperty( "mysql.user" );
            sqlPassword       = properties.getProperty( "mysql.password" );
            chartFile         = properties.getProperty( "jfreechart.chartfile" );
        } catch ( IOException e ) {
            logger.log( java.util.logging.Level.WARNING, "", e );
        }
    }

    String makeSQLStatement( String period ) {
        String conditions = sqlConditions.get( period );
        if ( conditions == null ) conditions = ";";

        return MessageFormat.format( sqlTemplate + conditions,
                                     sqlTable, sqlColDateTime, Integer.parseInt( period ) * 24 * 60 );
    }

    boolean isMarkerLabel( String period, String datetime ) {

        switch ( period ) {
            case PERIOD_2_DAYS:
            case PERIOD_3_DAYS:
            case PERIOD_7_DAYS:
                return datetime.indexOf( "00:00" ) >= 0;

            case PERIOD_30_DAYS:
                return datetime.indexOf( "-01" ) >= 0 || datetime.indexOf( "-11" ) >= 0 ||
                       datetime.indexOf( "-21" ) >= 0;

            case PERIOD_90_DAYS:
            case PERIOD_180_DAYS:
            case PERIOD_360_DAYS:
                return datetime.indexOf( "-01" ) >= 0;

            default: // assume "1"
                return datetime.indexOf( "00:00" ) >= 0 || datetime.indexOf( "12:00" ) >= 0;
        }
    }

    void customizeChart( LineAndShapeRenderer renderer, NumberAxis axis,
                         double lower, double upper, Color color ) {

        renderer.setSeriesPaint( 0, color );
        renderer.setSeriesStroke( 0, new BasicStroke( 1.0f ) );
        renderer.setSeriesShapesVisible( 0, true );

        axis.setRange( lower, upper);
        axis.setLabelFont( labelFont );
        axis.setTickLabelFont( tickLabelFont );
        axis.setTickLabelPaint( color );
    }
}
```

ソースコードをコンパイルして class ファイルを作成します。

```
$ cd ~/apache-tomcat-9.0.6/webapps/atmosphere/WEB-INF/classes
$ javac AtmosphereChart.java
```

JFreeChart を使用したプログラミングについては、以下の記事を参考にさせていただきました。

（参考記事）
> [JFreeChartでグラフ作成](https://qiita.com/tontan9616/items/7c755aadf954a5a26ffa)


このサーブレットの名前や呼び出し方などを定義するために ~/apache-tomcat-9.0.6/webapps/atmosphere/WEB-INF の下に以下のファイル web.xml を作成します。

```xml:web.xml
<?xml version="1.0" encoding="ISO-8859-1"?>

<web-app xmlns="http://java.sun.com/xml/ns/javaee"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
                      http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
  version="3.0">

  <!-- servlet definition -->
  <servlet>
    <servlet-name>AtmosphereChart</servlet-name>
    <servlet-class>AtmosphereChart</servlet-class>
  </servlet>

  <!-- mapping between servlet and UTL -->
  <servlet-mapping>
    <servlet-name>AtmosphereChart</servlet-name>
    <url-pattern>/AtmosphereChart</url-pattern>
  </servlet-mapping>

</web-app>
```

最後に 2 つの HTML ファイル index.html および result.html　を ~/apache-tomcat-9.0.6/webapps/atmosphere の下に配置して下さい。

```html:index.html
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
    <meta http-equiv="refresh" content="0; URL='./AtmosphereChart'">
</head>
</html>
```

```html:result.html
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <meta http-equiv="Pragma" content="no-cache">
    <meta http-equiv="Cache-Control" content="no-cache">
    <meta http-equiv="Expires" content="0">
    <style>
        .period {
            font-family:sans-serif;
            padding: 20px;
        }
        .period .form-title {
            font-weight: bold;
            font-size:14px;
        }
        .period label {
            font-size:12px;
        }
        .period input[type=submit] {
            appearance: none;
            outline: none;
            border: none;
            color: #fff;
            background-color: #7294f2;
            height: 24px;
            width: 52px;
            margin-left: 20px;
        }
        .period input[type=submit]:hover{
            background-color: #8af;
            cursor:pointer;
        }
    </style>
    <title>History Graph</title>
</head>
<body><center>
    <table border="0">
    <tr><td><img src="./atmosphere.png"></td></tr>
    <tr><td>
        <form class="period" method="get" action="./AtmosphereChart">
            <fieldset>
            <legend class="form-title">
            Select period to drow graph for Temperature, Humidity and Pressure.
            </legend>
            <center>
            <label><input type="radio" name="period" value="1" checked> 1 day</label>
            <label><input type="radio" name="period" value="2"> 2 days</label>
            <label><input type="radio" name="period" value="3"> 3 days</label>
            <label><input type="radio" name="period" value="7"> 7 days</label>
            <label><input type="radio" name="period" value="30"> 30 days</label>
            <label><input type="radio" name="period" value="90"> 90 days</label>
            <label><input type="radio" name="period" value="180"> 180 days</label>
            <label><input type="radio" name="period" value="360"> 360 days</label>
            <input type="submit" value="OK">
            </center>
            </fieldset>
        </form>
    </td></tr>
    </table>
</center></body>
</html>
```

以上ですべての設定の完了です。~/apache-tomcat-9.0.6/webapps/atmosphere/ の下には、以下のファイルが作成されています。

```
atmosphere
├── atmosphere.sh
├── index.html
├── result.html
└── WEB-INF
    ├── classes
    │   ├── AtmosphereChart.class
    │   ├── AtmosphereChart.java
    │   ├── atmosphere.properties
    │   ├── BMP180.class
    │   ├── BMP180.java
    │   ├── DHT11$1.class
    │   ├── DHT11.class
    │   ├── DHT11.java
    │   ├── DHT11Result.class
    │   ├── DHT11Result.java
    │   ├── DHT11$SignalTransition.class
    │   ├── RecordingTask.class
    │   └── RecordingTask.java
    └── web.xml
```

#グラフの表示
URLに以下を指定してグラフを表示させます。

```
http://[ipアドレス または localhost]:8080/atmosphere/
```

最新のデータから過去 1 日分のデータがグラフ化されます。表示する期間を変えたい場合は、ページの下にあるラジオボタンから期間を選んで OK ボタンをクリックします。グラフが煩雑にならないように、選択した期間によって以下のルールに従ってデータをピックアップしてグラフ化しています。

|選択した期間|ピックアップするデータ|
|:--|:--|
|1 Day / 2 Days|毎時|
|3 Days|2 時間ごと（偶数時）|
|7 Days|3 時間ごと（0時、3時、6時・・・）|
|30 Days|毎日12時|
|90 Days|1 日置き（1日、3日、5日・・・）の12時|
|180 Days|4 日置き（1日、5日、9日・・・）の12時|
|360 Days|8 日置き（1日、9日、17日・・・）の12時|

したがって例えば 90 Days を選択しても、90 日間分のデータがないと表示されるデータが少なかったり、上記の条件に合致しているデータがないと、折れ線グラフが表示されないこともあります。

既知の問題として、期間を選んで OK をクリックしてもグラフが更新されない場合があります。これはブラウザのキャッシュに残ったイメージが表示されるためのようですが（Raspberry Pi 上では正しくイメージファイルが更新されています）、この場合はもう一度期間を選んで OK をクリックし直してみて下さい。