package nl.topicus.onderwijs.dashboard.modules.wettercom;
import java.io.StringReader;
import java.security.MessageDigest;
import java.util.Calendar;
import java.util.Date;
import java.util.Map;
import java.util.TimeZone;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import nl.topicus.onderwijs.dashboard.config.ISettings;
import nl.topicus.onderwijs.dashboard.datasources.Weather;
import nl.topicus.onderwijs.dashboard.datatypes.WeatherReport;
import nl.topicus.onderwijs.dashboard.datatypes.WeatherType;
import nl.topicus.onderwijs.dashboard.keys.Key;
import nl.topicus.onderwijs.dashboard.modules.AbstractService;
import nl.topicus.onderwijs.dashboard.modules.DashboardRepository;
import nl.topicus.onderwijs.dashboard.modules.ServiceConfiguration;
import nl.topicus.onderwijs.dashboard.modules.topicus.RetrieverUtils;
import nl.topicus.onderwijs.dashboard.modules.topicus.StatusPageResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
@Service
@ServiceConfiguration(interval = 10, unit = TimeUnit.MINUTES)
public class WetterComService extends AbstractService {
private static final Logger log = LoggerFactory
.getLogger(WetterComService.class);
private Map<Key, WeatherReport> reports = new ConcurrentHashMap<Key, WeatherReport>();
@Autowired
public WetterComService(ISettings settings) {
super(settings);
}
@Override
public void onConfigure(DashboardRepository repository) {
for (Key key : getSettings().getKeysWithConfigurationFor(
WetterComService.class)) {
repository.addDataSource(key, Weather.class, new WeatherImpl(key,
this));
}
}
@Override
public void refreshData() {
try {
Map<Key, Map<String, ?>> serviceSettings = getSettings()
.getServiceSettings(WetterComService.class);
for (Map.Entry<Key, Map<String, ?>> curSettingEntry : serviceSettings
.entrySet()) {
Map<String, ?> wetterSettingsForProject = curSettingEntry
.getValue();
String apiKey = wetterSettingsForProject.get("apiKey")
.toString();
String applicationName = wetterSettingsForProject.get(
"applicationName").toString();
String cityKey = wetterSettingsForProject.get("cityKey")
.toString();
double latitude = (Double) wetterSettingsForProject
.get("latitude");
double longitude = (Double) wetterSettingsForProject
.get("longitude");
String total = applicationName + apiKey + cityKey;
byte[] thedigest = MessageDigest.getInstance("MD5").digest(
total.getBytes("UTF-8"));
StringBuffer sb = new StringBuffer();
for (int i = 0; i < thedigest.length; i++) {
sb.append(Integer.toString((thedigest[i] & 0xff) + 0x100,
16).substring(1));
}
StatusPageResponse response = RetrieverUtils
.getStatuspage(String.format("http://api.wetter.com"
+ "/forecast/weather/city/%s/project/%s/cs/%s",
cityKey, applicationName, sb.toString()));
DocumentBuilderFactory dbf = DocumentBuilderFactory
.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder();
InputSource is = new InputSource();
is.setCharacterStream(new StringReader(response
.getPageContent()));
Document doc = db.parse(is);
Element time = findTime(doc);
WeatherReport report = new WeatherReport();
report.setType(WeatherType.findType(Integer
.parseInt(getTextForElement(time, "w"))));
report.setRainfallProbability(Integer
.parseInt(getTextForElement(time, "pc")));
report.setMinTemperature(Double.parseDouble(getTextForElement(
time, "tn")));
report.setMaxTemperature(Double.parseDouble(getTextForElement(
time, "tx")));
report.setWindDirection(Integer.parseInt(getTextForElement(
time, "wd")));
report.setWindSpeed(Double.parseDouble(getTextForElement(time,
"ws")));
report.setSunrise(getSunrize(latitude, longitude));
report.setSunset(getSunset(latitude, longitude));
reports.put(curSettingEntry.getKey(), report);
}
} catch (Exception e) {
log.error("Unable to refresh data from wetter.com: {} {}", e
.getClass().getSimpleName(), e);
}
}
private Element findTime(Document doc) {
long curTime = System.currentTimeMillis() / 1000;
NodeList nodes = doc.getElementsByTagName("time");
for (int index = 0; index < nodes.getLength(); index++) {
Element curElement = (Element) nodes.item(index);
long date = Long.parseLong(getTextForElement(curElement, "d"));
long period = Long.parseLong(getTextForElement(curElement, "p"));
if (date != 0 && period != 0) {
if (date < curTime && (date + period * 3600) > curTime)
return curElement;
}
}
return null;
}
private String getTextForElement(Element root, String child) {
NodeList subNodes = root.getChildNodes();
for (int innerIndex = 0; innerIndex < subNodes.getLength(); innerIndex++) {
Node curSubNode = subNodes.item(innerIndex);
if (curSubNode.getNodeName().equals(child)) {
return curSubNode.getTextContent();
}
}
return null;
}
private static final double HOUR_IN_MS = 3600 * 1000;
public static Date getSunrize(double latitude, double longitude) {
return getSunrizeOrSet(latitude, longitude, true);
}
public static Date getSunset(double latitude, double longitude) {
return getSunrizeOrSet(latitude, longitude, false);
}
private static Date getSunrizeOrSet(double latitude, double longitude,
boolean rising) {
double zenith = 90.0 + 50.0 / 60.0;
// 1. first calculate the day of the year
//
// N1 = floor(275 * month / 9)
// N2 = floor((month + 9) / 12)
// N3 = (1 + floor((year - 4 * floor(year / 4) + 2) / 3))
// N = N1 - (N2 * N3) + day - 30
int N = Calendar.getInstance().get(Calendar.DAY_OF_YEAR);
// 2. convert the longitude to hour value and calculate an approximate
// time
//
// lngHour = longitude / 15
double lngHour = longitude / 15.0;
double t;
// if rising time is desired:
// t = N + ((6 - lngHour) / 24)
if (rising)
t = N + ((6.0 - lngHour) / 24.0);
else
t = N + ((18 - lngHour) / 24);
// 3. calculate the Sun's mean anomaly
// M = (0.9856 * t) - 3.289
double M = (0.9856 * t) - 3.289;
// 4. calculate the Sun's true longitude
// L = M + (1.916 * sin(M)) + (0.020 * sin(2 * M)) + 282.634
double L = M + (1.916 * sin(M)) + (0.020 * sin(2 * M)) + 282.634;
// NOTE: L potentially needs to be adjusted into the range [0,360) by
// adding/subtracting 360
L = makeInRange(L, 360.0);
// 5a. calculate the Sun's right ascension
// RA = atan(0.91764 * tan(L))
double RA = atan(0.91764 * tan(L));
// NOTE: RA potentially needs to be adjusted into the range [0,360) by
// adding/subtracting 360
RA = makeInRange(RA, 360.0);
// 5b. right ascension value needs to be in the same quadrant as L
// Lquadrant = (floor( L/90)) * 90
// RAquadrant = (floor(RA/90)) * 90
// RA = RA + (Lquadrant - RAquadrant)
double Lquadrant = (Math.floor(L / 90.0)) * 90.0;
double RAquadrant = (Math.floor(RA / 90.0)) * 90.0;
RA = RA + (Lquadrant - RAquadrant);
// 5c. right ascension value needs to be converted into hours
// RA = RA / 15
RA = RA / 15.0;
// 6. calculate the Sun's declination
// sinDec = 0.39782 * sin(L)
// cosDec = cos(asin(sinDec))
double sinDec = 0.39782 * sin(L);
double cosDec = cos(asin(sinDec));
// 7a. calculate the Sun's local hour angle
// cosH = (cos(zenith) - (sinDec * sin(latitude))) / (cosDec *
// cos(latitude))
double cosH = (cos(zenith) - (sinDec * sin(latitude)))
/ (cosDec * cos(latitude));
// 7b. finish calculating H and convert into hours
// if if rising time is desired:
// H = 360 - acos(cosH)
double H;
if (rising)
H = 360.0 - acos(cosH);
else
H = acos(cosH);
// H = H / 15
H = H / 15.0;
// 8. calculate local mean time of rising/setting
//
// T = H + RA - (0.06571 * t) - 6.622
double T = H + RA - (0.06571 * t) - 6.622;
//
// 9. adjust back to UTC
//
// UT = T - lngHour
double UT = T - lngHour;
// NOTE: UT potentially needs to be adjusted into the range [0,24) by
// adding/subtracting 24
// 10. convert UT value to local time zone of latitude/longitude
// localT = UT + localOffset
long timezone = TimeZone.getDefault().getOffset(
System.currentTimeMillis());
UT = makeInRange(UT + timezone / HOUR_IN_MS, 24.0);
Calendar today = Calendar.getInstance();
today.set(Calendar.HOUR_OF_DAY, 0);
today.set(Calendar.MINUTE, 0);
today.set(Calendar.SECOND, 0);
today.set(Calendar.MILLISECOND, 0);
today.setTimeInMillis(today.getTimeInMillis()
+ (long) (UT * HOUR_IN_MS));
return today.getTime();
}
private static double acos(double a) {
return Math.toDegrees(Math.acos(a));
}
private static double asin(double a) {
return Math.toDegrees(Math.asin(a));
}
private static double atan(double a) {
return Math.toDegrees(Math.atan(a));
}
private static double cos(double a) {
return Math.cos(Math.toRadians(a));
}
private static double sin(double a) {
return Math.sin(Math.toRadians(a));
}
private static double tan(double a) {
return Math.tan(Math.toRadians(a));
}
private static double makeInRange(double val, double max) {
if (val >= max)
return val - max;
else if (val < 0)
return val + max;
return val;
}
public WeatherReport getWeather(Key key) {
return reports.get(key);
}
}