Strict Standards: Non-static method Text_Highlighter::factory() should not be called statically in /home2/rwy/public_html/airquality/colorize.php on line 13
<?php
// readaqi.php
// russell young (Zaqi (at) youngZ-0.com, remove the "Z"s)
//
// Originally this used the twitter feed, but that has proven unreliable, often missing 
// readings for days at a time. Then I added the BeijingAir readings, which worked fine
// until my web host stopped being able to read it (why?). So, I reverted to using twitter,
// supplementing it with uploading BeijingAir data for the missing twitter data.
//
// This is also why the processing was changed from using the data in the feed to getting
// it from the db: bjhack would set history and the image right, but next time twitter
// ran it would overwrite them with old data.
//
// uncomment this to run locally for testing
//define('LOCAL', true);
 
// Set to 'twitter' or 'bjair'
define('SOURCE', 'bjair');
 
// get private information (sql login, directories, email accounts, etc.)
if (defined('LOCAL')) {
    define('BASEDIR', '/tmp/');
    define('BJAIR_FEED', '/tmp/BeijingAir.xml');
    define('TWITTER_FEED', '/tmp/twitter.xml');
    define('PUBLICDIR', '/tmp/');
}
else {
    include 'private.php';
//    define('BJAIR_FEED', 'http://www.beijingaqifeed.com/BeijingAQI/BeijingAir.xml');
    define('BJAIR_FEED', 'http://www.stateair.net/web/rss/1/1.xml');
    define('BJAIR_HACK_FEED', 'bjair.xml');
    define('TWITTER_FEED', 'https://api.twitter.com/1/statuses/user_timeline.rss?screen_name=beijingair');
    define('PUBLICDIR', BASEDIR . 'public_html/airquality/');
}
// This file gets hourly PM2.5 information from bjair, a public site, and 
// stores it into a sql database.
//define('BASEDIR', '/tmp/');
define('AQIDIR', BASEDIR . 'airquality/');
define('LOGFILE', AQIDIR . 'aqi.log');
define('HOURS_BETWEEN_WARNINGS', 6);
define('LAST_SENT_FILE', AQIDIR . 'warningSent.txt');
define('HISTORY_SIZE', 12);
define('RETRY_COUNT', 3);
define('RETRY_DELAY', 600);
 
class Readings {
    protected $support = false;
    private $offset = 1;
    private $xml = '';
    protected $feed = '(error)';
 
    function __construct($support) { $this->support = $support; }
 
    function init() {
    echo "Looking for " . $this->feed;
        $this->offset = 1;
        if (strlen($this->xml) < 10) 
            $this->xml = file_get_contents($this->feed);
        if (strlen($this->xml) > 10) 
            return '';
 
        $this->offset = false;
        return 'Was not able to read data from ' . $this->feed;
    }
 
    function reset($db = false) {
        $this->support->clear();
        $this->offset = (!defined('LOCAL') && $db) ? -1 : 1; 
    }
 
    // returns an array of values that give the data and the error status
    // returns false if completed
    function next() {
        if ($this->offset < 0)
            return $this->support->dbNext();
        if (!$this->offset)
            return false;
 
        $start = strpos($this->xml, '<item>', $this->offset);
        if ($start === false)
            return false;
        $this->offset = strpos($this->xml, '</item>', $start);
        if ($this->offset === false) 
            return array('error' => 'Could not find closing tag </item>', 'offset' => false);
        $xml = substr($this->xml, $start, $this->offset - $start);
        $ret = $this->parse($xml);
        return ($ret) ? $ret : $this->next();
    }
    
    function getXml() {return $this->xml; }
}        
 
class ReadTwitter extends Readings {
    protected $feed = TWITTER_FEED;
    function parse($xml) {
        if ((strpos($xml, '<title>Twitter') !== false) 
            || strpos($xml, 'No Data') 
            || strpos($xml, 'No Reading')
            || preg_match('/[0-9]+ to [0-9]+-/', $xml))
            return false;
        if (preg_match('/<title>[^0-9]*([0-9]+)-([0-9]+)-([0-9]+) ([0-9]+):[0-9]+; *PM2.5; *([0-9]+)[^;]*; *([0-9]+)/', $xml, $matches)) {
            return array('error' => '',
                 'date'  => "$matches[3]-$matches[1]-$matches[2]",
                 'hour'  => $matches[4],
                 'aqi'   => $matches[6],
                 'conc'  => $matches[5]);
        }
        return array('error' => 'Unrecognized data'. $xml, 'date' => '', 'hour' => '', 'aqi' => '', 'conc' => '');
    }
}
 
class ReadBJAir extends Readings {
    protected $feed = BJAIR_FEED;
    function parse($xml) { echo "reading $this->feed";
        $ret = array('error' => '', 'date' => '', 'hour' => '', 'aqi' => '', 'conc' => '');
        if (preg_match('/<AQI>([-0-9]*)/', $xml, $matches)) {
            $ret['aqi'] = $matches[1];
            if ($ret['aqi'] < 0)
                return false;
        }
        else 
            $ret['error'] .= "\nNo aqi found";
 
        if (preg_match('/<Conc>([0-9]*)/', $xml, $matches)) 
            $ret['conc'] = $matches[1];
        else 
            $ret['error'] .= "\nNo conc found";
            
        if (preg_match('/<ReadingDateTime>([0-9]+)\/([0-9]+)\/([0-9]+) ([0-9]+):[0-9:]+ *(.)M/', $xml, $matches)) {
            $ret['date'] = "$matches[3]-$matches[1]-$matches[2]";
            $ret['hour'] = $matches[4];
            if ($matches[5] == 'P') {
                if ($ret['hour'] != 12)
                    $ret['hour'] += 12;
            }
            else if ($ret['hour'] == 12)
                $ret['hour'] = 0;
        }
        else
            $ret['error'] .= "\nCould not find date/time";
        
        return $ret;
    }
}
 
class HackBJAir extends ReadBJAir {
    protected $feed = BJAIR_HACK_FEED;
}
 
class LocalSupport {
    function reschedule($count) {
        echo "rescheduling $count<br>\n";
    }
 
    function setdb($date, $hour, $aqi, $conc, $city) {
        $query = "REPLACE airQuality VALUES ('$date', $hour, $aqi, $conc, $city)";
        echo "$query<br>\n";
    }
    function dbnext() { }
    
    function mailInterval($subject, $message) {
        echo "Send mail<br>\n";
    }
    function logToFile($msg, $exit = true) {
        $msg = date('Y-m-d H:i') . " $msg";
        echo "LOG: $msg<br>\n";
        if ($exit)
            exit();
    }
    function makeImage($date, $hour, $level) {
        echo "Making image for $date $hour: $level<br>\n";
    }
    function clear() {}
}
 
class ServerSupport {
    private $sql = false;
    private $sqlResult = false;
    
    function reschedule($count) {
        echo "rescheduling $count<br>\n";
    }
 
    function sql() {
        if (!$this->sql) {
            $this->sql = new mysqli(DBHOST, DBUSER, DBPASSWORD, DB, DBSOCKET);
            if ($this->sql->connect_error) {
                global $argv;
                $this->reschedule((sizeof($argv) > 1) ? (int) $argv[1] : RETRY_COUNT);
                $this->logToFile('SQL connection error xxx' . $this->sql->connect_errno . ': ' . $this->sql->connect_error, 'email');
            }
        }
        return $this->sql;
    }
    
    function clear() {
        if ($this->sqlResult) {
            $this->sqlResult->free();
            $this->sqlResult = false;
        }
    }
    
    function setdb($date, $hour, $aqi, $conc, $city) {
        $query = "REPLACE airQuality VALUES ('$date', $hour, $aqi, $conc, $city)";
        if (!empty($_GET['test']))
            echo "$query<br>\n";
        else 
            return ($this->sql()->query($query)) ? '' : "Failed updating db: $date, $hour, $aqi, $conc";
        return '';
    }
    
    function dbnext() {
        if (!$this->sqlResult) {
            $query = 'SELECT * FROM `airQuality` where city = 0 order by day desc, hour desc limit 0, ' . HISTORY_SIZE;
            if (!($this->sqlResult = $this->sql()->query($query)))
                $this->logToFile('SQL query error ccc' . $this->sql->connect_errno . ': ' . $this->sql->connect_error, 'email');
        }
        $row = $this->sqlResult->fetch_array(MYSQLI_ASSOC);    
        if ($row) {
            $row['error'] = '';
            $row['aqi'] = $row['avg'];
            return $row;
        }
        $this->sqlResult->free();
        $this->sqlResult = false;
        return false;
    }
    
    function mailInterval($subject, $message) {
        $last = (is_file(LAST_SENT_FILE)) ? file_get_contents(LAST_SENT_FILE) : 0;
        if ((time() - $last)/3600 >= HOURS_BETWEEN_WARNINGS) {
            $this->sendwarning($subject, $message);
            file_put_contents(LAST_SENT_FILE, time());
        }
    }
 
    function sendwarning($subject, $contents) {
        mail(MAIL_TO_ME, $subject, $contents, "From: " . MAIL_FROM);
        mail(MAIL_TO_HUA, $subject, $contents, "From: " . MAIL_FROM);
    }
 
    function logToFile($msg, $exit = true) {
        $msg = date('Y-m-d H:i') . " $msg";
        file_put_contents(LOGFILE, "$msg\n", FILE_APPEND);
        echo "<br />$msg<br />\n";
        if ($exit) {
            if ($exit === 'email') 
                mail(MAIL_TO_ME, 'Error in getting aqi xxx', "Error message (readaqi):\n$msg", 'From: ' . MAIL_FROM);
            if ($this->sql)
                $this->sql->close();
            exit();
        }
    }
 
    function makeImage($date, $hour, $level) {
        $desc = array(array(0,        'no reading',    255, 255, 255, 0, 0, 0),
                  array(50,    'Good',        0,   0xe4, 0,   0, 0, 0),
                  array(100,    'Moderate',     0xff, 0xff, 0, 0, 0, 0),
                  array(150,    'Heavy',     0xff, 0x7e, 0, 255, 255, 255),
                  array(200,    'Unhealthy',     0xff, 0, 0, 255, 255, 255),
                  array(300,    'Very unhealthy', 0x7e, 0, 0x23, 255, 255, 255),
                  array(500,    'Hazardous',     0x88, 0x88, 0x88, 255, 255, 255),
                  array(9999,    'Crazy bad',    0,   0,   0, 255, 255, 255));
        
        for ($i = sizeof($desc); $i-- && ($desc[$i][0]) > $level; );
        $desc = $desc[$i + 1];
        
        $my_img = imagecreate( 330, 40 );
        $background = imagecolorallocate( $my_img, $desc[2], $desc[3], $desc[4]);
        $text_colour = imagecolorallocate( $my_img, $desc[5], $desc[6], $desc[7]);
        imagestring( $my_img, 5, 15, 15, "$date $hour:00 : $level ($desc[1])", $text_colour );
        imagejpeg($my_img, PUBLICDIR . 'mostRecent.jpg');
        imagecolordeallocate($my_img, $text_color );
        imagecolordeallocate($my_img, $background );
        imagedestroy( $my_img );
    }
}
 
 
//
// execution starts 
//
date_default_timezone_set('Asia/Shanghai');
$support = (defined('LOCAL')) ? new LocalSupport() : new ServerSupport();
 
// hack is called when a datafile is uploaded through the upload.php webpage. That sets $hack
// and includes this file.
if (isset($hack)) {
    echo "Doing bjair hack<br>";
    $readings = new HackBJAir($support);
}
else {
    $which = (empty($_GET['source'])) ? SOURCE : $_GET['source'];
    $readings = ($which == 'twitter') ? new ReadTwitter($support) : new ReadBJAir($support);
}
 
if (($error = $readings->init()))
    $support->logToFile($error, 'email');
 
// First fill up database
$errors = $good = 0;
$city = 0;
while (($data = $readings->next())) {
    extract($data);
    if (!$error) 
        $error = $support->setdb($date, $hour, $aqi, $conc, $city);
    if ($error) {
        $support->logToFile($error, false);
        $errors++;
    }
    else $good++;
}
 
// next make the web page image of the current value, and check it for warnings
$readings->reset(true);
if (($data = $readings->next())) {
    extract($data);
    if ($error)
        $support->logToFile($error);
    $support->makeImage($date, $hour, $aqi);
    if ($aqi >= 500) 
        $support->mailInterval("AQI is $aqi", "AQI has gone over 500, be aware");
}
 
// Next make the static history data
$readings->reset(true);
$history = array();
$dump = false;
for ($count = HISTORY_SIZE; $count-- && (($data = $readings->next())); ) {
    extract($data);
    if ($error)
        $support->logToFile($error, false);
    else
        $history[] = "$date $hour $aqi $conc";
    if (!$date) {
        $dump = true;
        $x = var_dump($date, true);
        $msg = "No date, hack=$hack, data = '$x'";
        file_put_contents('bug.txt', "$msg\n", FILE_APPEND);
    }
    if ($dump) file_put_contents('bug.txt', var_dump($reading->getXml(), true), FILE_APPEND);
}
if ($history)
    file_put_contents(PUBLICDIR . 'mostRecent.dat', implode("\n", $history));
 
$support->logToFile("Update completed, $good updates, $errors errors");