unit YahooWeather;

{  Unit to retrieve Yahoo Weather Forecast

   (C)2015 Hans Luijten, http://www.tweaking4all.com
   Version 1.0

   Uses:  Synapse (http://synapse.ararat.cz/, you can just put the files in the source directory in your project directory)
          fpjson (http://www.freepascal.org/docs-html/fcl/fpjson/index.html, comes with Lazarus)
          openssl (part of synapse, weather is retrieved with https://)

   Common use:
          GetYahooForecast(location);

          Retrieves current weather and weather forecast, and returns this in a record for easy access.
          Yahoo returns weather codes, which can be translated to text with WeatherCode2String(code) as shown below.
          For English this is not needed, as Yahoo already provides this text.
          This is only usefull as an illustration, especially when you want to write your own code
          to handle multiple languages. Example code and lists are provided at the end of the code.
}

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, httpsend, ssl_openssl, fpjson, jsonparser;

type
  TDayForcast = record
    code:string;
    date:string;
    day:string;
    high:string;
    low:string;
    text:string;
  end;

  TYahooForecast = record
    success:boolean;
    distance:string;
    pressure:string;
    speed:string;
    temperature:string;
    location_city:string;
    location_country:string;
    location_region:string;
    wind_chill:string;
    wind_direction:string;
    wind_speed:string;
    atmosphere_humidity:string;
    atmosphere_pressure:string;
    atmosphere_rising:string;
    atmosphere_visibility:string;
    astronomy_sunrise:string;
    astronomy_sunset:string;
    location_latitude:string;
    location_longitude:string;
    last_updated:string;
    current_code:string;
    current_date:string;
    current_temp:string;
    current_text:string;
    forecast: array [0..9] of TDayForcast;
  end;

function GetYahooWeatherData(Location:string; AsJSON:boolean =true; TemperatureUnits:string = 'c'):string;
function GetYahooForecast(Location:string; TemperatureUnits:string = 'c'):TYahooForecast;
function YahooURLEncode(s: string): string;

implementation

{
  URLEncode

  Encodes string to be suitable as an URL.

  Example:	myencodeURL:=YahooURLEncode(myURL);
}
function YahooURLEncode(s: string): string;
var
  i: integer;
  source: PAnsiChar;
begin
  result := '';
  source := pansichar(s);
  for i := 1 to length(source) do
    if not (source[i - 1] in ['A'..'Z', 'a'..'z', '0'..'9', '-', '_', '~', '.', ':', '/']) then
      result := result + '%' + inttohex(ord(source[i - 1]), 2)
    else
      result := result + source[i - 1];
end;

{ Retrieve Yahoo Weather with forcast and place it in a record for easy access

  MyYahooForcast := GetYahooForecast('town,country',true,'c')
}
function GetYahooForecast(Location:string; TemperatureUnits:string = 'c'):TYahooForecast;
var JSONData:TJSONData;
    RawData:string;
    tmpData:TYahooForecast;
    Counter:integer;
begin
  RawData := GetYahooWeatherData(Location,true,TemperatureUnits);

  Counter:=0;
  while ((RawData='') or (pos('ERROR',RawData)>0)) and (Counter<3) do
    begin
      RawData := GetYahooWeatherData(Location,true,TemperatureUnits);
      inc(Counter);
    end;

  if ((RawData='') or (pos('ERROR',RawData)>0)) then
    tmpData.success:=false
  else
    begin
      JSONData := GetJSON(RawData);

      with tmpdata do
        begin
          success:=true;

          distance:=JSONData.FindPath('query.results.channel.units.distance').AsString;
          pressure:=JSONData.FindPath('query.results.channel.units.pressure').AsString;
          speed:=JSONData.FindPath('query.results.channel.units.speed').AsString;
          temperature:=JSONData.FindPath('query.results.channel.units.temperature').AsString;

          location_city:=JSONData.FindPath('query.results.channel.location.city').AsString;
          location_country:=JSONData.FindPath('query.results.channel.location.country').AsString;
          location_region:=JSONData.FindPath('query.results.channel.location.region').AsString;

          wind_chill:=JSONData.FindPath('query.results.channel.wind.chill').AsString;
          wind_direction:=JSONData.FindPath('query.results.channel.wind.direction').AsString;
          wind_speed:=JSONData.FindPath('query.results.channel.wind.speed').AsString;

          atmosphere_humidity:=JSONData.FindPath('query.results.channel.atmosphere.humidity').AsString;
          atmosphere_pressure:=JSONData.FindPath('query.results.channel.atmosphere.pressure').AsString;
          atmosphere_rising:=JSONData.FindPath('query.results.channel.atmosphere.rising').AsString;
          atmosphere_visibility:=JSONData.FindPath('query.results.channel.atmosphere.visibility').AsString;

          astronomy_sunrise:=JSONData.FindPath('query.results.channel.astronomy.sunrise').AsString;
          astronomy_sunset:=JSONData.FindPath('query.results.channel.astronomy.sunset').AsString;

          location_latitude:=JSONData.FindPath('query.results.channel.item.lat').AsString;
          location_longitude:=JSONData.FindPath('query.results.channel.item.long').AsString;

          last_updated:=JSONData.FindPath('query.results.channel.item.pubDate').AsString;

          current_code:=JSONData.FindPath('query.results.channel.item.condition.code').AsString;
          current_date:=JSONData.FindPath('query.results.channel.item.condition.date').AsString;
          current_temp:=JSONData.FindPath('query.results.channel.item.condition.temp').AsString;
          current_text:=JSONData.FindPath('query.results.channel.item.condition.text').AsString;

          for Counter:=0 to 9 do
            begin
              with tmpData.forecast[Counter] do
                begin
                  code:=JSONData.FindPath('query.results.channel.item.forecast['+IntToStr(Counter)+'].code').AsString;
                  date:=JSONData.FindPath('query.results.channel.item.forecast['+IntToStr(Counter)+'].date').AsString;
                  day:=JSONData.FindPath('query.results.channel.item.forecast['+IntToStr(Counter)+'].day').AsString;
                  high:=JSONData.FindPath('query.results.channel.item.forecast['+IntToStr(Counter)+'].high').AsString;
                  low:=JSONData.FindPath('query.results.channel.item.forecast['+IntToStr(Counter)+'].low').AsString;
                  text:=JSONData.FindPath('query.results.channel.item.forecast['+IntToStr(Counter)+'].text').AsString;
                end;
            end;
       end;
    end;

  GetYahooForecast := tmpData;
end;

{ Retrieve Yahoo Weather information

  myString := GetYahooWeatherData('town,country',true,'c');

  Location: typically use town,country, for example: 'osceola,wi.usa', 'amsterdam,netherlands', 'breda,nederland'
  AsJSON  : return as a JSON string if TRUE, otherwise in XML format (JSON default)
  TemperatureUnits: 'c' for Celcius, and 'f' for Fahrentheit         (C default)
}
function GetYahooWeatherData(Location:string; AsJSON:boolean =true; TemperatureUnits:string = 'c'):string;
var HTTPSender : THTTPSend;
    tmpData : TStringList;
    resultString, resultFormat: String;
begin
  if Location='' then
    GetYahooWeatherData:='ERROR: Missing Location'
  else
    begin
      if AsJSON then
        resultFormat := 'json'
      else
        resultFormat := 'xml';

      tmpData    := TStringList.Create;
      HTTPSender := THTTPSend.Create;

      try
        HTTPSender.HTTPMethod('GET',
                              'https://query.yahooapis.com/v1/public/yql?'+
                                'q=select%20*%20from%20weather.forecast%20where%20woeid%20in%20('+                           // get forecast
                                  'select%20woeid%20from%20geo.places(1)%20where%20text%3D%22'+YahooURLEncode(Location)+'%22)'+              // find location based on string
                                '%20and%20u%3D%22'+TemperatureUnits+'%22'+                                                   // force units (c or f)
                                '&format='+resultFormat+'&env=store%3A%2F%2Fdatatables.org%2Falltableswithkeys');

        if (HTTPSender.ResultCode >= 100) and (HTTPSender.ResultCode<=299) then
          tmpData.LoadFromStream(HTTPSender.Document)
      finally
        HTTPSender.Free;
      end;

      if tmpData.Text='' then
        resultString:='ERROR: Unable to retrieve data'
      else
        resultString:=tmpData.Text;

      tmpData.Free;

      GetYahooWeatherData := resultString;
    end;
end;

end.


{
    Example list and code to convert weather code to text:

const YahooWeatherCondition : array[0..48] of string = ('tornado',
                                                        'tropical storm',
                                                        'hurricane',
                                                        'severe thunderstorms',
                                                        'thunderstorms',
                                                        'mixed rain and snow',
                                                        'mixed rain and sleet',
                                                        'mixed snow and sleet',
                                                        'freezing drizzle',
                                                        'drizzle',
                                                        'freezing rain',
                                                        'showers',
                                                        'showers',
                                                        'snow flurries',
                                                        'light snow showers',
                                                        'blowing snow',
                                                        'snow',
                                                        'hail',
                                                        'sleet',
                                                        'dust',
                                                        'foggy',
                                                        'haze',
                                                        'smoky',
                                                        'blustery',
                                                        'windy',
                                                        'cold',
                                                        'cloudy',
                                                        'mostly cloudy (night)',
                                                        'mostly cloudy (day)',
                                                        'partly cloudy (night)',
                                                        'partly cloudy (day)',
                                                        'clear (night)',
                                                        'sunny',
                                                        'fair (night)',
                                                        'fair (day)',
                                                        'mixed rain and hail',
                                                        'hot',
                                                        'isolated thunderstorms',
                                                        'scattered thunderstorms',
                                                        'scattered thunderstorms',
                                                        'scattered showers',
                                                        'heavy snow',
                                                        'scattered snow showers',
                                                        'heavy snow',
                                                        'partly cloudy',
                                                        'thundershowers',
                                                        'snow showers',
                                                        'isolated thundershowers',
                                                        'not available');

    Convert Yahoo weather code to text:

       myString:=WeatherCode2String('32');


function WeatherCode2String(code:string):string;
var intCode:integer;
begin
  try
    intCode:=StrToInt(code)
  except
    intCode:=3200
  end;

  if (intCode<0) or (intCode>47) then
    WeatherCode2String:= YahooWeatherCondition[48]
  else
    WeatherCode2String:= YahooWeatherCondition[intCode]
end;
}
