При разработке проекта для транспортной компании, занимающейся пассажирскими перевозками, возникла задача реализовать свой аналог Google Calendar для встраивания внутрь системы.
В силу некоторых причин(глубокая интеграция в проект, связь с кучей разных сущностей, полный контроль над всеми частями кода и прочее), использовать решение от Google было нерационально со многих точек зрения.
Итак, условия задачи:
- Интерфейс должен быть максимально приближен к интерфейсу от Google (т.к. до этого использовали его)
- Нормальная реализация RFC 2445, его части касательно RRULE (паттернов повторения)
- Быстрая скорость просчета дат событий (в данном случае рейсов) и их рендер в браузере
- Максимальное использование существующих библиотек для уменьшения потраченного времени .
Если тема интересна или вам есть что сказать, т.к. работа еще ведется и этот пост затрагивает лишь малую часть — прошу под кат, буду рад осмысленным советам.
Первоначально, перед детализацией, решено было определить общую стратегию работы данного решения. Попытка использовать ruby и gem ice_cube провалилась, т.к. уже довольно большое количество кода было написано на php и часть проекта выносить в другой язык программирования не очень кошерно. Ну и проблемы с производительностью (или моя неопытность работы с RoR).
Как итог, после размышлений, родилось следующее:
- За визуализацию в виде календаря будет ответственен немного доработанный jQuery плагин FullCalendar
- Создание повторяющихся рейсов возложено на Scheduler.js из набора FuelUX
- Хранение в БД паттернов повторения будет реализовано в виде «FREQ=DAILY;INTERVAL=2;UNTIL=20130130T230000Z;» для уменьшения размера БД (т.к. если хранить каждый рейс отдельно и окончание повторений не назначено количество отдельных рейсов стремится в бесконечность)
- Конвертация паттернов повторения в набор дат рейсов будет реализовано на стороне клиента для разгрузки серверных мощностей
Первое, что необходимо будет реализовать — получение RRULE, конвертация в набор дат и рендер с помощью плагина FullCalendar. После непродолжительного поиска было найдено следующее решение для конвертации — rrule.js работающее как в браузере, так и в качестве приложения на node.js, что в дальнейшем предоставляет возможность перенести на сервер с клиента.
Примерный путь ясен — приступим. Предупреждаю сразу, разрабатывается прототип и код пишется исходя из скоростных показателей, а не качественных.
Допустим у нас имеется json массив RRULE правил с полями имя, длина и сам паттерн повторений. Его получение с бекенда мы опустим.
[{
"name": "Reccurence Event #1",
"length": "120",
"rrule": "DTSTART=20020201T083000Z;FREQ=WEEKLY;WKST=MO;BYDAY=WE,FR"
},
{
"name": "Reccurence Event #2",
"length": "120",
"rrule": "FREQ=MONTHLY;DTSTART=20000201T083000Z;WKST=MO;BYDAY=TU"
},
{
"name": "Reccurence Event #3",
"length": "120",
"rrule": "FREQ=DAILY;DTSTART=20000201T063000Z;WKST=MO;BYDAY=MO,FR"
}
]
Инициализируем массивы и создаем из строк RRULE объекты плагина rrule.js:
var data = private_env.get_data();
var rules = new Array();
var occurs = new Array();
for (var k in data){
rules.push(
{
name: data[k].name,
length: data[k].length,
rrule: RRule.fromString(data[k].rrule)
});
}
Получаем из наших объектов список дат для каждого рейса, где DATE_START и DATE_END соответствуют началу и концу прмоежутка, за который нам необходимо их получить:
for (var k in rules){
occurs.push(
{
name: rules[k].name,
length: rules[k].length,
occurs: rules[k]['rrule'].beetween(DATE_START,DATE_END)
});
}
Очищаем календарь перед рендером:
$calendar.fullCalendar('removeEvents', function (event){
return true;
});
И выводим наши рейсы на экран:
for (var k in occurs){
for (var i in occurs[k].occurs){
var event = {
id: k,
title: occurs[k].name,
start: occurs[k].occurs[i],
/* Не элегантно высчитываем конец рейса по длине в минутах, но пусть */
end: new Date(occurs[k].occurs[i].getTime()+(1000*60*occurs[k].length)),
allDay: false
};
/* Фикс renderEvent, чтобы не запускал рендер каждое добавление рейса, т.к. регрессия производительности */
$calendar.fullCalendar('renderEvent', event, 1, 0);
}
}
/* Рендерим весь стек рейсов, отвечает последняя 1 за это */
this.fullCalendar('renderEvent',{allDay: false}, 0, 1);
Все вышенаписанное мы оборачиваем в одну функцию, к примеру render(DATE_START,DATE_END) и вызываем при событии viewRender плагина FullCalendar:
...
viewRender: function(view, element){
$(private_env.env_self).service('render', 'between', view.visStart, view.visEnd);
}
...
В данный момент у нас получилась примерно следующая картина:
Превращать пост в простыни из кода не вижу смысла, думаю общая идея ясна. Внешний вид и прочее доработать труда не составляет.
Пару слов о создании и редактировании.
Большое количество событий в FullCalendar позволяет нам реализовать функционал редактирования. Необходимый функционал:
- При клике открывается форма редактирования рейса
- При перетаскивании, ресайзе выдаем запрос на варианты действий. Редактирование отдельно взятого рейса, редактирование всей цепочки повторений, редактирование будущих повторений
Задача тривиальна и сводится к тому, чтобы навешать функций на события eventClick, eventDrop, eventResize. Кстати, у двух последних имеется возможность отмены действий:
...
revertFunc();
...
Единственный нюанс. При выборе «Изменять только отдельный рейс» мы режем «RRULE FREQ=MONTHLY;DTSTART=20000201T083000Z;WKST=MO;BYDAY=TU» на три разных паттерна.
То что было до рейса этого, сам текущий рейс и то, что будет после. Решается это изменением опций в правиле DTSTART и UNTIL.
Что касается создания рейсов — Scheduler.js может выдавать на выход строку повторений согласно RFC 2445, что и необходимо нам:
...
$('#myScheduler').scheduler('value')
...
Дорабатывать данную солянку из плагинов еще предстоит довольно долго, но направление куда двигаться ясно.
И спасибо вам за внимание, если имеется критика или предложения по улучшению — прошу в студию и буду только рад.
Автор: Valeriy_tw3eX