Initial commit

This commit is contained in:
Daniel Friesel 2018-09-02 21:41:33 +02:00
commit 21643b053d
37 changed files with 3263 additions and 0 deletions

604
index.pl Normal file
View file

@ -0,0 +1,604 @@
#!/usr/bin/env perl
use Mojolicious::Lite;
use Cache::File;
use DateTime;
use DBI;
use List::Util qw(first);
use Travel::Status::DE::IRIS;
use Travel::Status::DE::IRIS::Stations;
our $VERSION = qx{git describe --dirty} || 'experimental';
my $cache_iris_main = Cache::File->new(
cache_root => $ENV{TRAVELYNX_IRIS_CACHE} // '/tmp/dbf-iris-main',
default_expires => '6 hours',
lock_level => Cache::File::LOCK_LOCAL(),
);
my $cache_iris_rt = Cache::File->new(
cache_root => $ENV{TRAVELYNX_IRISRT_CACHE} // '/tmp/dbf-iris-realtime',
default_expires => '70 seconds',
lock_level => Cache::File::LOCK_LOCAL(),
);
my $dbname = $ENV{TRAVELYNX_DB_FILE} // 'travelynx.sqlite';
my %action_type = (
checkin => 1,
checkout => 2,
undo => -1
);
app->defaults( layout => 'default' );
app->attr(
add_station_query => sub {
my ($self) = @_;
return $self->app->dbh->prepare(
qq{
insert into stations (ds100, name) values (?, ?)
}
);
}
);
app->attr(
add_user_query => sub {
my ($self) = @_;
return $self->app->dbh->prepare(
qq{
insert into users (name) values (?)
}
);
}
);
app->attr(
checkin_query => sub {
my ($self) = @_;
return $self->app->dbh->prepare(
qq{
insert into user_actions (
user_id, action_id, station_id, action_time,
train_type, train_line, train_no, train_id,
sched_time, real_time,
route, messages
) values (
?, $action_type{checkin}, ?, ?,
?, ?, ?, ?,
?, ?,
?, ?
)
}
);
},
);
app->attr(
checkout_query => sub {
my ($self) = @_;
return $self->app->dbh->prepare(
qq{
insert into user_actions (
user_id, action_id, station_id, action_time,
train_type, train_line, train_no, train_id,
sched_time, real_time,
route, messages
) values (
?, $action_type{checkout}, ?, ?,
?, ?, ?, ?,
?, ?,
?, ?
)
}
);
}
);
app->attr(
dbh => sub {
my ($self) = @_;
return DBI->connect( "dbi:SQLite:dbname=${dbname}", q{}, q{} );
}
);
app->attr(
get_all_actions_query => sub {
my ($self) = @_;
return $self->app->dbh->prepare(
qq{
select action_id, action_time, stations.ds100, stations.name,
train_type, train_line, train_no, train_id,
sched_time, real_time,
route, messages
from user_actions
join stations on station_id = stations.id
where user_id = ?
order by action_time asc
}
);
}
);
app->attr(
get_last_actions_query => sub {
my ($self) = @_;
return $self->app->dbh->prepare(
qq{
select action_id, action_time, stations.ds100, stations.name,
train_type, train_line, train_no, train_id, route
from user_actions
join stations on station_id = stations.id
where user_id = ?
order by action_time desc
limit 10
}
);
}
);
app->attr(
get_userid_query => sub {
my ($self) = @_;
return $self->app->dbh->prepare(
qq{select id from users where name = ?});
}
);
app->attr(
get_stationid_by_ds100_query => sub {
my ($self) = @_;
return $self->app->dbh->prepare(
qq{select id from stations where ds100 = ?});
}
);
app->attr(
get_stationid_by_name_query => sub {
my ($self) = @_;
return $self->app->dbh->prepare(
qq{select id from stations where name = ?});
}
);
app->attr(
undo_query => sub {
my ($self) = @_;
return $self->app->dbh->prepare(
qq{
insert into user_actions (
user_id, action_id, action_time,
) values (
?, $action_type{undo}, ?
)
}
);
},
);
sub epoch_to_dt {
my ($epoch) = @_;
return DateTime->from_epoch(
epoch => $epoch,
time_zone => 'Europe/Berlin'
);
}
sub get_departures {
my ( $station, $lookbehind ) = @_;
$lookbehind //= 20;
my @station_matches
= Travel::Status::DE::IRIS::Stations::get_station($station);
if ( @station_matches == 1 ) {
$station = $station_matches[0][0];
my $status = Travel::Status::DE::IRIS->new(
station => $station,
main_cache => $cache_iris_main,
realtime_cache => $cache_iris_rt,
lookbehind => 20,
datetime => DateTime->now( time_zone => 'Europe/Berlin' )
->subtract( minutes => $lookbehind ),
lookahead => $lookbehind + 20,
);
return {
results => [ $status->results ],
errstr => $status->errstr,
station_ds100 =>
( $status->station ? $status->station->{ds100} : 'undef' ),
station_name =>
( $status->station ? $status->station->{name} : 'undef' ),
};
}
elsif ( @station_matches > 1 ) {
return {
results => [],
errstr => 'Ambiguous station name',
};
}
else {
return {
results => [],
errstr => 'Unknown station name',
};
}
}
helper 'checkin' => sub {
my ( $self, $station, $train_id ) = @_;
my $status = get_departures($station);
if ( $status->{errstr} ) {
return ( undef, $status->{errstr} );
}
else {
my ($train)
= first { $_->train_id eq $train_id } @{ $status->{results} };
if ( not defined $train ) {
return ( undef, "Train ${train_id} not found" );
}
else {
my $success = $self->app->checkin_query->execute(
$self->app->get_user_id,
$self->get_station_id(
ds100 => $status->{station_ds100},
name => $status->{station_name}
),
DateTime->now( time_zone => 'Europe/Berlin' )->epoch,
$train->type,
$train->line_no,
$train->train_no,
$train->train_id,
$train->sched_departure->epoch,
$train->departure->epoch,
join( '|', $train->route ),
join( '|',
map { ( $_->[0] ? $_->[0]->epoch : q{} ) . ':' . $_->[1] }
$train->messages )
);
if ( defined $success ) {
return ( $train, undef );
}
else {
return ( undef, 'INSERT failed' );
}
}
}
};
helper 'checkout' => sub {
my ( $self, $station, $force ) = @_;
my $status = get_departures( $station, 180 );
my $user = $self->get_user_status;
my $train_id = $user->{train_id};
if ( $status->{errstr} and not $force ) {
return $status->{errstr};
}
if ( not $user->{checked_in} ) {
return 'You are not checked into any train';
}
else {
my ($train)
= first { $_->train_id eq $train_id } @{ $status->{results} };
if ( not defined $train ) {
if ($force) {
my $success = $self->app->checkout_query->execute(
$self->app->get_user_id,
$self->get_station_id(
ds100 => $status->{station_ds100},
name => $status->{station_name}
),
DateTime->now( time_zone => 'Europe/Berlin' )->epoch,
undef, undef, undef, undef, undef,
undef, undef, undef
);
if ( defined $success ) {
return;
}
else {
return 'INSERT failed';
}
}
else {
return "Train ${train_id} not found";
}
}
else {
my $success = $self->app->checkout_query->execute(
$self->app->get_user_id,
$self->get_station_id(
ds100 => $status->{station_ds100},
name => $status->{station_name}
),
DateTime->now( time_zone => 'Europe/Berlin' )->epoch,
$train->type,
$train->line_no,
$train->train_no,
$train->train_id,
$train->sched_departure->epoch,
$train->departure->epoch,
join( '|', $train->route ),
join( '|',
map { ( $_->[0] ? $_->[0]->epoch : q{} ) . ':' . $_->[1] }
$train->messages )
);
if ( defined $success ) {
return;
}
else {
return 'INSERT failed';
}
}
}
};
helper 'get_station_id' => sub {
my ( $self, %opt ) = @_;
$self->app->get_stationid_by_ds100_query->execute( $opt{ds100} );
my $rows = $self->app->get_stationid_by_ds100_query->fetchall_arrayref;
if ( @{$rows} ) {
return $rows->[0][0];
}
else {
$self->app->add_station_query->execute( $opt{ds100}, $opt{name} );
$self->app->get_stationid_by_ds100_query->execute( $opt{ds100} );
my $rows = $self->app->get_stationid_by_ds100_query->fetchall_arrayref;
return $rows->[0][0];
}
};
helper 'get_user_name' => sub {
my ($self) = @_;
my $user = $self->req->headers->header('X-Remote-User') // 'dev';
return $user;
};
helper 'get_user_id' => sub {
my ( $self, $user_name ) = @_;
$user_name //= $self->get_user_name;
if ( not -e $dbname ) {
$self->app->dbh->do(
qq{
create table users (
id integer primary key,
name char(64) not null unique
)
}
);
$self->app->dbh->do(
qq{
create table stations (
id integer primary key,
ds100 char(16) not null unique,
name char(64) not null unique
)
}
);
$self->app->dbh->do(
qq{
create table user_actions (
user_id int not null,
action_id int not null,
station_id int,
action_time int not null,
train_type char(16),
train_line char(16),
train_no char(16),
train_id char(128),
sched_time int,
real_time int,
route text,
messages text,
primary key (user_id, action_time)
)
}
);
}
$self->app->get_userid_query->execute($user_name);
my $rows = $self->app->get_userid_query->fetchall_arrayref;
if ( @{$rows} ) {
return $rows->[0][0];
}
else {
$self->app->add_user_query->execute($user_name);
$self->app->get_userid_query->execute($user_name);
my $rows = $self->app->get_userid_query->fetchall_arrayref;
return $rows->[0][0];
}
};
helper 'get_user_travels' => sub {
my ($self) = @_;
my $uid = $self->get_user_id( $self->get_user_name );
$self->app->get_all_actions_query->execute($uid);
my @travels;
while ( my @row = $self->app->get_all_actions_query->fetchrow_array ) {
my (
$action, $raw_ts, $ds100, $name,
$train_type, $train_line, $train_no, $train_id,
$raw_sched_ts, $raw_real_ts, $raw_route, $raw_messages
) = @row;
if ( $action == $action_type{checkin} ) {
push(
@travels,
{
from_name => $name,
sched_departure => epoch_to_dt($raw_sched_ts),
rt_departure => epoch_to_dt($raw_real_ts),
type => $train_type,
line => $train_line,
no => $train_no,
messages => [ split( qr{|}, $raw_messages ) ],
completed => 0,
}
);
}
elsif ( $action == $action_type{checkout} ) {
my $ref = $travels[-1];
$ref->{to_name} = $name;
$ref->{completed} = 1;
# if train_no is undef, we have a forced checkout without data
if ($train_no) {
$ref->{sched_arrival} = epoch_to_dt($raw_sched_ts),
$ref->{rt_arrival} = epoch_to_dt($raw_real_ts),
$ref->{messages} = [ split( qr{|}, $raw_messages ) ];
}
}
}
@travels = reverse @travels;
return @travels;
};
helper 'get_user_status' => sub {
my ($self) = @_;
my $uid = $self->get_user_id( $self->get_user_name );
$self->app->get_last_actions_query->execute($uid);
my $rows = $self->app->get_last_actions_query->fetchall_arrayref;
if ( @{$rows} ) {
my $now = DateTime->now( time_zone => 'Europe/Berlin' );
my $ts = DateTime->from_epoch(
epoch => $rows->[0][1],
time_zone => 'Europe/Berlin'
);
my $checkin_station_name = $rows->[0][3];
my @route = split( qr{[|]}, $rows->[0][8] // q{} );
my @route_after;
my $is_after = 0;
for my $station (@route) {
if ( $station eq $checkin_station_name ) {
$is_after = 1;
}
if ($is_after) {
push( @route_after, $station );
}
}
return {
checked_in => ( $rows->[0][0] == $action_type{checkin} ),
timestamp => $ts,
timestamp_delta => $now->subtract_datetime($ts),
station_ds100 => $rows->[0][2],
station_name => $rows->[0][3],
train_type => $rows->[0][4],
train_line => $rows->[0][5],
train_no => $rows->[0][6],
train_id => $rows->[0][7],
route => \@route,
route_after => \@route_after,
};
}
return {
checked_in => 0,
timestamp => 0
};
};
helper 'navbar_class' => sub {
my ( $self, $path ) = @_;
if ( $self->req->url eq $self->url_for($path) ) {
return 'active';
}
return q{};
};
get '/' => sub {
my ($self) = @_;
$self->render('landingpage');
};
get '/a/checkin' => sub {
my ($self) = @_;
my $station = $self->param('station');
my $train_id = $self->param('train');
my ( $train, $error ) = $self->checkin( $station, $train_id );
if ($error) {
$self->render(
'checkin',
error => $error,
train => undef
);
}
else {
$self->render(
'checkin',
error => undef,
train => $train
);
}
};
get '/a/checkout' => sub {
my ($self) = @_;
my $station = $self->param('station');
my $force = $self->param('force');
my $error = $self->checkout( $station, $force );
if ($error) {
$self->render( 'checkout', error => $error );
}
else {
$self->redirect_to("/${station}");
}
};
get '/*station' => sub {
my ($self) = @_;
my $station = $self->stash('station');
my $status = get_departures($station);
if ( $status->{errstr} ) {
$self->render( 'landingpage', error => $status->{errstr} );
}
else {
my @results = sort { $a->line cmp $b->line } @{ $status->{results} };
# You can't check into a train which terminates here
@results = grep { $_->departure } @results;
$self->render(
'departures',
ds100 => $status->{station_ds100},
results => \@results,
station => $status->{station_name}
);
}
};
app->defaults( layout => 'default' );
app->config(
hypnotoad => {
accepts => 10,
listen => [ $ENV{DBFAKEDISPLAY_LISTEN} // 'http://*:8092' ],
pid_file => '/tmp/db-fakedisplay.pid',
workers => $ENV{DBFAKEDISPLAY_WORKERS} // 2,
},
);
app->start;

View file

View file

@ -0,0 +1,38 @@
@font-face {
font-family: 'Material Icons';
font-style: normal;
font-weight: 400;
src: url(/static/fonts/MaterialIcons-Regular.eot); /* For IE6-8 */
src: local('Material Icons'),
local('MaterialIcons-Regular'),
url(/static/fonts/MaterialIcons-Regular.woff2) format('woff2'),
url(/static/fonts/MaterialIcons-Regular.woff) format('woff'),
url(/static/fonts/MaterialIcons-Regular.ttf) format('truetype');
}
.material-icons {
font-family: 'Material Icons';
font-weight: normal;
font-style: normal;
font-size: 24px; /* Preferred icon size */
display: inline-block;
width: 1em;
height: 1em;
line-height: 1;
text-transform: none;
letter-spacing: normal;
word-wrap: normal;
white-space: nowrap;
direction: ltr;
/* Support for all WebKit browsers. */
-webkit-font-smoothing: antialiased;
/* Support for Safari and Chrome. */
text-rendering: optimizeLegibility;
/* Support for Firefox. */
-moz-osx-font-smoothing: grayscale;
/* Support for IE. */
font-feature-settings: 'liga';
}

16
public/static/css/materialize.min.css vendored Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

After

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

4
public/static/js/jquery-2.2.4.min.js vendored Normal file

File diff suppressed because one or more lines are too long

10
public/static/js/materialize.min.js vendored Normal file

File diff suppressed because one or more lines are too long

47
templates/checkin.html.ep Normal file
View file

@ -0,0 +1,47 @@
% if ($error) {
<div class="row">
<div class="col s12 m6">
<div class="card red darken-4">
<div class="card-content white-text">
<span class="card-title">I am Error</span>
<p><%= $error %></p>
</div>
<div class="card-action">
% if (param('station')) {
<a href="/<%= param('station') %>">Zurück zu den Abfahrten</a>
% }
% else {
<a href="/">Zur Hauptseite</a>
% }
</div>
</div>
</div>
% }
% else {
<div class="row">
<div class="col s12">
<div class="card green darken-4">
<div class="card-content white-text">
<span class="card-title">Eingecheckt in <%= $train->line %></span>
<p>Abfahrt um <%= $train->sched_departure->strftime('%H:%M') %>
% if ($train->departure_delay) {
+<%= $train->departure_delay %>
% }
</p>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col s12">
Weitere Route:
<ul>
% for my $station ($train->route_post) {
<li><%= $station %>
(<a href="/a/checkout?station=<%= $station %>">hier auschecken</a>)
</li>
% }
</ul>
</div>
</div>
% }

View file

@ -0,0 +1,31 @@
% if ($error) {
<div class="row">
<div class="col s12 m6">
<div class="card red darken-4">
<div class="card-content white-text">
<span class="card-title">I am Error</span>
<p><%= $error %></p>
</div>
<div class="card-action">
<a href="/a/checkout?station=<%= param('station') %>&amp;force=1">Ohne Echtzeitdaten auschecken</a>
% if (param('station')) {
<a href="/<%= param('station') %>">Zurück zu den Abfahrten</a>
% }
% else {
<a href="/">Zur Hauptseite</a>
% }
</div>
</div>
</div>
% }
% else {
<div class="row">
<div class="col s12">
<div class="card green darken-4">
<div class="card-content white-text">
<span class="card-title">Erfolgreich ausgecheckt</span>
</div>
</div>
</div>
</div>
% }

View file

@ -0,0 +1,49 @@
<div class="row">
<div class="col s12">
% my $status = $self->get_user_status;
% if ($status->{checked_in}) {
<div class="card grey darken-4">
<div class="card-content white-text">
<span class="card-title">Aktuell eingecheckt</span>
<p>In <%= $status->{train_type} %> <%= $status->{train_no} %>
ab <%= $status->{station_name} %></p>
</div>
<div class="card-action">
<a href="/a/checkout?station=<%= $ds100 %>">Hier auschecken</a>
</div>
</div>
% }
</div>
</div>
<div class="row">
<table class="striped">
<thead>
<tr>
<th>Zug</th>
<th></th>
<th>Abfahrt</th>
</tr>
</thead>
<tbody>
% for my $result (@{$results}) {
<tr>
<td>
<a href="/a/checkin?station=<%= $ds100 %>&amp;train=<%= $result->train_id %>" title="Check In">
<%= $result->line %>
</a>
</td>
<td>
<a href="/a/checkin?station=<%= $ds100 %>&amp;train=<%= $result->train_id %>" title="Check In">
<%= $result->destination %>
</a>
</td>
<td><%= $result->departure->strftime('%H:%M') %>
% if ($result->departure_delay) {
(+<%= $result->departure_delay %>)
% }
</td>
</tr>
% }
</tbody>
</table>
</div>

View file

@ -0,0 +1,61 @@
<div class="row">
<div class="col s12">
% my $status = $self->get_user_status;
% if ($status->{checked_in}) {
<div class="card green darken-4">
<div class="card-content white-text">
<span class="card-title">Hallo, <%= $self->get_user_name %>!</span>
<p>Du bist gerade eingecheckt in
<%= $status->{train_type} %> <%= $status->{train_no} %>
ab <%= $status->{station_name} %>.</p>
<p>Auschecken?
<ul>
% my $is_after = 0;
% for my $station (@{$status->{route_after}}) {
<li><a href="/a/checkout?station=<%= $station %>"><%= $station %></a></li>
% }
</ul>
</p>
</div>
</div>
% }
% else {
<div class="card grey darken-4">
<div class="card-content white-text">
<span class="card-title">Hallo, <%= $self->get_user_name %>!</span>
<p>Du bist gerade nicht eingecheckt.</p>
</div>
</div>
% }
</div>
</div>
<h1>Bisherige Fahrten</h1>
<div class="row">
<table class="striped">
<thead>
<tr>
<th>Datum</th>
<th>Zug</th>
<th>Strecke</th>
<th>Dauer</th>
</tr>
</thead>
<tbody>
% for my $travel (get_user_travels()) {
% if ($travel->{completed}) {
<tr>
<td><%= $travel->{sched_departure}->strftime('%d.%m.%Y') %></td>
<td><%= $travel->{type} %> <%= $travel->{line} // $travel->{no} %></td>
<td><%= $travel->{from_name} %> → <%= $travel->{to_name} %></td>
% if ($travel->{rt_arrival} and $travel->{rt_departure}) {
<td><%= ($travel->{rt_arrival}->epoch - $travel->{rt_departure}->epoch) / 60 %> min</td>
% }
% else {
<td>?</td>
% }
</tr>
% }
% }
</tbody>
</tabel>
</div>

View file

@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="de">
<head>
<title><%= stash('title') // 'travelynx' %></title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
%= stylesheet '/static/css/materialize.min.css'
%= stylesheet '/static/css/material-icons.css'
%= stylesheet '/static/css/local.css'
%= javascript '/static/js/jquery-2.2.4.min.js'
%= javascript '/static/js/materialize.min.js'
</head>
<body>
<nav class="blue">
<div class="nav-wrapper container">
<a href="/" class="brand-logo left">travelynx</a>
<ul id="nav-mobile" class="right">
<li class="<%= navbar_class('/') %>"><a href='/' title="Einchecken"><i class="material-icons">Check In</i></a></li>
</ul>
</div>
</nav>
<div class="container">
%= content
</div>
</body>
</html>