Увод

Използването на пътнически железопътен транспортни в Европейският съюз нараства с около един процент всяка година [eurostat]. Търсенето на този вид транспорт от клиентите се обуславя от неговото удобство, предсказуемост и не на последно място цена. За съжаление пътническият железопътният транспорт в България посреща тази тенденция с неадекватни мерки, които според официалната статистика от Националния статистически институт, води и до намаляване на превозите на пътници всяка година фиг. 1.1 [nsi].

Превозени пътници от железопътен транспорт
Фигура 1. Превозени пътници от железопътен транспорт по години

Причините за спадът на пътникопотока са много и не винаги икономически. Голяма част от загубите на БДЖ за в следствие от недалновидна и нецеленасочена политика по управление на компанията, която все още се държи като монополист в един отворен пазар. Тази заблуда за монополно положение все още се поддържа жива от социалните пакети за пътуване: билети за възрастни граждани и учащи се, преференциалните билети за държавни служители и др. В тази ниша БДЖ наистина има традиционно силно присъствие. За съжаление тази дейност най-често е губеща дори в случаите, когато е субсидирана от държавата. Във средният и високият потребителски сегмент, компанията отстъпва на автобусните превози и личните автомобили. Причината е че тези два сегмента не са толкова чувствителни към цената на билета а определящи за техния избор са по-скоро гъвкавостта, комфорта и удобството. Удобството не се изразява само в удобството на самото пътуване а в цялостния процес на взаимодействие с компанията.

Първата стъпка от това взаимодействие е изборът и закупуването на билет за превоз. Това е най-важната стъпка в която клиентът трябва да бъде убеден да използва дадена услуга. Последващите стъпки от превозния процес са не по-малко важни но точно тук инвестицията се капитализира в конкретна покупка.

БДЖ направи опит през 2011 година да въведе хибридна система за резервация на билети чрез международните бюра "Рила"[dnevnik], но целият процес по-скоро затрудняваше клиента, отколкото да му помага. Реализацията на он-лайн система за продажба на електронни билети би дало стратегическо предимство на БДЖ в сухопътните пътнически превози. Компанията, за разлика от по-малките транспортни фирми, има необходимия обем и разнообразие на транспортни услуги, които да съставят притегателното ядро за потенциални клиенти.

Анализ на европейските практики в онлайн продажбата на електронни билети за железопътен транспорт

Фигура 2. Железопътна мрежа на Европа 2000г. [hgise]

Болшинството от европейските железопътни оператори предлагат на клиентите си възможности за закупуване и резервация на билети, он-лайн:

Таблица 1. Он-лайн присъствие на основните железопътните оператори в Европа
Държава Оператор Сайт Онлайн разписание Онлайн продажба на билети

Австрия

ÖBB

http://www.oebb.at/

да

да

Белгия

NMBS

http://www.b-rail.be/

да

да

България

БДЖ

http://www.bdz.bg/

да

не

Германия

Deutsche Bahn

http://www.bahn.com/

да

да

Гърция

OSE

http://trainose.gr/

да

да

Дания

DSB

http://www.dsb.dk/

да

да

Ирландия

CIÉ

http://www.irishrail.ie/

да

да

Испания

Renfe

http://www.renfe.com/

да

да

Италия

Trenitalia

http://www.trenitalia.com/

да

да

Люксембург

CFL

http://www.cfl.lu/

да

да

Норвегия

NSB

http://www.nsb.no/

да

да

Полша

PKP

http://rozklad-pkp.pl/

да

не

Португалия

CP

http://www.cp.pt/

да

да

Румъния

CFR

http://www.cfr.ro/

да

да

Словакия

ZSSK

http://www.zssk.sk/

да

да

Словения

http://www.slo-zeleznice.si/

да

не

Сърбия

ŽS

http://www.serbianrailways.com/

да

да

Финландия

VR

http://www.vr.fi/

да

да

Франция

SNCF

http://www.sncf.com/

да

да

Унгария

MÁV-START

http://www.mav-start.hu/

да

не

Холандия

NS

http://www.ns.nl/

да

да

Хърватска

HZ

http://www.hznet.hr/

да

не

Черна гора

ZCG

http://www.zcg-prevoz.me/

да

не

Чехия

Czech Railways

http://www.cd.cz/

да

да

Щвейцария

SBB

http://www.sbb.ch

да

да

Швеция

SJ

http://www.sj.se/

да

да

Онлайн портали на европейски железопътни оператори

Следва кратко представяне на всеки един от сайтовете:

Австрия

Сайта на ÖBB предлага следните възможности за он-лайн закупуване на билети: - електронни билети, които се разпечатват на принтер; - електронни билети с електронни карти; - електронни билети с доставка по пощата; - SMS билети при които цената се включва в сметката за мобилен телефон или се заплаща на специални терминали на гарите;

Освен различните видове билети, сайта предлага закупуването на карти за отстъпка и различни пакетни оферти за почивки и културни събития.

Белгия

Порталът предлага електронни билети за печат и такива, които се зареждат в електронни карти. Освен билети от сайта могат да бъдат закупени и ограничен брой туристически пакети.

България

Сайтът на БДЖ към момента не предлага он-лайн продажба на билети. Разписанието дава информация за някой цени на билети а информацията в него е актуална.

Германия

Deutsche Bahn предлагат електронни билети за разпечатване, MMS билети и доставка на билети по пощата. Сайта предлага също така оферти за хотели и кола под наем. Също така се предлага голямо разнообразие от карти за намаление и пътуване до международни дестинации.

Гърция

Порталът на Гърция предлага резервиране и заплащане на билети онлайн. За съжаление голяма част от сайта е само на гръцки език, което го прави неудобен за чужденци.

Дания

Датският портал, предлага възможност за използване на различни видове транспорт при избор на маршрут. Интересна възможност е показването на индекса на вредни емисии на всяко пътуване и сравнението при пътуване с автомобил. За съжаление части от сайта са само на датски и въпреки че има ръководство за използване на английски език, използването е затруднено. Закупените електронни билети се разпечатват на принтер и се предоставят за проверка в превозното средство.

Ирландия

В Ирландия се предлагат електронни билети за печат, които в някой случаи са по-евтини от редовните билети, издавани на каса.

Испания

Сайта на Renfe е изцяло на испански. Сайта предлага закупуването на електронни билети он-лайн.

Италия

Продават се електронни билети и свързани с пътуване пакетни оферти. Електронните билети се разпечатват на принтер.

Люксембург

Продават се електронни билети за пътуване до съседни държави.

Норвегия

Билети чрез смартфони. Ако изчакването при прехвърляне надхвърля 30 минути, клиентът получава компенсация. Срещу доплащане се предлагат места със електрическо захранване. Възможност за резервация на хотели. Закупените билети се използват през приложението за смартфон или се получават от автомати разположени на гарите.

Полша

В Полша се предлага само он-лайн резервация на билети чрез сайта http://www.polrail.com/, който се явява фирма посредник.

Португалия

В Португалия, могат да бъдат закупени он-лайн електронни билети за разпечатване на принтер. Интересен вариант е и закупуването на билети от банкомати.

Румъния

В Румъния, он-лайн билети могат да бъдат закупени чрез портала http://www.cfrcalatori.ro/. За закупените по този начин билети, клиентите получават отстъпка от 5% от стойността на билета.

Словакия

Има възможност за закупуване на он-лайн билети.

Словения

На клиентите е предложено само разписание без възможност за он-лайн поръчка или резервация на билети.

Сърбия

В Сърбия електронни билети могат да бъдат закупени от сайта http://w3.srbrail.rs/eticketing/ но само за влакове по определени направления. Електронните билети се разпечатват на принтер и служат като легитимен превозен документ.

Финландия

Електронните билети във Финландия могат да бъдат разпечатани или получени като SMS.

Франция

Във Франция, он-лайн билети се продават през портала http://www.voyages-sncf.com/. Билетите могат да се разпечатват на принтер или да бъдат доставени по пощата.

Унгария

Предлага се само онлайн разписание и няколко туристически оферти.

Холандия

Предлагат се електронни билети за разпечатване

Хърватска

В Хърватска не се предлага он-лайн продажба на билети.

Черна гора

На сайта на железопътни превози - Черна гора може да се открие базова информация за нормативната уредба в страната и PDF файлове с маршрутите. Он-лайн билети не се предлагат.

Чехия

Предлагат се няколко вида електронни билети, които могат да бъдат разпечатани.

Швейцария

В Швейцария се предлагат електронни билети за печат и мобилни билети за смартфони.

Швеция

Предлага се он-лайн закупуване на електронни билети.

Изисквания за он-лайн системата

След анализ на данните от порталите на националните железопътни оператори се налага изводът че една модерна система трябва да притежава всички или поне по-голямата част от следните характеристики:

Многоезичност

Немалък процент от потребителите на системи за онлайн продажба на билети са чужденци, за които в някой случаи системата е единствения достъп до транспортния пазар в страната. Поддръжката на съдържание на няколко езика е времеемко и ресурсоемко като освен това намалява гъвкавостта на информацията. Затова изборът на поддържани езици трябва да се направи на база оценка на потенциалния пазар. Почти всички портали поддържат версия на английски език, който се е наложил де факто като стандарт в Интернет

Мобилна версия

Използването на мобилни устройства бележи ръст в последните 5 години. Тази тенденция е нормална като се има предвид удобството и преносимостта на този тип устройства. Благодарение на широкото им разпространение в тази ниша се оформя все по-голям пазар. Това налага поддръжката на мобилни платформи от системата. Това може да стане по няколко различни начина.

  • основен сайт с адаптивен дизайн който да поддържа както мобилни, така и десктоп устройства. Предимствата на този метод е че се разработва и поддържа само един сайт което чувствително намалява разходите и сроковете за изработка и поддръжка. Недостатъците са че хибридния модел носи със себе си доста ограничения, които макар че в повечето случаи не са непреодолими, затрудняват разработката.

  • отделна версия за мобилни устройства. Този вариант дава свобода в разработката на двата отделни сайта, но прави поддръжката по-сложна.

  • мобилни приложения. Плюсовете на този подход са че мобилните приложения обикновено са по-бързи, прехвърлят по-малко количество данни през преносната мрежа и могат да предоставят функционалност, недостъпна през WEB браузъра. Недостатъците са фрагментираната екосистема от мобилни платформи, за да е ефективна подобна стратегия трябва да се разработят приложения поне за iOS и Android платформите, които след това трябва и да се поддържат.

  • външни разработчици. Тъй като софтуерните разработки, рядко са приоритет на железопътните оператори, задачата по разработка на мобилна версия на системата може да се отстъпи на трети лица. Най-добрият начин за това е като се предостави програмен интерфейс към системата т.нар. API, който да може да се ползва свободно от разработчици, който да разработят собствен програмен продукт на база системата.

Разнообразни методи за плащане

Успешното плащане е една от основните цели на системата, затова поддръжката на разнообразни кредитни и дебитни карти е важно за използваемостта на системата. В България възможните варианти са:

  • ePay (http://www.epay.bg)- поддържат болшинството от дебитни карти издавани в страната, изискват потребителя да има регистрация;

  • eBg (http://www.ebg.bg/) - поддържат плащане с български дебитни карти и виртуални кредитни карти;

  • EasyPay (http://www.easypay.bg/) - комбинирано онлайн задължаване и офлайн заплащане. Популярен вариант за потребители без банкови карти;

  • виртуален POS - предлага се от няколко банки, приемат се плащания с международни кредитни карти, обикновено не се изисква регистрация на потребителя;

  • банков превод - предлага се от всички банки;

  • наложен платеж - широко използван метод за плащане в България, предлага се от куриерските фирми, има приложение само при доставка на билети;

  • PayPal (http://www.paypal.com) - международен оператор на картови разплащания, удобен за използване от клиентите;

  • SMS плащания - имат фиксирана цена за обработка на транзакция така че не са ефективни за много малки суми. Изискват подписване на договори със всички оператори и разработка на обща система с тях. Удобни за потребителите, тъй като изискват само мобилен телефон без претенции към операционната му система;

От този списък трябва да се избере не само един оператор а всички рентабилни, тъй като различните потребители разполагат с различни платежни средства а и имат различни предпочитания към използваните услуги.

Превозни документи

Създаването на валидируем превозен документ е една от основните цели на системата. В практиката се използват следните варианти на документи:

  • електронен билет - един от най-лесните за издаване документи, разчита на разпечатване на генерирана от системата бланка, която включва информация за пътуването, цената на билета и служебна информация, която да удостовери валидността на документа. Най-често за проверка се използват баркодове, разпечатани на билета, които се проверяват от кондуктора. Също така могат да се използват QR кодове или прост идентификационен номер който да се въведе ръчно. За да се използва метода, кондукторите трябва да са екипирани с устройства с памет баркод четци за да могат да проверят дали билетът е уникален, издаден за този влак и тази дата на пътуван. Информацията в устройствата може да се синхронизира след приключване на резервационния период и преди потеглянето на влака.

  • мобилен билет - мобилен билет се реализира чрез специално приложение за смартфон, което в се явява приемник на данните за билета. При този вариант се избягва хартиения носител а проверката се извършва най-често чрез QR-код.

  • SMS билет - клиента получава уникален код от системата изпратен под формата на SMS, кодът се проверява ръчно от кондуктора;

  • електронни карти - обикновено магнитна картата, която идентифицира уникално клиента. При покупка на билет, системата указва че с картата може да се пътува за определена дата по дадено направление и тази информация се проверява от кондуктора;

Обвръзка с други услуги и видове транспорт

Нуждата от транспортна услуга рядко е самоцелна и много често е част от по-широки намерения на клиента. Предлагането на допълнителни услуги като резервация на хотели и наемане на автомобил както и обвръзката с останалите видове транспорт създава голяма добавена стойност към системата. Цялостното обслужване на клиента създава в него доверие а също така може да доведе до по-ниска обща цена на услугата и по-висок оборот на оператора. Този похват се използва особено широко при въздушните превозвачи но няма причина да не е приложим и при сухопътните превози.

Достъп до пълна и актуална информация

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

Бонус програми

Част от операторите предлагат намаление на цената на билета ако е закупен онлайн. Тази разлика се обуславя от по-ниските разходи за продажбата но не трябва да е единствения стимул за клиента. Бонус програми, натрупани километри пътуване, намаления за чести пътувания са само някой от вариантите за създаване на лоялни клиенти.

Проектиране и изграждане на система за он-лайн продажба на електронни билети

Системата се състои от четири функционални модула. Потребителският и административен модул са WEB базирани реализирани на езикът за програмиране PHP, като е използвана и библиотеката CodeIgniter. Системата използва WEB сървър Apache2 но би могла да работи и с nginx, lighthttpd или друг сървър, който поддържа mod_php или php-cgi. Целите, които си поставя системата е да реализира базовата функционалност необходима за успешно обслужване на клиенти. Разбира се една такава система може да се усложнява и развива почти безгранично в зависимост от конкретната обстановка и плановете на мениджмънта.

Схема на функционалните модули на системата
Фигура 3. Схема на функционалните модули на системата

Системата е изготвена от четири атомарни хетерогенни модула, които комуникират по утвърдени комуникационни протоколи избрани на база рентабилност. Модулът razpisanie е реализиран като отделен HTTP сървър на езикът Go. Комуникацията с основния сайт се извършва посредством HTTP протокол като част от данните се предават чрез JSON протокол. Комуникацията на сайта с базата данни се осъществява чрез UNIX socket или TCP socket в зависимост от настройките.

Първата стъпка в разработката е проектиране на структурата на базата данни.

База данни

Сайта използва MySQL база данни за съхранение и обработка на данните. Изборът на MySQL е обусловен от широката употреба на тази СУБД и сравнително лесното ѝ използване както от професионалисти така и от начинаещи. Системата позволява миграция и към друга СУБД, примерно PostgreSQL със сравнително малки модификации.

Данните са разпределена в следните таблици:

Схема база данни
Фигура 4. Схема на базата данни

Следва кратко описание на структурата на всяка таблица:

calendar_prices

Списък на календарните влакове.

  • id - идентификатор;

  • train_id - идентификатор на превозно средство;

  • day_of_week - ден от седмицата в PHP формат (1 - понеделник, 7 - неделя);

  • status - статус на записа;

  • created - дата и час на добавяне на записа

ci_sessions

Таблица за съхранение на потребителските сесии.

  • session_id - идентификатор на сесията;

  • ip_address - IP адрес на потребителя;

  • user_agent - User-Agent на браузъра;

  • last_activity - последна активност на потребителя;

  • user_data - потребителски данни;

distance_prices

Тарифи за разстояния.

  • id - идентификатор;

  • train_type_id - идентификатор на типа превозно средство;

  • ticket_type_id - идентификатор на типа тарифа;

  • lfrom - долна граница на интервала по разстояние;

  • lto - горна граница на интервала по разстояние;

  • klass1 - цена за билет клас 1;

  • klass2 - цена за билет клас 2;

order_status

Списък с възможни статуси на поръчка.

  • id - идентификатор;

  • name - име на статуса;

orders

Клиентски поръчки и резервации.

  • id - идентификатор;

  • user_id - идентификатор на потребител;

  • route_detail_id - идентификатор на сегмент от маршрут;

  • train_id - идентификатор на превозно средство;

  • ticket_type_id - идентификатор на тарифа;

  • date - дата на пътуване;

  • quantity - количество билети;

  • price - единична цена;

  • from_station_id - идентификатор на начална гара;

  • to_station_id - идентификатор на крайна гара;

  • class - клас билет;

  • wagon - номер на вагон (0 ако е без резервация);

  • place_num - номер на запазено място (0 ако е без резервация);

  • status - статус на поръчката;

  • created - дата и час на поръчване;

  • expires - дата и час на изтичане на резервацията;

route

Изчислен маршрут между две гари.

  • id - идентификатор;

  • from_station_id - идентификатор на начална гара;

  • to_station_id - идентификатор на крайна гара;

  • distance - разстояние (в стотици метри);

  • departure_time - час на заминаване (в минути след 0:00 часа);

  • arrival_time - час на пристигане (в минути след 0:00 часа);

  • transfers - брой прехвърляния по маршрута;

route_detail

Сегмент от изчислен маршрут.

  • id - идентификатор;

  • route_id - идентификатор на маршрут;

  • train_id - идентификатор на превозно средство;

  • from_station_id - идентификатор на начална гара;

  • to_station_id - идентификатор на крайна гара;

  • distance - разстояние (в стотици метри);

  • departure_time - час на заминаване (в минути след 0:00 часа);

  • arrival_time - час на пристигане (в минути след 0:00 часа);

station

Списък гари.

  • id - идентификатор;

  • name - име на гарата;

  • latitude - географска ширина;

  • longitude - географска дължина;

  • sorder - ред на сортиране;

  • status - статус на записа;

  • created - дата и час на създаване на записа;

ticket_type

Списък тарифи.

  • id - идентификатор;

  • name - име на тарифата;

  • sorder - ред на сортиране;

train

Превозни средства.

  • id - идентификатор;

  • name - име на превозното средство;

  • train_type_id - тип на превозното средство;

  • from_station_id - идентификатор на начална гара;

  • to_station_id - идентификатор на крайна гара;

  • status - статус на превозното средство;

  • departure_time - час на заминаване (в минути след 0:00 часа);

  • arrival_time - час на пристигане (в минути след 0:00 часа);

  • created - дата и час на създаване на записа;

train_station

Точки от маршрута на влак.

  • id - идентификатор;

  • train_id - идентификатор на превозно средство;

  • station_id - идентификатор на гара;

  • travel_time - време за пътуване (в минути);

  • departure_time - час на заминаване (в минути след 0:00 часа);

  • arrival_time - час на пристигане (в минути след 0:00 часа);

  • distance - разстояние (в стотици метра);

  • sorder - хронологичен ред;

  • created - дата и час на създаване на записа;

train_type

Списък на типове превозни средства.

  • id - идентификатор;

  • name - кратко име на типа;

  • long_name - описание на типа;

  • status - статус;

  • created - дата и час на създаване на записа;

train_wagon

Подвижен състав на превозните средства.

  • id - идентификатор;

  • date - дата на състава (0 ако е базов);

  • train_id - идентификатор на превозно средство;

  • wagon_type_id - идентификатор на тип вагон;

  • sorder - пореден номер;

train_wagon_pricelist

Конфигурации за комбинации влак/вагон/тарифа.

  • id - идентификатор;

  • is_calendar - календарен (0-не, 1-да);

  • train_type_id - идентификатор на тип превозно средство;

  • wagon_type_id - идентификатор на тип вагон;

  • ticket_type_id - идентификатор на тарифа;

user

Регистрирани потребители.

  • id - идентификатор;

  • email - e-mail адрес на потребителя;

  • password - парола в криптиран формат;

  • name - име на потребителя;

  • phone - телефон на потребителя;

  • admin - администраторски права (0 -не, 100 - да);

  • status - статус на профила;

  • created - дата и час на създаване на записа;

wagon_type

Видове вагони.

  • id - идентификатор;

  • name - име на вагона;

  • long_name - описание на вагона;

  • reservation - възможност за резервация на място (0-не, 1-да);

  • seats - брой места във вагона;

  • passenger - пътнически? (0-не, 1-да);

  • layout - схема на местата във вагона;

  • status - статус на вагона;

  • created - дата и час на създаване на записа;

Попълване на БД от изходните данни

Част от изходните данни са в електронен формат, което ги прави идеални кандидати за автоматичен импорт в системата.

Разписание

Файлът 2010-TIMETABL.TXT съдържа информация за движението на всички влакове в текстов формат. Кодировката на файла е windows-1251, затова първо преобразуваме текста в utf-8 кодировка която е де-факто стандарт в модерните мултиезични системи. За целта използваме служебната програма iconv:

$iconv -f cp1251 -t utf8 2010-TIMETABL.TXT > timetable.txt

Използваме конзолен PHP скрипт за обработка на текста, като резултата от скрипта е SQL скрипт, който директно може да се изпълни на SQL сървъра:

Скрипта обработва файла за разписание и генерира SQL скрипт от него:

import.php
<?php
mb_internal_encoding("UTF-8");
class Import {

  var $in_train = FALSE;
  var $train = array();
  var $stations = array();
  var $trains = array();
  var $train_types = array();

  function do_import($file_name){
    if (!file_exists($file_name)) {
      die('File '.$file_name.' not found!'.PHP_EOL);
    }
    $handle = fopen($file_name, 'r');
    if ($handle) {
      while (($buffer = fgets($handle, 4096)) !== false) {
        $this->processLine($buffer);
      }
      if (!feof($handle)) {
        echo "Error: unexpected fgets() fail\n";
      }
      fclose($handle);
    }
    if ($this->train) {
      $this->saveTrain();
    }
    //print_r($this->trains);
  }

  function processLine($line) {
    if (trim($line)) {
      if ($line[0]!= ' ') {
        if ($this->train) {
          $this->saveTrain();
        }
        $this->train = explode(' ', $line);
        //echo 'header'.$line.PHP_EOL;
      } else {
        $station['len'] = mb_substr($line, 0, 7);
        $station['speed'] = (int)mb_substr($line, 8, 4);
        $station['name'] = trim(mb_substr($line, 12, 15));
        $station['minutes'] = trim(mb_substr($line, 28, 2));
        $station['minutes2'] = mb_substr($line, 31, 2);
        $station['arrival_time'] = mb_substr($line, 34, 5);
        $station['wait'] = mb_substr($line, 40, 3);
        $station['departure_time'] = mb_substr($line, 44, 5);
        $station['unknown'] = mb_substr($line, 50, 4);
        $this->train['stations'][] = $station;
      }
    }
  }

  function getElement(&$array, $name) {
    $index = array_search($name, $array);
    if ($index === FALSE) {
      $array[] = $name;
      return count($array)-1;
    }
    return $index;
  }

  function timeToMinutes($time) {
    if ($time) {
      list($min, $sec) = explode(':', trim($time));
      return $sec+($min*60);
    }
    return 0;
  }

  function saveTrain() {
    $train = array(
      'train_type' => $this->getElement($this->train_types, $this->train[0]),
      'name' => $this->train[1],
    );
    $train['stations'] = array();
    foreach($this->train['stations'] as $station) {
      $name = mb_convert_case($station['name'], MB_CASE_TITLE);

      $train['stations'][] = array(
        'station_id' => $this->getElement($this->stations, $name),
        'time' => $station['minutes']?$station['minutes']:0,
        'arrival_time' => $this->timeToMinutes($station['arrival_time']),
        'departure_time' => $this->timeToMinutes($station['departure_time']),
        'len' => $station['len']*10,
      );
    }
    $this->trains[] = $train;
  }

  function generateSQL(){
    echo "SET names utf8;\n";
    foreach($this->stations as $id => $station) {
      echo sprintf("INSERT INTO station (id, name) VALUES (%d, '%s');\n", $id+1, $station);
    }
    foreach($this->train_types as $id => $tt) {
      echo sprintf("INSERT INTO train_type (id, name) VALUES (%d, '%s');\n", $id+1, $tt);
    }
    foreach($this->trains as $train_id => $train) {
      $from_id = $train['stations'][0]['station_id'];
      $to_id = $train['stations'][count($train['stations'])-1]['station_id'];
      $departure_time = $train['stations'][0]['departure_time'];
      $arrival_time = $train['stations'][count($train['stations'])-1]['arrival_time'];
      echo sprintf("INSERT INTO train (id, name, train_type_id, from_station_id,
        to_station_id, departure_time, arrival_time) VALUES (%d, '%s', %d, %d, %d, %d, %d);\n",
        $train_id+1, $train['name'], $train['train_type']+1, $from_id+1,
        $to_id+1, $departure_time, $arrival_time);
            $num = 1;
      foreach ($train['stations'] as $station) {
        echo sprintf("INSERT INTO train_station (train_id, station_id, travel_time,
          arrival_time, departure_time, distance, sorder) VALUES (%d, %d, %d, %d, %d, %d, %d);\n",
          $train_id+1, $station['station_id']+1, $station['time'],
          $station['arrival_time'], $station['departure_time'], $station['len'], $num);
                $num++;
      }
    }
  }
}

$file_name = '../../_data/timetable.txt';
$i = new Import();
$i->do_import($file_name);
$i->generateSQL();

Информацията за подвижния състав се намира във файла 2010-TABLE1.RTF. Директната обработка на Rich Text Format е затруднена, затова първо експортваме файла в HTML формат, който след това изчистваме от HTML таговете и преобразуваме до файла processed.txt, съдържащ информацията за всеки влак на един ред. Файлът се обработва от скрипта wagons.php, който отново генерира SQL скрипт, готов за изпълнение на MySQL сървъра:

wagons.php
<?php
mb_internal_encoding("UTF-8");
$re = '/([\d])([A-ZАВ]+)/u';
$lokre = '/(лок[\d]+)/u';

$text = file_get_contents('../../_data/vagoni/processed.txt');

$arr = explode(PHP_EOL, $text);

$res = array();

$wagons = array();

function fix($t) {
  return str_replace(
    array('А', 'В'),
    array('A', 'B'),
    $t
  );
}

foreach ($arr as $row) {
  $t = explode('|', $row);
  if (isset($t[1])) {
      $train_num = $t[1];
      $tr = array();
      if (preg_match($lokre, $row, $m)) {
        $lok = $m[1];
        if (!in_array($lok, $wagons)) {
          $wagons[] = $lok;
        }
        $tr[$lok] = 1;
      }
      if (preg_match_all($re, $row, $m, PREG_SET_ORDER)) {
        foreach ($m as $mrow) {
          $w = fix($mrow[2]);
          if (!isset($tr[$w])) {
            $tr[$w] = $mrow[1];
          }
          if (!in_array($w, $wagons)) {
            $wagons[] = $w;
          }
        }
//        print_r($tr);
      }
      $res[$train_num] = $tr;
  }
}

sort($wagons);
//print_r($wagons);

foreach ($wagons AS $id => $row) {
  printf("INSERT INTO wagon_type (id, name) VALUES (%d, '%s');".PHP_EOL, ($id+1), $row);
}

foreach ($res as $train_num => $lwagons) {
  $n = 1;
  foreach ($lwagons as $type => $cnt) {
    for ($i = 1; $i <= $cnt; $i++) {
      $ndx = array_search($type, $wagons);
      printf("INSERT INTO train_wagon (train_id, wagon_type_id, sorder)
        VALUES (COALESCE((SELECT id FROM train WHERE name='%s'), 0), %d, %d);".PHP_EOL,
        $train_num, $ndx+1, $n);
      $n++;
    }
  }
}

?>

Останалите данни: тарифната информация, GPS координати на гарите и подробностите за подвижния състав не беше достъпна в електронен вид, затова е заложена ръчно в SQL скрипта populate.sql, който е с големина над 2000 реда.

Целият процес по изграждане и попълване на данните в MySQL е автоматизиран чрез следния Makefile:

Makefile
DBHOST = localhost
DBUSER = username
DBDB = database
DBPASS = password

all: clean_database create_database populate

create_database:
    mysql -u $(DBUSER) -h $(DBHOST) --password=$(DBPASS) $(DBDB) < create.sql

clean_database:
    mysql -u $(DBUSER) -h $(DBHOST) --password=$(DBPASS) $(DBDB) < clean.sql

populate:
    php import.php | mysql -u $(DBUSER) -h $(DBHOST) --password=$(DBPASS) $(DBDB)
    php wagons.php | mysql -u $(DBUSER) -h $(DBHOST) --password=$(DBPASS) $(DBDB)
    mysql -u $(DBUSER) -h $(DBHOST) --password=$(DBPASS) $(DBDB) < populate.sql

Процесът се стартира чрез командата make при което последователно се извършват следните задачи:

  • унищожаване на всички таблици в базата данни (ако съществуват);

  • създаване на всички таблици в базата данни;

  • стартиране на import.php и изпълнение на резултата от скрипта на MySQL сървъра.

  • стартиране на wagons.php и изпълнение на резултата от скрипна на MySQL сървъра.

  • изпълнение на populate.sql на MySQL сървъра.

Задачите по унищожаване на данните, създаване на таблиците и попълване на данните могат да се стартират и индивидуално по желание.

Разписание

Модулът razpisanie изпълнява една от двете основни цели на системата, да изготви близки до оптималния маршрути между две гари или спирки. Изискването към модула е да изпълнява търсенето възможно най-бързо и точно.

Железопътната система може да се представи като симетричен насочен граф където всяка гара е връх от графа а всеки маршрут между две гари като насочена дъга с тегло разстоянието между двете гари по маршрута:

Примерен граф
Фигура 5. Примерен граф на железопътна мрежа

Има множество алгоритми за намиране на пътища в граф но в случая ще използваме модификация на метода описан в DRUMURI OPTIME ÎN GRAFURI ORAR[cozac].

Входящите данни за модула са списък със следното съдържание:

  • идентификатор на превозно средство;

  • име на превозно средство;

  • идентификатор на гара;

  • име на гара;

  • час на пристигане;

  • час на заминаване;

  • разстояние от началната точка на маршрута;

Часовете на пристигане и заминаване са представени чрез брой минути от 0:00 часа а разстоянието е в стотици метри. Целта на тези преобразувания е да използваме цели числа като по този начин спестяваме памет и изчислително време. За разделител между колоните използваме стойността на константата SEPARATOR, дефинирана в constants.go, в случая вертикална черта "|";

Опростени данни за движение по осма линия
120|7620|1|София|0|435|0
120|7620|215|Враца|542|544|1015
120|7620|341|Видин|725|0|2652
121|7621|341|Видин|0|365|0
121|7621|215|Враца|528|530|1637
121|7621|1|София|635|0|2653
122|7622|1|София|0|745|0
122|7622|215|Враца|868|870|1036
122|7622|341|Видин|1060|0|2673
124|7623|341|Видин|0|765|0
124|7623|215|Враца|948|950|1637
124|7623|1|София|1075|0|2673
125|7624|1|София|0|985|0
125|7624|215|Враца|1082|1084|1015
125|7624|341|Видин|1250|0|2652
126|7625|341|Видин|0|968|0
126|7625|215|Враца|1165|1167|1637
126|7625|1|София|1275|0|2653
127|7630|1|София|0|1150|0
127|7630|215|Враца|1269|1271|1036
127|7630|350|Лом|1377|0|2031
128|7631|350|Лом|0|335|0
128|7631|215|Враца|451|454|995
128|7631|1|София|588|0|2031

При стартиране на модула се изпълнява функцията StartServer:

func StartServer() {
  // Създаване на нова глобална променлива за данните
  razpisanie = NewRazpisanie()
  // Зареждане на входящите данни
  razpisanie.LoadFromFile("data/paths.txt")
  // Обработка на данните
  razpisanie.Process()

  log.Println("Server operational")
  // Дефиниране на услугите на сървъра
  http.Handle("/route", http.HandlerFunc(routeHandler))
  http.Handle("/distance", http.HandlerFunc(distanceHandler))
  // Стартиране на сървъра
  log.Fatal(http.ListenAndServe(":8080", nil))
}

Razpisanie е основната функционална структура на модула. Затова променлива от тип Razpisanie е дефинирана като глобална за целия модул.

// Основна функционална структура
type Razpisanie struct {
  tempList   TempList   // Временен списък за данните при зареждане
  stations   *Dictionary  // Речник на гарите
  trains     *Dictionary  // Речник на превозните средства
  floydGraph FloydGraph // Матрица на теглата
  dfsGraph   DFSGraph   // Разширен граф
}

Зареждането на входящите данни се осъществява от функцията LoadFromFile:

// Зареждане на данните от файл
func (raz *Razpisanie) LoadFromFile(file_name string) {
  // Отваряне на файла за четене
  f, err := os.Open(file_name)
  if err != nil {
    // В случай на грешка прекъсваме работата на модула
    panic(err)
  }
  // Заявка за затваряне на файла след приключване на работа с него
  defer f.Close()

  // Обект за буферирано четене от файлов дескриптор
  reader := bufio.NewReader(f)

  line := 1 // Брояч на редове
  for {
    // Прочитане на ред от файла
    rline, _, err := reader.ReadLine()

    if err != nil {
      break // Приключване на четенето
    }
    // Разделяне на реда на елементи
    array := strings.Split(string(rline), SEPARATOR)

    train_id, _ := strconv.Atoi(array[0]) // Идентификатор на превозно средство
    train_name := array[1]  // Име на превозно средство
    station_id, _ := strconv.Atoi(array[2]) // Идентификатор на гара
    station_name := array[3]  // Име на гара

    var row TRow  // Нов ред-контейнер за временния списък

    row.arrival, _ = strconv.Atoi(array[4]) // Час на пристигане
    row.departure, _ = strconv.Atoi(array[5]) // Час на заминаване
    row.length, _ = strconv.Atoi(array[6]) // Дължина пътуването от началната точка

    // Добавяне на името и идентификатора на гара в речника за гари и
    // указване на вътрешния идентификатор във реда-контейнер
    row.station = raz.stations.Add(station_id, station_name)
    // Добавяне на името и идентификатора на превозно средство в речника за превозни
    // средства и указване на вътрешния идентификатор във реда-контейнер
    row.train = raz.trains.Add(train_id, train_name)

    // Добавяне на реда-контейнер във временния списък
    raz.tempList = append(raz.tempList, &row)
    line++ // Увеличаване на брояча на редове
  }
  log.Printf("Loaded %d lines from \"%s\"\n", line, file_name)
}

Съхраняваме имената и идентификаторите на гарите и превозните средства в два речника за да използваме по-малко памет за съхранението на данните. Това също така опростява инициализирането на структурите за търсене в последствие. Данните вече са налични но не са в удобен за употреба формат. Обработката им се поема от функцията Process:

// Обработка на временния списък
func (raz *Razpisanie) Process() {
  // Създаване на структурата за матрица на теглата
  (*raz).floydGraph = *(NewFloydGraph((*raz).stations.Len()))
  // Създаване на структурата за разширен граф
  (*raz).dfsGraph = *(NewDFSGraph((*raz).stations.Len()))

  // Инициализация на променливите
  var (
    last_station  int = -1
    last_train    int = -1
    last_distance     = 0
    distance          = 0

    node1 *DFSNode = nil
    node2 *DFSNode = nil
  )


  // Обработка на входящите данни
  for _, row := range (*raz).tempList {
    if row.length == 0 {
      last_distance = 0
    }
    distance = row.length - last_distance
    //Floyd
    if last_station != -1 && row.train == last_train {
      if row.length == 0 {
        last_distance = 0
      }
      // Добавяме разстоянието в матрицата на теглата
      (*raz).floydGraph.Add(last_station, row.station, distance)
      last_distance = row.length
    }
    //DFS
    if row.arrival != 0 {
      // Добавяме нов връх от тип "пристигане"
      node1 = (*raz).dfsGraph.AddTrainStation(row.train, row.station, row.arrival, ARRIVAL)
    } else {
      node1 = nil
    }
    if node1 != nil && node2 != nil {
      // Добавяме дъга за продължаване на пътуването
      node2.addArc(node1, distance, CONTINUE)
    }
    if row.departure != 0 {
      // Добавяме нов връх от тип "заминаване"
      node2 = (*raz).dfsGraph.AddTrainStation(row.train, row.station, row.departure, DEPARTURE)
    } else {
      node2 = nil
    }
    if node1 != nil && node2 != nil {
      // Добавяме дъга за продължаване на пътуването
      node1.addArc(node2, 0, CONTINUE)
    }

    last_station = row.station
    last_train = row.train
  }
  // Обработка на матрица на теглата
  (*raz).floydGraph.Resolve()
  // Обработка на разширен граф
  (*raz).dfsGraph.Init()
}

За реализация на търсенето се използват два алгоритъма от теория на графите: алгоритъм на Флойд-Уоршал[nakov] и търсене в дълбочина на граф[nakov]

Алгоритъмът на Флойд-Уоршал в случая се използва за определяне на минималните разстояния между всеки две точки в граф. Дефинираме матрица, представена от двумерен масив (в конкретната имплементация на езика Go използваме отрязъци за да използваме оптимално количество памет):

type FloydGraph [][]int

Инициализираме матрицата с размер броят на гарите и запълваме всички клетки със константната стойност INFINITY, дефинирана във файла constants.go.

// Създаване на нова матрица на теглата и инициализиране
// на всички клетки с INFINITY
func NewFloydGraph(num_stations int) *FloydGraph {
  // създаване на първото измерение на матрицата
  fg := make(FloydGraph, num_stations)
  for x := range fg {
    // създаване на второто измерение
    fg[x] = make([]int, num_stations)
    for y := range fg[x] {
      // инициализиране на клетките
      fg[x][y] = INFINITY
    }
  }
  return &fg
}

Добавяме всички известни разстояния между гарите:

  // Обработка на входящите данни
  for _, row := range (*raz).tempList {
    if row.length == 0 {
      last_distance = 0
    }
    distance = row.length - last_distance
    //Floyd
    if last_station != -1 && row.train == last_train {
      if row.length == 0 {
        last_distance = 0
      }
      // Добавяме разстоянието в матрицата на теглата
      (*raz).floydGraph.Add(last_station, row.station, distance)
      last_distance = row.length
    }

    // ....

    last_station = row.station
    last_train = row.train
  }

И стартираме стартираме самият алгоритъм:

// връщаме по-малкото от две цели числа
func MinInt(v1, v2 int) int {
  if v1 <= v2 {
    return v1
  }
  return v2
}

// Определяне на минимални пътища между върхове на граф
// по алгоритъм на Флойд-Уоршал
func (fg *FloydGraph) Resolve() {
  n := len(*fg) // Размерността на матрицата
  for k := 0; k < n; k++ {
    for i := 0; i < n; i++ {
      for j := 0; j < n; j++ {
        if i == j {
          (*fg)[i][j] = 0 // Разстоянието от гарата до самата нея винаги е 0
        } else {
          (*fg)[i][j] = MinInt((*fg)[i][j], (*fg)[i][k]+(*fg)[k][j])
        }
      }
    }
  }
}

Състоянието на матрицата преди и след обработката са дадени съответно в Таблица 2 и Таблица 3:

Таблица 2. Матрица на теглата преди изчисляване на разстоянията
София Враца Видин Лом

София

999999

1036

999999

999999

Враца

1036

999999

1637

995

Видин

999999

1637

999999

999999

Лом

999999

995

999999

999999

стойностите са в стотици метри

Таблица 3. Матрица на теглата след изчисляване на разстоянията
София Враца Видин Лом

София

0

1036

2673

2031

Враца

1036

0

1637

995

Видин

2673

1637

0

2632

Лом

2031

995

2632

0

стойностите са в стотици метри

След обработката на матрицата, можем да достъпваме разстоянията чрез функцията GetDistance

// Разстояние между гари x и y
func (fg *FloydGraph) GetDistance(x, y int) int {
  return (*fg)[x][y]
}

За търсене в дълбочина на граф изграждаме разширен граф с цел ускоряване на търсенето на маршрути. Графът е представен от следните структури:

// Разширен граф
type DFSGraph []*NodeColumn

// Колона на гара
type NodeColumn []*DFSNode

// Връх
type DFSNode struct {
  station int     // гара
  train   int     // влак
  time    int     // време(минути)
  ntype   bool    // тип на върха
  arcs    []*DFSArc   // списък дъги
}

// Дъга
type DFSArc struct {
  next   *DFSNode   // връх
  length int      // тегло (дължина)
  atype  bool     // тип на дъгата
}

Графът се състои от колони като всяка една колона представлява гара. Във всяка колона са подредени по време върховете а всеки връх съдържа в себе си списък с връзки към други върхове от графа:

Разширен граф
Фигура 6. Разширен граф на движението по осма линия

На фигура 6, нормалното движение на влаковете е представено с плътни стрелки а трансферните дъги са означени с пунктир.

Разширен граф ще наричаме такъв насочен граф за който всеки връх е комбинация от гара, влак и час а всяка дъга свързва два върха с тегло равно на разстоянието между гарите на двата върха.

Точките биват два вида: на пристигане и на потегляне, представени от една булева променлива: ntype със стойности дефинирани във файла constants.go:

const (
  // ...
  DEPARTURE = true  // заминаване
  ARRIVAL   = false // пристигане
  // ...
)

Дъгите също биват два вида: движение на влака и трансферни също представени от една булева променлива: atype със стойности дефинирани във файла constants.go:

const (
  // ...
  CONTINUE = true   // движение на влака
  TRANSFER = false  // трансфер
  // ...
)

При създаване на разширеният граф, създаваме колоните, като всяка от тях ще съдържа върхове са една и съща гара:

// Създаване на нов разширен граф
func NewDFSGraph(num_stations int) *DFSGraph {
  dfs := make(DFSGraph, num_stations)
  for i := 0; i < num_stations; i++ {
    // Създаване на колона за всяка гара
    dfs[i] = NewNodeColumn()
  }
  return &dfs
}

Информацията в разширения граф се попълва паралелно с тази в матрицата на теглата, при обработката на входящите данни:

  // Обработка на входящите данни
  for _, row := range (*raz).tempList {
    if row.length == 0 {
      last_distance = 0
    }
    distance = row.length - last_distance

    // ....

    if row.arrival != 0 {
      // Добавяме нов връх от тип "пристигане"
      node1 = (*raz).dfsGraph.AddTrainStation(row.train, row.station, row.arrival, ARRIVAL)
    } else {
      node1 = nil
    }
    if node1 != nil && node2 != nil {
      // Добавяме дъга за продължаване на пътуването
      node2.addArc(node1, distance, CONTINUE)
    }
    if row.departure != 0 {
      // Добавяме нов връх от тип "заминаване"
      node2 = (*raz).dfsGraph.AddTrainStation(row.train, row.station, row.departure, DEPARTURE)
    } else {
      node2 = nil
    }
    if node1 != nil && node2 != nil {
      // Добавяме дъга за продължаване на пътуването
      node1.addArc(node2, 0, CONTINUE)
    }

    last_station = row.station
    last_train = row.train
  }

Използваме два метода за запълване на графа:

добавяне на нов връх:

// Създаване на нов връх
func NewNode(station, train, time int, ntype bool) *DFSNode {
  return &DFSNode{station, train, time, ntype, nil}
}

//Добавяне на връх в графа
func (dfs *DFSGraph) addNode(station int, node *DFSNode) {
  nc := (*dfs)[station]
  nnc := append(*nc, node)
  (*dfs)[station] = &nnc
}

// Добавяне на връх
func (dfs *DFSGraph) AddTrainStation(train int, station int, time int, ntype bool) *DFSNode {
  node := NewNode(station, train, time, ntype)  //Създаваме нов връх
  dfs.addNode(station, node) // Добавяме върхът в графа
  return node
}

и добавяне на дъга свързваща два върха:

// Създаване на нова дъга
func NewArc(next *DFSNode, length int, atype bool) *DFSArc {
  return &DFSArc{next, length, atype}
}

// Добавяне на дъга между два върха
func (node *DFSNode) addArc(next *DFSNode, length int, atype bool) {
  // Добавя нов дъга към списъка с дъги на текущия връх
  node.arcs = append(node.arcs, NewArc(next, length, atype))
}

След запълването на графа с върхове и дъгите за движение на влака, предстои изграждането на трансферните дъги:

// Проверка за възможен трансфер между влакове
func (node *DFSNode) canTransferTo(next *DFSNode) bool {
  return node.ntype == ARRIVAL && // Влак 1 пристига
    next.ntype == DEPARTURE && // Влак 2 заминава
    node.train != next.train && // Влак 1 <> Влак 2
    next.time-node.time >= MIN_TRANSFER_TIME && // Времето между двете събития е по-голямо или равно на минималното трансферно време
    next.time-node.time <= MAX_TRANSFER_TIME // Времето между двете събития е по-малко или равно на максималното трансферно време
}

// Инициализиране на трансферните дъги
func (dfs *DFSGraph) Init() {
  for _, nc := range *dfs {
    sort.Sort(nc) // Сортираме върховете във всяка гара по време
    c := nc.Len()
    for i, node := range *nc {
      for j := i; j < c; j++ {
        // Ако е възможен трансферът между два върха
        if node.canTransferTo((*nc)[j]) {
          // Създаваме трансферна дъга
          node.addArc((*nc)[j], 0, TRANSFER)
        }
      }
    }
  }
}

За сортиране на всяка колона с върхове използваме вграденият алгоритъм на езика Go като сме дефинирали интерфейса за сортиране по следния начин:

// Брой елементи в колоната
func (nc *NodeColumn) Len() int {
  return len(*nc)
}

// Компаратор
func (nc *NodeColumn) Less(i, j int) bool {
  return (*nc)[i].time < (*nc)[j].time // Сравняваме по време
}

// Разменяне на местата на два върха
func (nc *NodeColumn) Swap(i, j int) {
  (*nc)[i], (*nc)[j] = (*nc)[j], (*nc)[i]
}

След като сме сигурни че върховете са сортирани. За всеки връх проверяваме дали между него и някой от следващите върхове е възможно да се осъществи трансфер. Ако такъв е възможен създаваме трансферна дъга между двата върха.

Целта на използването на разширен граф е да преизчислим възможно най-много условия в процеса на инициализация на структурата, за да можем да изпълняваме по-малко операции след това в търсенето на пътища.

На този етап сме извършили зареждането и инициализацията на данните и сме ги подготвили за употреба. Ред е на стартирането на WEB сървъра:

func StartServer() {
  // Създаване на нова глобална променлива за данните
  razpisanie = NewRazpisanie()
  // Зареждане на входящите данни
  razpisanie.LoadFromFile("data/paths.txt")
  // Обработка на данните
  razpisanie.Process()

  log.Println("Server operational")
  // Дефиниране на услугите на сървъра
  http.Handle("/route", http.HandlerFunc(routeHandler))
  http.Handle("/distance", http.HandlerFunc(distanceHandler))
  // Стартиране на сървъра
  log.Fatal(http.ListenAndServe(":8080", nil))
}

Сървърът предлага две услуги route и distance през HTTP протокол на порт 8080;

Услугата distance връща минималното разстояние между две гари от графа. Тъй като имаме изчислени всички разстояния от обработката на матрицата на теглата с алгоритъмът на Флойд-Уоршал, реализацията на услугата е тривиална:

// Реализация на услугата distance
func distanceHandler(w http.ResponseWriter, r *http.Request) {
  // Идентификатор на начална гара
  fromid, _ := strconv.Atoi(r.FormValue("from"))
  // Идентификатор на крайна гара
  toid, _ := strconv.Atoi(r.FormValue("to"))
  // Вътрешен идентификатор на начална гара
  from := razpisanie.stations.FindId(fromid)
  // Вътрешен идентификатор на крайна гара
  to := razpisanie.stations.FindId(toid)

  log.Printf("Distance from: %d\tto: %d", from, to)
  // Разстоянието се получава директно от матрицата на теглата
  p := razpisanie.FindDistance(from, to)
  // Връщане на резултата към клиента
  fmt.Fprint(w, p)
}

func (raz *Razpisanie) FindDistance(from, to int) int {
  return (*raz).floydGraph.GetDistance(from, to)
}

Услугата route предоставя оптималния и близки до оптималния (ако са възможни) маршрути между две гари. Услугата се реализиран от следната функция:

// Реализация на услугата route
func routeHandler(w http.ResponseWriter, r *http.Request) {
  // Идентификатор на начална гара
  fromid, _ := strconv.Atoi(r.FormValue("from"))
  // Идентификатор на крайна гара
  toid, _ := strconv.Atoi(r.FormValue("to"))
  // Вътрешен идентификатор на начална гара
  from := razpisanie.stations.FindId(fromid)
  // Вътрешен идентификатор на крайна гара
  to := razpisanie.stations.FindId(toid)

  log.Printf("Search from: %d\tto: %d", from, to)
  // Търсене на маршрути
  p := razpisanie.FindRoutes(from, to)
  // Изграждане на JSON формат на резултата
  fmt.Fprint(w, Encode(p))
}

За успешна работа, изисква подаването на два параметъра чрез POST заявка: - from - идентификатор на начална гара; - to - идентификатор на крайна гара;

Двата идентификатора се преобразуват във вътрешни идентификатори и се подават на функцията FindRoutes:

// Търсене на близки до оптималния маршрути
func (raz *Razpisanie) FindRoutes(from, to int) *Paths {
  // Минимално разстояние между две гари
  min_distance := (*raz).floydGraph.GetDistance(from, to)
  var p *Paths = nil // Нов контейнер за маршрути
  // Започва проверката от маршрути без прехвърляне като се увеличават възможните
  // прехвърляния с единица докато не се получи възможен по смисъла на ограниченията
  // маршрут(и)
  for max_transfers := 0; max_transfers <= MAX_TRANSFERS; max_transfers++ {
    // Търсене в дълбочина на разширен граф
    p = (*raz).dfsGraph.FindRoutes(from, to, int(float64(min_distance)*DISTANCE_COEF), &(*raz).floydGraph, max_transfers)
    // В случай на резултат търсенето се прекратява
    if len((*p).paths) > 0 {
      // Връщане на откритите резултати
      return p
    }
  }
  return p
}

Тъй като в общия случай, маршрутите между две върха на графа са много, като в конкретния случай малка само част от тях са рентабилни, поставяме няколко ограничаващи условия на търсенето:

  • възможно най-малко прехвърляния между превозни средства. Започваме проверките от пътуване без прехвърляне и увеличаваме броя на прехвърлянията с единица докато не получим възможен маршрут. За налагането на това ограничение изхождаме от презюмцията че прехвърлянията са неудобни за пътника и болшинството клиенти при предоставени два маршрута с еднакви други параметри биха предпочели този с по-малко прехвърляне;

Максималният брой прехвърляния е константа, дефинирана в constants.go:

  MAX_TRANSFERS = 4 // Максимален брой прехвърляния
  • дължина на маршрута по-малка или равна на минималната дължина между двете гари умножена по коефициент за разстояние:

  DISTANCE_COEF = 1.2 // Коефициент на разстояние

В този случай приемаме, че маршрути с дължина 20% над минималната, все още са рентабилни за пътника;

Търсенето в дълбочина на граф е алгоритъм измислен още през 19 век като метод за откриване на пътища в лабиринт. Алгоритъмът е рекурсивен и се състои в последователно обхождане на всички дъги на даден връх докато не се достигне до желания краен връх.

Ще използваме модификация на алгоритъмът с употреба на стек за да можем да използваме едни и същи данни за едновременно търсене от много клиенти:

// Търсене в дълбочина на граф
func (p *Paths) FindWay(node *DFSNode, end_station, length, transfers, max_transfers int) {
  // При достигане на крайната гара
  if node.station == end_station {
    // Добавяне на крайната гара в стека
    p.stack.Push(NewStackNode(node, nil))
    // Копиране на стека в списъка с маршрути
    p.AddPath()
    // Изкарване на последната гара от стека
    _ = p.stack.Pop()
  } else {
    // Обхождане на всички дъги за върха
    for _, arc := range node.arcs {
      // Ако дъгата не е крайна т.е последна гара
      if arc.next != nil {
        // Забранява прехвърлянето за начална гара
        if length == 0 && arc.atype == TRANSFER {
          continue
        }
        // Разстоянието от текущата гара до крайната гара
        now := (*p).floyd.GetDistance(node.station, end_station)
        // Разстоянието от следващата гара до крайната гара
        next := (*p).floyd.GetDistance(arc.next.station, end_station)
        // Проверка дали следваща гара ни доближава до целта,
        // дали разстоянието не надхвърля максималното разстояние,
        // дали броят прехвърляния не надхвърля максималният брой прехвърляния
        if now >= next && arc.length+length <= (*p).max_distance && transfers <= max_transfers {
          // Добавяме следващия връх и дъгата до него в стека
          p.stack.Push(NewStackNode(node, arc))
          // Ако дъгата е прехвърляне, увеличаваме броят на прехвърлянията
          add := 0
          if arc.atype == TRANSFER {
            add = 1
          }
          // Викаме търсене в дълбочина с новия връх
          p.FindWay(arc.next, end_station, length+arc.length, (transfers + add), max_transfers)
          // Изваждаме последния елемент от стека
          _ = p.stack.Pop()
        }
      }
    }
  }
}

В стека съхраняваме комбинация от указатели към връх и дъга до него за не губим информация за разстоянието между два върха, което се носи в дъгите.

type StackNode struct {
  node *DFSNode // Връх
  arc  *DFSArc  // Дъга
}

// Създаване на нов елемент за стека
func NewStackNode(node *DFSNode, arc *DFSArc) *StackNode {
  return &StackNode{node, arc}
}

В списъка с маршрутите съхраняваме копие на текущия стек:

// Добавяне на стек към списъкът с маршрути
func (p *Paths) AddPath() {
  //Създаване на копие на текущия стек
  ns := (*p).stack.Copy()
  p.paths = append(p.paths, ns)
}

След приключване на търсенето, структурата Paths съдържа списък с откритите маршрути между началната и крайна гара:

type Paths struct {
  max_distance int      // Максимално разстояние
  floyd        *FloydGraph  // Матрица на теглата
  stack        *Stack     // Текущ стек
  paths        []*Stack   // Списък маршрути
}

Така получените данни са в суров вид, съдържат пълните пътища между двете точки. За целите на сайта имаме нужда само от от интервалите между значещите гари (начална, крайна и трансферна). Данните се подготвят за клиента от функцията Encode като се ползват експортни структури чрез които резултатът автоматично се конвертира във JSON формат:

// Експортна структура
type JSONStack []JSONPath

// Маршрут
type JSONPath struct {
  From_Station_Id int       `json:"from_station_id"`
  To_Station_Id   int       `json:"to_station_id"`
  Distance        int       `json:"distance"`
  Transfers       int       `json:"transfers"`
  Departure_Time  int       `json:"departure_time"`
  Arrival_Time    int       `json:"arrival_time"`
  Details         []JSONDetail  `json:"details"`
}

// Сегмент от маршрута
type JSONDetail struct {
  Train_Id        int       `json:"train_id"`
  From_Station_Id int       `json:"from_station_id"`
  To_Station_Id   int       `json:"to_station_id"`
  Distance        int       `json:"distance"`
  Departure_Time  int       `json:"departure_time"`
  Arrival_Time    int       `json:"arrival_time"`
}
// Подготвяне на маршрутите за връщане към клиента
func Encode(p *Paths) string {
  // Създаване и запълване нова експортна структура
  js := p.CreateJSONStack(razpisanie.stations, razpisanie.trains)
  // Преобразуване на структурата в JSON запис
  b, err := json.Marshal(js)
  if err != nil {
    log.Println(err)
    b = []byte("{'err': 'Marshal Failure'}")
  }
  // Връщане на JSON текста
  return string(b)
}

// Създаване и запълване нова експортна структура
func (p *Paths) CreateJSONStack(stations, trains *Dictionary) JSONStack {
  var js JSONStack
  for _, s := range p.paths {
    // Добавяне към експортната структура на всеки маршрут
    js = append(js, s.DumpJSON(stations, trains))
  }
  return js
}

// Експорт на заглавна част на маршрута
func (s *Stack) DumpJSON(stations, trains *Dictionary) JSONPath {
  var jp JSONPath
  // Начална гара
  jp.From_Station_Id = (*stations)[(*s)[0].node.station].id
  // Крайна гара
  jp.To_Station_Id = (*stations)[(*s)[len(*s)-1].node.station].id
  // Разстояние и брой прехвърляния
  jp.Distance, jp.Transfers = s.GetDistanceAndTransfers()
  // Час на потегляне за маршрута
  jp.Departure_Time = (*s)[0].node.time
  // Час на пристигане за маршрута
  jp.Arrival_Time = (*s)[len(*s)-1].node.time
  // Експорт на сегментите на маршрута
  jp.Details = (*s).DumpRouteDeatilsJSON(stations, trains)
  return jp
}

// Експорт на сегментите на маршрута
func (s *Stack) DumpRouteDeatilsJSON(stations, trains *Dictionary) []JSONDetail {
  var jds []JSONDetail
  var jd JSONDetail
  jd.Distance = 0     // Разстояние за сегмента
  jd.To_Station_Id = 0  // Крайна гара за сегмента
  jd.Arrival_Time = 0   // Час на пристигане за сегмента
  // Идентификатор на влак за сегмента
  jd.Train_Id = (*trains)[(*s)[0].node.train].id
  // Начална гара за сегмента
  jd.From_Station_Id = (*stations)[(*s)[0].node.station].id
  // Час на потегляне за сегмента
  jd.Departure_Time = (*s)[0].node.time
  // Обхождане на всички елементи на маршрута
  for _, path := range *s {
    // Промяна на крайната гара
    jd.To_Station_Id = (*stations)[path.node.station].id
    // Промяна на часа на пристигане
    jd.Arrival_Time = path.node.time
    // Ако има следващ връх
    if path.arc != nil {
      // Добавяме разстоянието на дъгата към сегмента
      jd.Distance += path.arc.length
      // Ако дъгата е за прехвърляне
      if path.arc.atype == TRANSFER {
        // Добавяме сегмента към резултата
        jds = append(jds, jd)
        // Инициализираме променливите за новия сегмент
        jd.Distance = 0
        jd.Train_Id = (*trains)[path.node.train].id
        jd.From_Station_Id = (*stations)[path.node.station].id
        jd.Departure_Time = path.node.time
      }
    }
  }
  // Добавяме последния сегмент
  jds = append(jds, jd)
  return jds
}

Използвайки така дефинирания граф, можем да приложим алгоритъм за обхождане в дълбочина с ограничаващи условия за да получим възможните маршрути. Самият алгоритъм по същността си е рекурсивно обхождане на дъгите на всяка точка до достигане на целта:

func (p *Paths) FindWay(node *DFSNode, end_station, length, transfers, max_transfers int) {
  if node.station == end_station {
    p.stack.Push(NewStackNode(node, nil))
    p.AddPath()
    _ = p.stack.Pop()
  } else {
    for _, arc := range node.arcs {
      if arc.next != nil {
        // Без прехвърляне на първа станция
        if length == 0 && arc.atype == TRANSFER {
          continue
        }
        // Избор на дъга само ако ни приближава към целта
        now := (*p).floyd.GetDistance(node.station, end_station)
        next := (*p).floyd.GetDistance(arc.next.station, end_station)
        if now >= next && arc.length+length <= (*p).max_distance && transfers <= max_transfers {
          p.stack.Push(NewStackNode(node, arc))
          add := 0
          if arc.atype == TRANSFER {
            add = 1
          }
          p.FindWay(arc.next, end_station, length+arc.length, (transfers+add), max_transfers)
          _ = p.stack.Pop()
        }
      }
    }
  }
}

В класическата реализация на обхождането, пътят се пази във върховете но в случая използваме стек за да постигнем многонишкова работа с данните, т.е. да можем да обработим няколко конкурентни заявки едновременно.

Модулът работи като API сървър, отворен за заявки от потребителската част на сайта. Цялостния модел на работа на модула е представен на фигура 7:

Обща схема на модул разписание
Фигура 7. Обща схема на работа на модул razpisanie

Модулът е реализиран на езикът Go (http://golang.org).

Потребителски модул

Потребителският модул на системата е предназначен за крайните клиенти на превозната услуга. За създаването му са използвани следните технологии:

Модулът позволява на потребителя следните възможности:

  • Търсене на маршрути между всяка двойка гари/спирки от разписанието за определена дата (Приложение: фиг. 1);

  • Избор на подходящ маршрут от списък (Приложение: фиг. 2);

  • Преглед на гарите/спирките, през които преминава превозното средство (Приложение: фиг. 8);

  • Преглед на превозните средства, които преминават през дадена гара/спирка (Приложение: фиг. 9);

  • Резервация на различни видове билети и избор на запазено място в превозното средство, където това е възможно (Приложение: фиг. 3);

  • Възможност за заплащане на резервираните билети онлайн (Приложение: фиг. 7).

  • Възможност за регистрация в портала от където да се следят всички закупени от клиента билети (Приложение: фиг. 5, фиг. 6);

Основният процес на монетизация на сайта е чрез закупуването на превозни документи. Това се осъществява чрез следния процес:

Закупуване на билет
Фигура 8. Процес на избор и закупуване на електронен билет за пътуване

Административен модул

Административният модул дава възможност за манипулация на следните елементи:

Гари/спирки

Всяка гара или спирка в системата се представя от име и географски координати (Приложение: фиг. 10 и фиг. 11). Координатите се използват при показване на информация за гара както и за маршрутите на влаковете.

Влакове

Администрацията позволява въвеждането на нови влакове, както и редакцията на вече съществуващи (Приложение: фиг. 12, фиг. 13). Всеки влак се характеризира с име и тип.

Маршрути на влакове

Маршрутът на всеки влак се състои от поредица от име на гара, час на пристигане, час на заминаване и разстояние от началото на маршрута. Маршрутите могат да бъдат допълвани и променяни. (Приложение: фиг. 15)

Подвижен състав на влак

Административен модул

Позволява указване на състава на влака. Тази информация се използва при продажбата на билети (Приложение: фиг. 14).

Превозни средства

Тази администрация позволява добавяне и редактиране на елементите на подвижния състав. Всяко превозно средство се описва с код, име, брой места за пътници и схема, която се показва при избора на запазени места (Приложение: фиг. 18).

Синтаксиса за дефиниране на схемата на превозното средство е:

  • " " - коридор;

  • "–" или "|" - стена;

  • "1" или "2" - съответен клас място;

Тарифи

Позволява редакция на тарифните коефициенти за превоз. (Приложение: фиг. 17)

Освен промяната на данни, администрацията дава достъп до няколко основни статистически справки:

  • Минимално разстояние между две гари (получава се от матрицата на теглата на модул razpisanie) (Приложение: фиг. 19);

  • Натоварване на гари (Приложение: фиг. 20);

  • Участъкова скорост по отсечки за влак (Приложение: фиг. 16);

  • Месечни приходи за влак;

Възможно е добавянето на допълнителни справки при необходимост.

Ефективност на системата

Така изготвената примерна система позволява бързо и лесно закупуване на желаните билети от потребителя. За проверка на ефективността на търсене на маршрути от модула razpisanie използваме следния скрипт:

benchmark.php
<?php

function test($from, $to) {
  $url = 'http://localhost:8080/route';

  $post = sprintf('from=%d&to=%d', $from, $to);

  $ch = curl_init();

  curl_setopt($ch,CURLOPT_URL,$url);
  curl_setopt($ch,CURLOPT_POST, 2);
  curl_setopt($ch,CURLOPT_POSTFIELDS,$post);
  curl_setopt($ch,CURLOPT_RETURNTRANSFER, true);
  $result = curl_exec($ch);
  curl_close($ch);
  return $result;
}

for ($i=0; $i< 1000; $i++) {
  $from = rand(1,700);
  $to = rand(1,700);
  $time_start = microtime(true);
  test($from,$to);
  echo microtime(true) - $time_start.PHP_EOL;
}
?>

Скриптът извършва 1000 търсения на маршрути между две произволни гари. Резултатите от теста се виждат на фигура 9:

Време за търсене на маршрути между произволни гари
Фигура 9. Време за търсене на маршрути между произволни гари

Средното време за търсене е 1.4 милисекунди като в това време се включва и времето за HTTP комуникация.

По отношение на първоначалното стартиране на сървъра, времето от стартиране до пълна оперативна готовност е 4 секунди:

2012/07/03 14:41:28 Loaded 12552 lines from "data/paths.txt"
2012/07/03 14:41:32 Server operational

Тестовете са проведени на конфигурация:

Процесор

Intel® Core™ i3 CPU 3.20GHz

Памет

8 GB

Операционна система

Debian Wheezy

Ядро

linux-image-3.2.0-2-686-pae #1 SMP Mon Jun 11 18:27:04 UTC 2012 i686 GNU/Linux

Компилатор

go1.0.1

Изводи и препоръки

Так описаната система подлежи на развитие постоянно развитие и модификация. Едно допълнително приложение на системата би могло да бъде интегрирането и в т.нар. киоск системи. Примерна конфигурация на такава система би могла да бъде окомплектована с АТМ функционалност както и с фискален принтер за директен печат на билети.

Друго направление за употреба на системата би могло да бъде интегрирането и със сродни системи за други видове транспорт. Това ще добави още по голяма стойност към услугата. Възвращаемостта от подобна система може да надхвърли в пъти инвестицията направена за изработка или закупуване на подобна система. Оперативните разходи по системата са минимални и голяма част от тях се правят и при сега наличия портал.

Принципна схема на интегрирана система за предоставяне на транспортни услуги
Фигура 10. Принципна схема на интегрирана система за предоставяне на транспортни услуги

Една такава децентрализирана система би комуникирала с отделните системи на доставчиците на транспортни услуги чрез унифициран програмен интерфейс (API), който минимално би трябвало да включва команди за:

  • маршрути между две точки за интервал от време;

  • свободни позиции за маршрут;

  • възможност за резервация на позиция;

Ползите от подобни системи са:

  • удобство и предсказуемост за клиентите;

  • унификация на критериите;

  • по-добри условия за конкуренция;

  • по-лесно "продаване" на транспортната услуга;

  • възможности за оптимизация и планиране;

Информационни системи се използват в транспорта и днес. Това което се забравя обикновено е че една информационна система никога не е завършена, защото тя трябва да отразява реалния свят, който винаги се променя.

Използвана литература

Списък с използваната литература

Книги
  • [nakov] Наков, Преслав, Основи на компютърните алгоритми, Top Team Co., 1999

Публикации