Создаем Location Viewer приложение с Webix

Еще пару лет назад трудно было и представить, что удаленная работа может стать нормой повседневной жизни. Сегодня же это обычное дело. Многие сотрудники предпочитают не тратить драгоценные часы на то, чтобы добраться в час пик к своему рабочему месту в центре огромного мегаполиса.

Куда приятнее провести это время за чтением увлекательной книги, чашкой кофе и с любимым котом, сидя на балконе с видом на море. Мечта, спросите вы? Нет – современная реальность многих IT специалистов и не только.

Исходя из такой специфики работы сотрудников, у руководителей компании могут появиться дополнительные заботы. Они заключаются в том, как отслеживать местоположение многочисленного персонала и синхронизировать их рабочее расписание. И в этой статье я попробую рассказать вам о том, как можно решить подобную задачу с помощью компонентов библиотеки Webix и сторонних решений.

С кодом готового приложения и живой демкой можно ознакомиться здесь.

Location Viewer App

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

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

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

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

И для начала, мы разделим наше приложение на 2 условные части: слева будет список сотрудников и кнопки для управления картой, а справа – карта геолокации.

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

Мы определились с возможностями и интерфейсом приложения. Теперь давайте перейдем к практике и посмотрим, как реализовать все это с помощью компонентов библиотеки Webix и сторонних решений.

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

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

<script type="text/javascript" src="https://cdn.webix.com/edge/webix.js"></script>

Помимо основной библиотеки, нам нужно подключить ресурсы библиотеки OpenStreetMap, которую мы будем использовать в качестве интерактивной карты.

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

<script type="text/javascript" src="//cdn.webix.com/components/edge/openmap/openmap.js"></script>

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

А теперь давайте разберем лейаут нашего приложения. Здесь стоит отметить, что Webix использует декларативный подход в построении интерфейсов. Это значит, что каждый компонент представляет собой json объект, в котором хранятся все его основные характеристики в виде пар ключ:значение.

Вкладывая такие объекты друг в друга, мы формируем структуру интерфейса. А для его инициализации предусмотрен специальный конструктор webix.ui(), который принимает json объект с уже готовым лейаутом.

С теорией построения лейаутов мы разобрались, теперь давайте применим эти знания для создания каркаса нашего приложения.

Чуть выше мы определились, что разделим интерфейс по вертикали на 2 части. В левой мы разместим список сотрудников и контролы для управления картой, а в правом – саму карту. Разделение лейаута на столбцы реализуется с помощью свойства cols. В коде такое разделение будет выглядеть следующим образом:

webix.ui({
    cols: [
        {
            // конфигурации списка и 2 контролов
        },
        {
            // конфигурации карты
        },
    ],
});

Список сотрудников и 2 кнопки для управления элементами карты мы разместим в левом столбце в виде рядов. А за разделения лейаута на ряды отвечает свойство rows. Код нашего лейаута теперь будет выглядеть так:

webix.ui({
    cols: [
        {
            rows: [
                {...}, // конфигурации кнопки
                {...}, // конфигурации кнопки
                {...} // конфигурации списка
            ],
        },
        {
        // конфигурации карты
        },
    ],
});

Мы подключили все необходимые ресурсы и сформировали лейаут приложения. А теперь давайте перейдем к разработке соответствующих частей интерфейса и реализуем вышеописанный функционал.

Создаем список сотрудников

Исходя из того, что нам нужно отобразить список сотрудников в виде обычного списка, мы воспользуемся виджетом list. Базовая конфигурация компонента будет выглядеть следующим образом:

{
    view: "list",
    id: "list_id",
    template: "#name#, #position#",
    select: true,
    borderless: true,
    url:"./data/list_data.json"
}

Сам виджет инициализируется через свойство view. Чтобы обращаться к виджету, мы задаем ему уникальный идентификатор через свойство id.

Каждый элемент списка будет содержать имя работника и занимаемую им должность. Такой шаблон отображения определяется через свойство template, которое принимает строку с ключами соответствующих данных в качестве значения.

Помимо прочего, мы активируем селект элементов через свойство select и убираем границы виджета через свойство borderless.

И нам осталось загрузить данные, которые хранятся в отдельном json файле. Сделать это можно с помощью свойства url, указав путь к файлу в качестве значения. Стоит отметить, что для загрузки локальных данных мы бы использовали свойство data.

Список сотрудников у нас есть. Давайте заодно добавим 2 кнопки для управления картой и разместим их над списком. Конфигурация этих контролов будет выглядеть следующим образом:

{ id: "show_all_locations", view: "button", label: "Show all locations", css: "webix_primary" },
{ id: "clear_all_locations", view: "button", label : "Clear all locations" },
{ /* list */ }

Кроме свойств id и view мы определяем название каждой кнопки через свойство label, а для верхнего контрола задаем встроенные стили через свойство css. Вот и все, левая часть интерфейса у нас полностью готова. А в браузере мы увидим такой результат:

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

Для геолокации сотрудников компании, нам понадобится соответствующая карта. Для этого мы будем использовать библиотеку с открытым кодом OpenStreetMap. Этот сервис позволяет отмечать маркерами определенные точки на картах по заданным координатам и прикреплять к этим маркерам всплывающие сообщения.

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

{
    view: "open-map",
    id: "map",
    zoom: 5,
    center: central_point
}

Помимо свойств id и view, мы задаем изначальный масштаб карты через свойство zoom, а также массив с координатами центральной точки через свойство center.

В будущем, нам еще не раз придется возвращаться к исходным координатам карты, поэтому мы сохраняем их в переменную central_point:

const central_point = [ 48.679314021314354, 22.274143952644422 ];

Вот и все. Интерфейс приложения мы создали, и нам остается сделать его интерактивным.

Делаем приложение интерактивным

Большинство операций нашего приложения будут связаны со взаимодействием с картой. Исходя из этого, нам нужно отправить запрос и дождаться полной загрузки API карты. Делаем мы это с помощью метода getMap(), которому передаем параметр true. Этот параметр позволяет методу вернуть промис, который резолвится с объектом карты.

$$("map").getMap(true).then(map =&gt; {
// здесь будут описаны все операции
});

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

Настраиваем список сотрудников

Для каждого сотрудника из списка нам надо показать, где он на карте. Логично делать это по клику на элемент списка. На карте мы будем показывать маркер и попап с информацией о работнике: имя, город, страна и локальное время.

Давайте создадим отдельную функцию, которая сможет показывать всплывающее сообщение и прятать его через 3 секунды. Эта функция будет принимать объект данных сотрудника и объект маркера, для которого нужно показать попап. Код функции будет выглядеть следующим образом:

function showPopup(item, marker) {
    const time = new Date().toLocaleString("en-GB", {
        timeZone: item.time_zone,
        dateStyle: "short",
        timeStyle: "short",
    });
   
    if (marker.isPopupOpen()) {
        marker.closePopup().unbindPopup();
        clearTimeout(item.popup_timer);
        item.popup_timer = null;
    }
   
    marker.bindPopup(
        `<b>Name: </b>${item.name}
        <b>Position: </b>${item.position}
        <b>Location: </b>${item.location}
        <b>Local time: </b>${time}`,
        { closeButton: false, closeOnClick: false }
    ).openPopup();
   
    item.popup_timer = setTimeout(function () {
        marker.closePopup().unbindPopup();
    }, 3000);
   
    list.updateItem(item.id, item);
    map.setView(item.coordinates);
}

В самом начале функции мы получаем дату и локальное время сотрудника через метод toLocaleString() класса Date. Метод принимает локаль (по умолчания мы будем использовать «en-GB») и объект с конфигурациями даты и времени.

В этом объекте мы указываем временную зону, которую храним в данных каждого сотрудника под ключом time_zone, и параметры отображения даты и времени. Возвращаемое значение сохраняем в переменную time, которую будем использовать при формировании всплывающего сообщения.

Дальше мы создаем попап для маркера через его метод bindPopup(), передав ему строку с соответствующим сообщением, а затем показываем этот попап через метод openPopup().

Чуть раньше мы определили, что будем отображать сообщение только 30 секунд. Для этого нам нужно создать таймер, по истечении которого следует закрыть попап и удалить его с помощью методов closePopup() и unbindPopup().

Чтобы, при необходимости, завершить таймер досрочно, мы сохраняем его значение в объект данных сотрудника под ключом popup_timer и обновляем этот объект через метод updateItem() списка.

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

Сделать это можно через метод маркера isPopupOpen(). Если попап открыт, мы закрываем и удаляем его, а затем завершаем таймер и обнуляем его значение в данных сотрудника:

if (marker.isPopupOpen()) {
    marker.closePopup().unbindPopup();
    clearTimeout(item.popup_timer);
    item.popup_timer = null;
}

И в завершение, мы центрируем маркер для которого вызываем попап. Реализуется это с помощью метода setView(), который принимает массив с соответствующими координатами. К слову, координаты каждого сотрудника хранятся в его объекте данных под ключом coordinates.

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

list.attachEvent("onItemClick", function (id) {
    const item = list.getItem(id);
    if (item.marker) {
        showPopup(item, item.marker);
    } else {
        item.marker = L.marker(item.coordinates).addTo(map);
        item.marker.on("click", function (ev) {
            showPopup(item, ev.target);
            list.select(item.id);
        });
        showPopup(item, item.marker);
    }
});

Сначала мы получаем объект данных элемента списка, на котором сработало событие. Для этого используется метод getItem(), который принимает id соответствующего элемента. Дальше нужно проверить, существует ли маркер для данного сотрудника.

Если маркер уже существует, то нам остается только показать для него попап с помощью ранее созданной функции showPopup().

В противном случае, мы создаем новый маркер через метод L.marker(), который принимает координаты сотрудника, добавляем его на карту через метод addTo() и показываем попап с помощью той же функции showPopup().

Но и это еще не все! Нам также нужно отображать попап при клике по самому маркеру на карте. А для этого необходимо установить обработчик на событие клика по этому маркеру. Сделать это можно с помощью метода on(). Внутри обработчика мы показываем попап через функцию showPopup() и селектим соответствующего сотрудника в списке.

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

Настраиваем контролы для управления картой

Нам осталось настроить 2 контрола для управления картой. Первый контрол будет отмечать маркерами геолокацию всех сотрудников на карте, а второй – полностью очистит карту.

Давайте сначала создадим и установим обработчик для первой кнопки с названием «Show all locations». Код обработчика будет выглядеть следующим образом:

$$("show_all_locations").attachEvent("onItemClick", function () {
    list.unselectAll();
    list.data.each(function (obj) {
        if (!obj.marker) {
            obj.marker = L.marker(obj.coordinates).addTo(map);
            obj.marker.on("click", function (ev) {
                showPopup(obj, ev.target);
                list.select(obj.id);
            });
        }
    });
    map.setView(central_point);
});

Поскольку выбранные элементы списка напрямую связаны с маркерами на карте, нам нужно предварительно очистить селект любых элементов. Делается это с помощью метода unselectAll().

Дальше мы перебираем элементы нашего списка с помощью метода each() и проверяем наличие в них данных о маркерах. Если у элемента нет маркера, то мы его создаем и прикрепляем к карте.

obj.marker = L.marker(obj.coordinates).addTo(map);

После этого, нам нужно установить обработчик на событие клика по этому маркеру, чтобы отображать попап и селектить соответствующего сотрудника в списке.

obj.marker.on("click", function (ev) {
    showPopup(obj, ev.target);
    list.select(obj.id);
});

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

map.setView(central_point);

Теперь переходим к кнопке «Clear all locations». При клике по ней нам нужно очистить карту. Код ее обработчика будет выглядеть следующим образом:

$$("clear_all_locations").attachEvent("onItemClick", function () {
    list.unselectAll();
    list.data.each(function (obj) {
        if (obj.marker) {
            obj.marker.remove();
            obj.marker = null;
        }
    });
    map.setView(central_point);
});

Здесь мы проделываем похожие действия что и в предыдущем обработчике, а именно:
— сбрасываем селект элементов списка
— проходимся по всем элементам, у которых есть маркеры, удаляя эти маркеры и обнуляя их данные
— центрируем карту.

Всё готово. Теперь при клике по соответствующему контролу пользователи смогут видеть местонахождение всех сотрудников из списка, а при необходимости очищать карту.

Заключение

С кодом готового приложения и живой демкой можно ознакомиться здесь.

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

Узнать больше о возможностях интеграции сторонних расширений с виджетами библиотеки Webix вы можете в соответствующей статье документации.