com.github.ithildir.airbot.service.impl.AirNowMeasurementServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for com.github.ithildir.airbot.service.impl.AirNowMeasurementServiceImpl.java

Source

/**
 * Copyright (c) 2017 Andrea Di Giorgi
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package com.github.ithildir.airbot.service.impl;

import com.github.ithildir.airbot.model.Location;
import com.github.ithildir.airbot.model.Measurement;
import com.github.ithildir.airbot.service.MeasurementService;
import com.github.ithildir.airbot.util.RecordParserWriteStream;

import io.netty.handler.codec.http.HttpResponseStatus;

import io.vertx.core.AsyncResult;
import io.vertx.core.Future;
import io.vertx.core.Handler;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.http.HttpHeaders;
import io.vertx.core.logging.Logger;
import io.vertx.core.logging.LoggerFactory;
import io.vertx.core.parsetools.RecordParser;
import io.vertx.ext.web.client.HttpRequest;
import io.vertx.ext.web.client.HttpResponse;
import io.vertx.ext.web.client.WebClient;
import io.vertx.ext.web.client.WebClientOptions;
import io.vertx.ext.web.codec.BodyCodec;
import io.vertx.serviceproxy.ServiceException;

import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.CharUtils;
import org.apache.commons.lang3.StringUtils;

/**
 * @author Andrea Di Giorgi
 */
public class AirNowMeasurementServiceImpl implements MeasurementService {

    public AirNowMeasurementServiceImpl(Vertx vertx) {
        WebClientOptions webClientOptions = new WebClientOptions();

        webClientOptions.setDefaultHost("files.airnowtech.org");
        webClientOptions.setDefaultPort(443);
        webClientOptions.setSsl(true);

        _webClient = WebClient.create(vertx, webClientOptions);
    }

    @Override
    public void getMeasurement(double latitude, double longitude, Handler<AsyncResult<Measurement>> handler) {

        Measurement measurement = null;

        String reportingArea = _getReportingArea(latitude, longitude);

        if (reportingArea != null) {
            measurement = _reportingAreaMeasurements.get(reportingArea);
        }

        handler.handle(Future.succeededFuture(measurement));
    }

    @Override
    public void getName(Handler<AsyncResult<String>> handler) {
        handler.handle(Future.succeededFuture("airnow"));
    }

    @Override
    public void init(Handler<AsyncResult<Void>> handler) {
        if (StringUtils.isBlank(_reportingAreaETag)) {
            _initReportingAreaRecords(handler);

            return;
        }

        HttpRequest<?> httpRequest = _webClient.head(_REPORTING_AREA_URI);

        httpRequest = httpRequest.as(BodyCodec.none());

        httpRequest.send(asyncResult -> {
            HttpResponse<?> httpResponse = _handleHttpResponse(asyncResult, handler);

            if (httpResponse == null) {
                return;
            }

            String etag = httpResponse.getHeader(HttpHeaders.ETAG.toString());

            if (_reportingAreaETag.equals(etag)) {
                if (_logger.isDebugEnabled()) {
                    _logger.debug("AirNow reporting area records are already " + "up-to-date with ETag {0}", etag);
                }

                return;
            }

            _initReportingAreaRecords(handler);
        });
    }

    private String _getReportingArea(double latitude, double longitude) {
        Location coordinates = new Location(latitude, longitude, "US");

        double distance = Double.MAX_VALUE;
        String reportingArea = null;

        for (Map.Entry<String, Location> entry : _reportingAreaCoordinates.entrySet()) {

            Location curCoordinates = entry.getValue();

            double curDistance = coordinates.getDistance(curCoordinates);

            if ((reportingArea == null) || (curDistance < distance)) {
                distance = curDistance;
                reportingArea = entry.getKey();
            }
        }

        return reportingArea;
    }

    private <R, T> HttpResponse<T> _handleHttpResponse(AsyncResult<HttpResponse<T>> asyncResult,
            Handler<AsyncResult<R>> handler) {

        if (asyncResult.failed()) {
            handler.handle(Future.failedFuture(asyncResult.cause()));

            return null;
        }

        HttpResponse<T> httpResponse = asyncResult.result();

        int statusCode = httpResponse.statusCode();

        if (statusCode != HttpResponseStatus.OK.code()) {
            handler.handle(ServiceException.fail(statusCode, httpResponse.statusMessage()));

            return null;
        }

        return httpResponse;
    }

    private void _initReportingAreaRecord(Buffer buffer) {
        String[] values = StringUtils.split(buffer.toString(), '|');

        char dataType = CharUtils.toChar(values[5]);

        if (dataType != 'O') {
            return;
        }

        boolean primary = BooleanUtils.toBoolean(values[6]);

        if (!primary) {
            return;
        }

        String stateCode = values[8];

        if (StringUtils.isBlank(stateCode)) {
            return;
        }

        long time = _parseTime(values[1], values[2], values[3]);
        String reportingArea = values[7];
        double latitude = Double.parseDouble(values[9]);
        double longitude = Double.parseDouble(values[10]);
        String mainPollutant = _parsePollutant(values[11]);
        int aqi = Integer.parseInt(values[12]);
        String comments = values[15];

        Location coordinates = new Location(latitude, longitude, "US");

        _reportingAreaCoordinates.put(reportingArea, coordinates);

        Measurement measurement = new Measurement(reportingArea, time, aqi, mainPollutant, Collections.emptyMap(),
                comments);

        _reportingAreaMeasurements.put(reportingArea, measurement);
    }

    private void _initReportingAreaRecords(Handler<AsyncResult<Void>> handler) {
        HttpRequest<?> httpRequest = _webClient.get(_REPORTING_AREA_URI);

        RecordParser recordParser = RecordParser.newDelimited("\n", this::_initReportingAreaRecord);

        httpRequest = httpRequest.as(BodyCodec.pipe(new RecordParserWriteStream(recordParser)));

        httpRequest.send(asyncResult -> {
            HttpResponse<?> httpResponse = _handleHttpResponse(asyncResult, handler);

            if (httpResponse == null) {
                return;
            }

            _reportingAreaETag = httpResponse.getHeader(HttpHeaders.ETAG.toString());

            if (_logger.isInfoEnabled()) {
                _logger.info("AirNow reporting area records updated with ETag {0}", _reportingAreaETag);
            }

            handler.handle(Future.succeededFuture());
        });
    }

    private String _parsePollutant(String pollutant) {
        if (pollutant.equalsIgnoreCase("ozone")) {
            pollutant = "o3";
        } else if (pollutant.equalsIgnoreCase("pm2.5")) {
            pollutant = "pm25";
        } else {
            pollutant = pollutant.toLowerCase();
        }

        return pollutant;
    }

    private long _parseTime(String dateString, String timeString, String timeZoneString) {

        int year = Integer.parseInt(dateString.substring(6)) + 2000;
        int month = Integer.parseInt(dateString.substring(0, 2));
        int dayOfMonth = Integer.parseInt(dateString.substring(3, 5));
        int hour = Integer.parseInt(timeString.substring(0, timeString.length() - 3));
        int minute = Integer.parseInt(timeString.substring(timeString.length() - 2));

        ZoneId zoneId = ZoneId.of(timeZoneString, _shortZoneIds);

        ZonedDateTime zonedDateTime = ZonedDateTime.of(year, month, dayOfMonth, hour, minute, 0, 0, zoneId);

        Instant instant = zonedDateTime.toInstant();

        return instant.toEpochMilli();
    }

    private static final String _REPORTING_AREA_URI = "/airnow/today/reportingarea.dat";

    private static Logger _logger = LoggerFactory.getLogger(AirNowMeasurementServiceImpl.class);

    private static final Map<String, String> _shortZoneIds;

    static {
        _shortZoneIds = new HashMap<>(ZoneId.SHORT_IDS);

        _shortZoneIds.put("ADT", "US/Alaska");
        _shortZoneIds.put("CDT", "America/Chicago");
        _shortZoneIds.put("COT", "America/Bogota");
        _shortZoneIds.put("EDT", "America/New_York");
        _shortZoneIds.put("MDT", "America/Denver");
        _shortZoneIds.put("PDT", "America/Los_Angeles");
    }

    private final Map<String, Location> _reportingAreaCoordinates = new HashMap<>();
    private String _reportingAreaETag;
    private final Map<String, Measurement> _reportingAreaMeasurements = new HashMap<>();
    private final WebClient _webClient;

}