Создаем приложение Weather App с Webix UI

Сэр Ранульф Файнс, британский исследователь и писатель, однажды сказал: «Не бывает плохой погоды, бывает только неподходящая одежда». Вы можете выглянуть в окно, чтобы посмотреть, не идет ли дождь. Но, согласитесь, это немного старомодно. В наше время все пользуются приложениями для прогноза погоды. С их помощью можно легко решить, брать ли с собой утром зонтик или нет.

Сегодня я покажу вам, как создать подобное приложение для просмотра прогноза погоды с помощью компонентов Webix. Вы сможете собрать подробную информацию о погодных условиях в вашем городе в одном месте. На первом плане будут данные о температуре, влажности, скорости ветра и УФ-индексе. Также можно будет узнать подробнее о метеорологических условиях, суточном диапазоне температур и атмосферном давлении.

Это приложение предоставляет отличную возможность показать вам несколько наших виджетов из PRO версии. Webix Pro предлагает множество HTML5 веб-виджетов, которые не включены в стандартную версию библиотеки. Все они имеют свои отличительные особенности и расширенный функционал. Наше погодное приложение также является примером интеграции с картами Google. Если вам интересно узнать о том, как написать подобное приложение, оставайтесь с нами до конца статьи.

С живым демо можно ознакомиться здесь.

Обзор приложения

Для начала давайте выберем виджеты Webix, наиболее подходящие под наши требования к отображению данных о погоде.

Хорошая визуализация очень важна. Чтобы выделить важную информацию, давайте воспользуемся двумя виджетами из PRO-версии: Bullet Graph (для УФ-индекса) и Gage (для температуры, влажности и скорости ветра). Что касается стандартных виджетов, то Combo поможет нам выбрать город из доступных вариантов. Webix GoogleMap будем использовать для переключения между городами. Наконец, информацию о давлении, суточном диапазоне температур и метеорологических условиях мы сохраним в Template.

Для получения данных о погоде и УФ-индексе мы будем использовать API OpenWeatherMap и API OpenUV (вы можете использовать любые другие API для погоды). Чтобы получить собственные API ключи, зарегистрируйтесь на сайтах openweathermap.org и openuv.io. Для использования виджета Webix GoogleMap необходимо также получить персональный API ключ Google.

В браузере наше приложение будет выглядеть следующим образом:
weather app interface

Подготавливаем почву

Как я уже говорила, мы будем использовать PRO-версию библиотеки, чтобы получить доступ к некоторым виджетам. Вы можете установить библиотеку через NPM или загрузить ее через клиентскую зону. Скачайте бесплатную пробную версию здесь, если у вас еще нет PRO лицензии.

Создаем лейаут

Небольшое напоминание: каждый компонент Webix представляет собой объект в формате JSON, который содержит все атрибуты в виде пар ключ:значение. Мы можем создать структуру приложения, расположив эти компоненты внутри друг друга. Стоит отметить, что интерфейс создается с помощью конструктора webix.ui(), который принимает объект в формате JSON с заданным лейаутом.

Интерфейс разделен на два ряда: тулбар и лейаут. В свою очередь, второй ряд разделен на три части. Первая включает в себя два столбца, заданных свойством cols: карту и список погодных условий. Вторая — для трех виджетов Gage , а третья — для BulletGraph. В коде это выглядит следующим образом:

webix.ui({
    rows: [
        bar,
        {
            rows: [{
                    cols: [
                        map,
                        mainStats,
                    ]
                },
                gages,
                UVbullet
            ]
        }
    ]
});

Для удобства каждый виджет хранится в отдельной переменной, чтобы облегчить общий интерфейс.

Теперь давайте рассмотрим, как создать необходимые компоненты для реализации наших планов.

Создаем тулбар

Для хранения списка городов мы будем использовать DataCollection — неотображаемый компонент, который хранит линейную коллекцию данных. В эту коллекцию мы загрузим данные для польских городов — массив объектов с названиями и координатами:

const pl = [{
        "city": "Warsaw",
        "lat": "52.2167",
        "lng": "21.0333",
    },
    {
        "city": "Kraków",
        "lat": "50.0614",
        "lng": "19.9372",
    }, ...
];

const cities = new webix.DataCollection({
    data: pl
});

Позже мы добавим эту DataCollection в качестве значения свойства data у combo и карты.

На тулбаре мы разместим название нашего приложения и combo с выпадающим списком городов. Сделаем три столбца: первый — лейбл с названием, второй — спейсер (чтобы отделить первый и второй столбцы друг от друга), третий — combo.

const bar = {
    view: "toolbar",
    cols: [
{
            view: "label",
            label: "Weather app"
        },
        {},
        {
            view: "combo",
            label: "Choose a city"
        }
    ]
};

Для удобства добавим фильтрацию вариантов по названиям городов. Поскольку по умолчанию combo фильтруется по свойству value, нам нужно переопределить фильтрацию. Сделаем это кастомной функцией.

{
    view: "combo",
    options: {
        filter: function(item, value) {
            const city = item.city.toString().toLowerCase();
            return city.indexOf(value.toLowerCase()) !== -1;
        },
        ...
    },
};

Создаем карту

Далее мы создадим карту, которая будет показывать все города с данными о погоде (для демо мы выбрали только 20). Задаем Webix GoogleMap, которую мы настроим так же, как и любой другой компонент:

const map = {
  view:"google-map",
  key:"AIzaSyAi0oVNVO-e603aUY8SILdD4v9bVBkmiTg", // use your own key
  id:"map",
  zoom:9,
  center:[ 52.2167, 21.0333 ],
  data: cities
};

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

Мы хотим, чтобы приложение работало следующим образом: когда пользователь кликает по маркеру, приложение выбирает этот город из combo и отображает информацию о погоде. А если пользователь выбирает город из combo, на карте отображается этот город и соответствующая информация о погоде. Для всего этого нам нужно синхронизировать карту и combo.

Чтобы установить название города,по которому кликнул пользователь, в качестве значения combo, создадим функцию selectCity(). Мы собираемся использовать этот код повторно, поэтому сделаем для этого отдельную функцию.

function selectCity(id) {
  $$("select").setValue(id);
};

Здесь мы обрабатываем событие onItemClick карты и вызываем selectCity():

$$("map").attachEvent("onItemClick", function(id){
  selectCity(id);
});

Мы также добавим код, который заставит карту показывать нужный город при выборе опции в combo, но об этом позже.

Создаем Gage

Для наглядного отображения погодных условий нам нужно создать три gage: один для температуры, один для скорости ветра и один для влажности. Для примера возьмем тот, что для скорости ветра, поскольку все они работают по одной и той же схеме:

const windGage = {
  id:"wind",
  view: "gage",
  minRange: 0,
  maxRange: 60,
  label: "Wind speed",
  placeholder: "km/h",
};

Мы устанавливаем максимальные и минимальные значения с помощью свойств maxRange/minRange. Свойство placeholder задает единицы измерения.

Затем мы помещаем все наши gage в одну переменную и организуем их в три столбца:

const gages = {
  cols: [
    windGage,
    humidityGage,
    temperatureGage
  ]
};

Создаем Bullet Graph

Переходим к Bullet Graph. Идея заключается в том, чтобы создать шкалу с диапазонами. Они будут окрашены в зеленый, желтый, оранжевый и красный цвета в зависимости от значения УФ-индекса.

const UVbullet = {
  view: "bullet",
  maxRange: 11,
  scale: {
    step: 1
  },
  bands: [
    {
      value: 11,
      color: "#f0443d"
    },
    {
      value: 7,
      color: "#F18835"
    },
    {
      value: 5,
      color: "#fec02f"
    },
    {
      value: 2,
      color: "#47c6b9"
    },
  ],
};

Мы используем свойство maxRange для установки максимального значения для индекса UV. Значение свойства bands — это массив, который содержит набор значений value и color для диапазонов.

Создаем список погодных условий

Для списка погодных условий мы будем использовать template. Когда пользователь выберет новый город, мы загрузим в него данные. Провернем небольшой трюк: в коде приложения вы не увидите стандартных HTML-тегов для таблиц, таких как <table>, <tr> or <td>. Вместо этого столбцы, строки и ячейки будут отображаться с помощью display:flex и пользовательских классов CSS. Вы можете более подробно рассмотреть код и стили в демо.

const mainStats = {
    id: "mainStats",
    template: obj => {
        if (obj.weather)
            return `<div class="row"><div class="name-col cell">City</div><div class="cell">${obj.city} (${obj.name})</div></div>
  <div class="row">
  <div class="name-col cell">Conditions</div>
  <div class="cell condition">
  <img width=32 src='http://openweathermap.org/img/w/${obj.weather[0].icon}.png'></img>
  <div class="condition-text">${obj.weather[0].main}</div>
 
  <!-- остальные строки template-->`;
    }
};

Получение данных с сервера

Переходим к тому, где мы будем получать данные о погоде.

Сначала мы должны получить API-ключ на сайте OpenWeather для того, чтобы у нас была возможность делать запросы для получения данных. Затем, используя функцию getWeather(), мы отправим AJAX-запрос с параметрами широты, долготы и API-ключа на api.openweathermap.org. Наш запрос вернет промис. Если запрос был успешным, мы получим данные, которые будут преобразованы в JSON callback-функцией. Если что-то пойдет не так, .catch() уведомит нас с помощью webix.message() внутри callback-функции для ошибок.

const apiKey = "4a667b3caefe6915603d5d48f02b50d";

function getWeather(lat, lon) {
    return webix.ajax()
        .get(`https://api.openweathermap.org/data/2.5/weather?lat=${lat}&lon=${lon}&appid=${apiKey}&units=metric`)
        .then(data => data.json())
        .catch(err => {
            webix.message("Openweather: Server side error, see console", "error");
        });
};

Запрос для УФ-индекса будет аналогичным. Обратите внимание, что наш API-ключ передается здесь в заголовке запроса, а не включается в него.

function getUV(lat, lon){
  return webix.ajax().headers({
    'x-access-token':'67fc0055b6f99e711657641dceb207f' // use your own token  }).get(`https://api.openuv.io/api/v1/uv lat=${lat}&lng=${lon}`)
    .then(data => data.json())
    .catch(err => {
    webix.message("Openuv: Server side error, see console", "error");
  });
};

Позже мы будем вызывать эти две функции для получения необходимых данных с обоих серверов.

Обратите внимание, что мы не используем реальные API запросы в нашем примере, потому что мы используем бесплатные версии API, которые имеют ограничение на количество запросов.

Реализация кэширования

Чтобы нам не приходилось посылать одни и те же запросы повторно в течение короткого промежутка времени, добавим кэширование. Например, мы можем создать коллекцию для хранения кэша:

const weatherDataCache = new webix.DataCollection({
  data: []
});

После поступления новых данных с серверов мы добавляем их в кэш. Название города будет значением свойства id, а оператор spread скопирует объект с данными:

weatherDataCache.add({  ...data, id:city });

По желанию вы можете сохранять новые данные из коллекции в session storage или local storage. Для этого используйте событие onAfterAdd:

weatherDataCache.attachEvent("onAfterAdd", function() {
  webix.storage.session.put("weather-data", this.serialize());
});

Передача данных компонентам

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

Мы уже начали синхронизировать карту и combo, и теперь пришло время для завершения. Для этого мы должны написать код, который подготовит данные о погоде и отправит их виджетам.

Событие onChange вызовет выполнение этого кода внутри функции setCityData():

{
    view: "combo",
    ...
    on: {
        onChange(cityID) {
            setCityData(cityID);
        }
    }
}

Сначала мы синхронизируем combo с картой, чтобы на ней отображался выбранный город:

function setCityData(cityID) {
        ...
    $$("map").getMap(true).then(map => {
        map.setCenter(
            new google.maps.LatLng(parseFloat(city.lat), parseFloat(city.lng))
        );
    });
        ...
}

Следующим шагом будет отправка данных о погоде виджетам.

Напомню, что ранее в статье мы создали кэш. Прежде чем посылать запросы, мы должны проверить его, так как есть вероятность, что мы уже посылали запросы для определенного города. Если это так, то мы передаем данные виджетам из кэша. Если нет, то посылаем запросы на серверы.

function setCityData(cityID) {
    ... // синхронизация карты
    const cache = weatherDataCache.getItem(city.city);
    if (cache) {
        setWeatherData(cache);
    } else {
        ...}
}

У нас есть два запроса, которые нужно послать одновременно. Поэтому мы используем webix.promise.all, чтобы дождаться завершения обоих запросов и затем начать работу с полученными данными.

{
    ... // проверка кэша
    else {
        const lat = parseFloat(city.lat);
        const lng = parseFloat(city.lng);
        webix.promise.all([
            getWeather(lat, lng),
            getUV(lat, lng)
        ]).then(data => {
            const obj = {
                ...data[0],
                uv: data[1].result.uv,
                city: city.city
            };
            setWeatherData(obj);
            setToCache(city.city, obj);
        });
    }
}

Давайте объединим оба ответа в один объект, а затем передадим этот объект виджетам. Для этого мы вызываем функцию setWeatherData(). Внутри нее мы устанавливаем значения для всех трех gage, bullet graph и списка погодных условий.

function setWeatherData(data){
  $$("temperature").setValue(Math.round(data.main.temp));
  $$("UV").setValue(data.uv);
  $$("wind").setValue(Math.round(data.wind.speed));
  $$("humidity").setValue(data.main.humidity);
  $$("mainStats").parse(webix.copy(data));
}

Следующим шагом будет передача этого же объекта в функцию setToCache(). Это действие добавит в кэш информацию о погоде для определенного города:

function setToCache(city, data){
  weatherDataCache.add({  ...data, id:city });
}

Чтобы выбрать какой-либо город с самого начала работы приложения, вызовем selectCity() после webix.ui():

selectCity(cities.getFirstId());

Вот и все! Мы настроили компоненты и связали их с данными с серверов. Теперь любой пользователь может узнать погоду в определенном городе с помощью карты или выбрать нужную из combo.

Заключение

С живым демо можно ознакомится здесь.

В этой статье мы показали, как создать погодное приложение с использованием компонентов как из Pro версии Webix, так и стандартной. Переходите по этой ссылке, чтобы узнать больше о разнице между Рro и стандартными версиями.

Если у вас возникли вопросы, поделитесь ими в комментариях ниже. Чтобы узнать больше обо всех возможностях виджетов Webix с соответствующими примерами, посетите документацию библиотеки.