Статьи

Google Weather API на Windows Phone 7

Я не совсем уверен, насколько хороши погодные приложения по умолчанию — те, что поставляются в комплекте с системой Windows Phone 7, но я всегда хотел разработать собственное приложение, которое отображало бы погоду в моем понимании. Для этого я довольно долго экспериментировал с Google Weather API . Хотя это и не задокументировано, это API, который прост в использовании и в то же время достаточно информативен.

Каждый вызов API выполняется через простой HTTP-запрос, который возвращает данные в формате XML. Сначала я думал о разработке пользовательского элемента управления, который бы отображал условия для определенного местоположения, но затем я решил просто перечислить их и передать пользовательский шаблон данных для каждого прогнозируемого дня.

Как правило, я пытался сделать пользовательский интерфейс очень простым — есть TextBlock, который будет показывать текущее местоположение, TextBox , который будет местом, где пользователь будет вводить местоположение, кнопка, которая будет запускать обновление, и ListBox, который будет содержать прогноз на ближайшие четыре дня.

Так сказать, XAML для главной страницы выглядит так:

<phone:PhoneApplicationPage
    x:Class="WeatherAlerts.MainPage" x:Name="HeadPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
    xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    FontFamily="{StaticResource PhoneFontFamilyNormal}"
    FontSize="{StaticResource PhoneFontSizeNormal}"
    Foreground="{StaticResource PhoneForegroundBrush}"
    SupportedOrientations="Portrait" Orientation="Portrait"
    mc:Ignorable="d" d:DesignWidth="480" d:DesignHeight="768"
    shell:SystemTray.IsVisible="True" Loaded="PhoneApplicationPage_Loaded">

    <Grid x:Name="LayoutRoot" Background="Transparent">
        <Grid.RowDefinitions>
            <RowDefinition Height="160"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <TextBlock  Margin="10,-15,0,0" Grid.Row="0" x:Name="PageTitle" Style="{StaticResource PhoneTextTitle1Style}"/>
        <TextBox Grid.Row="0" Margin="0,90,100,0" Name="locationBox"></TextBox>
        <Button Margin="360,90,0,0" Content="GET" Click="Button_Click"></Button>

        <ListBox Grid.Row="1" x:Name="weatherList" ItemsSource="{Binding ElementName=HeadPage,Path=Conditions}">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <Grid Height="100">
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="100"></ColumnDefinition>
                            <ColumnDefinition Width="*"></ColumnDefinition>
                        </Grid.ColumnDefinitions>

                        <Image Grid.Column="0" Height="100" Width="100" Source="{Binding Path=Icon}"></Image>
                        <TextBlock Text="{Binding Path=Day}" Grid.Column="1" Margin="10,10,10,60"></TextBlock>

                        <TextBlock Grid.Column="1" Margin="10,40,310,30" Text="LOW:"></TextBlock>
                        <TextBlock Text="{Binding Path=Low}" Grid.Column="1" Margin="70,40,250,30" FontWeight="Bold"></TextBlock>

                        <TextBlock Grid.Column="1" Margin="150,40,170,30" Text="HIGH:"></TextBlock>
                        <TextBlock Text="{Binding Path=High}" Grid.Column="1" Margin="215,40,90,30" FontWeight="Bold"></TextBlock>

                        <TextBlock Text="{Binding Path=Condition}" Grid.Column="1" Margin="10,75,10,0"></TextBlock>
                    </Grid>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </Grid>
</phone:PhoneApplicationPage>

Как видите, элемент управления ListBox привязан к коллекции. Я выбрал ObservableCollection , чтобы упростить привязку — мне не придется вручную повторно привязывать коллекцию к элементу управления. Фактическая коллекция также представлена DependencyProperty в основном классе:

public static readonly DependencyProperty _conditions = DependencyProperty.Register("Conditions", typeof(ObservableCollection<WeatherElement>), typeof(PhoneApplicationPage), new PropertyMetadata(null));
public ObservableCollection<WeatherElement> Conditions
{
    get { return (ObservableCollection<WeatherElement>)GetValue(_conditions); }
    set { SetValue(_conditions, value); }
}

Неизвестный класс здесь — WeatherElement — он используется для отображения деталей для каждого прогнозируемого дня. Класс состоит из пяти свойств, которые устанавливаются при создании экземпляра:

public class WeatherElement
{
    public BitmapImage Icon {get;set;}
    public string Day { get; set; }
    public string Low { get; set; }
    public string High { get; set; }
    public string Condition { get; set; }
}

Свойство Icon также будет установлено на основе информации из Google Weather API — с каждым условием связано изображение. Однако с изображением есть одна проблема — она ​​представлена ​​в формате GIF-файла и не поддерживается в Silverlight. Конечно, есть обходной путь для этого, и я буду обсуждать это позже в этой статье.

Но теперь давайте посмотрим на основной метод, который будет извлекать данные, необходимые для заполнения ObservableCollection :

void GetData()
{
    string location = IsolatedStorageSettings.ApplicationSettings["location"].ToString();
    var sc = SynchronizationContext.Current;

    if (!string.IsNullOrEmpty(location))
    {
        HttpWebRequest request = (HttpWebRequest)WebRequest.Create("http://www.google.com/ig/api?weather=" + location);
        request.BeginGetResponse(asyncResult =>
            {
                HttpWebResponse response = (HttpWebResponse)request.EndGetResponse(asyncResult);

                XDocument doc = XDocument.Load(response.GetResponseStream());
                if (doc.Root.Element("problem_cause") == null)
                {
                    var x = from c in doc.Root.Element("weather").Elements() where c.Name == "forecast_conditions" select c;

                    foreach (XElement element in x)
                    {
                        image = null;
                        WeatherElement welement = new WeatherElement();
                        welement.Condition = element.Element("condition").Attribute("data").Value;
                        welement.Day = element.Element("day_of_week").Attribute("data").Value;
                        welement.High = element.Element("high").Attribute("data").Value;
                        welement.Low = element.Element("low").Attribute("data").Value;

                        WebClient client = new WebClient();
                        client.OpenReadCompleted += new OpenReadCompletedEventHandler(client_OpenReadCompleted);
                        client.OpenReadAsync(new Uri("http://www.google.com/"+ element.Element("icon").Attribute("data").Value));

                        while (image == null)
                        {
                            Thread.Sleep(0);
                        }

                        welement.Icon = image;

                        sc.Post(postData =>
                        {
                            Conditions.Add(welement);
                        }, null);
                    }
                }
            }
            ,null);
        
    }
}

Как видите, я отправляю обычный веб-запрос, который обрабатывается асинхронно. Затем я получаю текущие условия с помощью LINQ, чтобы выбрать только прогнозируемые условия (поскольку в возвращенном XML-файле также присутствует другой набор данных).

Вы, наверное, заметили, что перед добавлением WeatherElement в коллекцию я создаю новый веб-запрос для получения изображения. Единственная проблема, как я уже упоминал, заключается в том, что изображение в формате GIF.

Я создал сервис WCF (см. Эту статью о том, как это сделать), который будет конвертировать GIF в JPEG. По завершении OpenReadAsync запускает следующий обработчик событий:

void client_OpenReadCompleted(object sender, OpenReadCompletedEventArgs e)
{
    byte[] imageContents;

    using (BinaryReader reader = new BinaryReader(e.Result))
    {
        imageContents = reader.ReadBytes((int)e.Result.Length);
    }

    ServiceReference1.ConverterClient client = new ServiceReference1.ConverterClient();
    client.ConvertGifToJpegCompleted += new EventHandler<ServiceReference1.ConvertGifToJpegCompletedEventArgs>(client_ConvertGifToJpegCompleted);
    client.ConvertGifToJpegAsync(imageContents);
}

Здесь я ссылаюсь на созданную мной службу WCF (ссылка на службу была добавлена ​​ранее) и вызываю еще одно асинхронное действие, которое преобразует изображение (метод зарегистрирован как часть службы):

void client_ConvertGifToJpegCompleted(object sender, ServiceReference1.ConvertGifToJpegCompletedEventArgs e)
{
    using (MemoryStream stream = new MemoryStream(e.Result))
    {
        BitmapImage image = new BitmapImage();
        image.SetSource(stream);
        this.image = image;
    }
    
}

this.image является экземпляром BitmapImage, который доступен для всего класса, объявленного в заголовке класса. Когда асинхронная операция запущена, метод GetData ожидает, пока этот BitmapImage получит значение. Если он отличается от нуля , он добавляется в экземпляр WeatherElement ( свойство Icon ) и добавляется в коллекцию, связанную с ListBox .

Теперь перейдем к более простой части приложения — фактическим вызовам методов. Когда пользователь нажимает кнопку, вы хотите сбросить коллекцию и установить для настройки приложения новое значение — новое местоположение.

private void Button_Click(object sender, RoutedEventArgs e)
{
    if (!string.IsNullOrEmpty(locationBox.Text))
    {
        PageTitle.Text = IsolatedStorageSettings.ApplicationSettings["location"].ToString();
        IsolatedStorageSettings.ApplicationSettings["location"] = locationBox.Text;
        IsolatedStorageSettings.ApplicationSettings.Save();

        Conditions = new ObservableCollection<WeatherElement>();
        GetData();
    }
    else
    {
        MessageBox.Show("No location entered.", "LOCATION", MessageBoxButton.OK);
    }
}

Это установит заголовок страницы на новое место, а также сохранит местоположение в настройках приложения. Он также сбрасывает коллекцию, восстанавливая ее. Здесь вы можете сказать, что я мог связать свойство location со свойством Text для TextBlock с заголовком. Тем не менее, я использую этот вызов только в двух местах — когда пользователь нажал кнопку GET и когда приложение загружается:

private void PhoneApplicationPage_Loaded(object sender, RoutedEventArgs e)
{
    if (IsolatedStorageSettings.ApplicationSettings["location"] != null)
    {
        PageTitle.Text = IsolatedStorageSettings.ApplicationSettings["location"].ToString();

        Conditions = new ObservableCollection<WeatherElement>();
        GetData();
    }
}

Поэтому я оставил это без прямой привязки. Когда вы запускаете приложение, вы должны получить результат, подобный этому, учитывая, что служба конвертации GIF работает и Google Weather доступна.

В этой статье я не включил текущие условия, доступные через API. Вы можете реализовать это, просто добавив еще один запрос LINQ.

Причина, по которой я добавляю дополнительный слой преобразования изображений, заключается в том, чтобы не связывать целый новый набор значков — это потребует дополнительного пространства и ресурсов. Кроме того, прямое преобразование в приложении без вызова фактической службы приведет к значительному потреблению ресурсов, особенно на устройстве, где аппаратные средства имеют очень ограниченную производительность, поэтому целесообразно делегировать преобразование внешнему ресурсу.

Для экспериментов вы можете скачать тестовый проект здесь .

Сервисный (для конвертации) исходный код можно скачать здесь .