initial commit
This commit is contained in:
commit
4111a0cfd8
8
.env.dist
Normal file
8
.env.dist
Normal file
@ -0,0 +1,8 @@
|
||||
# This file is a "template" of which env vars need to be defined for your application
|
||||
# Copy this file to .env file for development, create environment variables when deploying to production
|
||||
# https://symfony.com/doc/current/best_practices/configuration.html#infrastructure-related-configuration
|
||||
|
||||
###> symfony/framework-bundle ###
|
||||
APP_ENV=dev
|
||||
APP_SECRET=1bdf86cdc78fba654e4f2c309c6bbdbd
|
||||
###< symfony/framework-bundle ###
|
16
.gitignore
vendored
Normal file
16
.gitignore
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
|
||||
###> symfony/framework-bundle ###
|
||||
/.env
|
||||
/public/bundles/
|
||||
/var/
|
||||
/vendor/
|
||||
###< symfony/framework-bundle ###
|
||||
|
||||
###> symfony/web-server-bundle ###
|
||||
/.web-server-pid
|
||||
###< symfony/web-server-bundle ###
|
||||
|
||||
/node_modules/
|
||||
/.idea/
|
||||
/public/*
|
||||
!/public/index.php
|
39
bin/console
Executable file
39
bin/console
Executable file
@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
use App\Kernel;
|
||||
use Symfony\Bundle\FrameworkBundle\Console\Application;
|
||||
use Symfony\Component\Console\Input\ArgvInput;
|
||||
use Symfony\Component\Debug\Debug;
|
||||
use Symfony\Component\Dotenv\Dotenv;
|
||||
|
||||
set_time_limit(0);
|
||||
|
||||
require __DIR__.'/../vendor/autoload.php';
|
||||
|
||||
if (!class_exists(Application::class)) {
|
||||
throw new \RuntimeException('You need to add "symfony/framework-bundle" as a Composer dependency.');
|
||||
}
|
||||
|
||||
if (!isset($_SERVER['APP_ENV'])) {
|
||||
if (!class_exists(Dotenv::class)) {
|
||||
throw new \RuntimeException('APP_ENV environment variable is not defined. You need to define environment variables for configuration or add "symfony/dotenv" as a Composer dependency to load variables from a .env file.');
|
||||
}
|
||||
(new Dotenv())->load(__DIR__.'/../.env');
|
||||
}
|
||||
|
||||
$input = new ArgvInput();
|
||||
$env = $input->getParameterOption(['--env', '-e'], $_SERVER['APP_ENV'] ?? 'dev', true);
|
||||
$debug = (bool) ($_SERVER['APP_DEBUG'] ?? ('prod' !== $env)) && !$input->hasParameterOption('--no-debug', true);
|
||||
|
||||
if ($debug) {
|
||||
umask(0000);
|
||||
|
||||
if (class_exists(Debug::class)) {
|
||||
Debug::enable();
|
||||
}
|
||||
}
|
||||
|
||||
$kernel = new Kernel($env, $debug);
|
||||
$application = new Application($kernel);
|
||||
$application->run($input);
|
76
composer.json
Normal file
76
composer.json
Normal file
@ -0,0 +1,76 @@
|
||||
{
|
||||
"name": "kadet/czydojade",
|
||||
"type": "project",
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"php": "^7.1.3",
|
||||
"ext-ctype": "*",
|
||||
"ext-iconv": "*",
|
||||
"ext-json": "*",
|
||||
"nesbot/carbon": "^1.33",
|
||||
"sensio/framework-extra-bundle": "^5.2",
|
||||
"symfony/console": "*",
|
||||
"symfony/flex": "^1.1",
|
||||
"symfony/framework-bundle": "*",
|
||||
"symfony/serializer-pack": "^1.0",
|
||||
"symfony/twig-bundle": "*",
|
||||
"symfony/yaml": "*",
|
||||
"tightenco/collect": "^5.6"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/dotenv": "*",
|
||||
"symfony/web-server-bundle": "*",
|
||||
"kadet/functional": "dev-master"
|
||||
},
|
||||
"config": {
|
||||
"preferred-install": {
|
||||
"*": "dist"
|
||||
},
|
||||
"sort-packages": true,
|
||||
"secure-http": false
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"App\\": "src/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"App\\Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"replace": {
|
||||
"symfony/polyfill-ctype": "*",
|
||||
"symfony/polyfill-iconv": "*",
|
||||
"symfony/polyfill-php71": "*",
|
||||
"symfony/polyfill-php70": "*",
|
||||
"symfony/polyfill-php56": "*"
|
||||
},
|
||||
"scripts": {
|
||||
"auto-scripts": {
|
||||
"cache:clear": "symfony-cmd",
|
||||
"assets:install %PUBLIC_DIR%": "symfony-cmd"
|
||||
},
|
||||
"post-install-cmd": [
|
||||
"@auto-scripts"
|
||||
],
|
||||
"post-update-cmd": [
|
||||
"@auto-scripts"
|
||||
]
|
||||
},
|
||||
"conflict": {
|
||||
"symfony/symfony": "*"
|
||||
},
|
||||
"extra": {
|
||||
"symfony": {
|
||||
"allow-contrib": false,
|
||||
"require": "4.1.*"
|
||||
}
|
||||
},
|
||||
"repositories": [
|
||||
{
|
||||
"type": "vcs",
|
||||
"url": "http://git.kadet.net/kadet/functional-php.git"
|
||||
}
|
||||
]
|
||||
}
|
3167
composer.lock
generated
Normal file
3167
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
8
config/bundles.php
Normal file
8
config/bundles.php
Normal file
@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
|
||||
Symfony\Bundle\WebServerBundle\WebServerBundle::class => ['dev' => true],
|
||||
Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle::class => ['all' => true],
|
||||
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
|
||||
];
|
3
config/packages/dev/routing.yaml
Normal file
3
config/packages/dev/routing.yaml
Normal file
@ -0,0 +1,3 @@
|
||||
framework:
|
||||
router:
|
||||
strict_requirements: true
|
30
config/packages/framework.yaml
Normal file
30
config/packages/framework.yaml
Normal file
@ -0,0 +1,30 @@
|
||||
framework:
|
||||
secret: '%env(APP_SECRET)%'
|
||||
#default_locale: en
|
||||
#csrf_protection: true
|
||||
#http_method_override: true
|
||||
|
||||
# Enables session support. Note that the session will ONLY be started if you read or write from it.
|
||||
# Remove or comment this section to explicitly disable session support.
|
||||
session:
|
||||
handler_id: ~
|
||||
|
||||
#esi: true
|
||||
#fragments: true
|
||||
php_errors:
|
||||
log: true
|
||||
|
||||
cache:
|
||||
# Put the unique name of your app here: the prefix seed
|
||||
# is used to compute stable namespaces for cache keys.
|
||||
#prefix_seed: your_vendor_name/app_name
|
||||
|
||||
# The app cache caches to the filesystem by default.
|
||||
# Other options include:
|
||||
|
||||
# Redis
|
||||
#app: cache.adapter.redis
|
||||
#default_redis_provider: redis://localhost
|
||||
|
||||
# APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues)
|
||||
#app: cache.adapter.apcu
|
3
config/packages/routing.yaml
Normal file
3
config/packages/routing.yaml
Normal file
@ -0,0 +1,3 @@
|
||||
framework:
|
||||
router:
|
||||
strict_requirements: ~
|
3
config/packages/sensio_framework_extra.yaml
Normal file
3
config/packages/sensio_framework_extra.yaml
Normal file
@ -0,0 +1,3 @@
|
||||
sensio_framework_extra:
|
||||
router:
|
||||
annotations: false
|
4
config/packages/test/framework.yaml
Normal file
4
config/packages/test/framework.yaml
Normal file
@ -0,0 +1,4 @@
|
||||
framework:
|
||||
test: true
|
||||
session:
|
||||
storage_id: session.storage.mock_file
|
7
config/packages/translation.yaml
Normal file
7
config/packages/translation.yaml
Normal file
@ -0,0 +1,7 @@
|
||||
framework:
|
||||
default_locale: '%locale%'
|
||||
translator:
|
||||
paths:
|
||||
- '%kernel.project_dir%/translations'
|
||||
fallbacks:
|
||||
- '%locale%'
|
4
config/packages/twig.yaml
Normal file
4
config/packages/twig.yaml
Normal file
@ -0,0 +1,4 @@
|
||||
twig:
|
||||
paths: ['%kernel.project_dir%/templates']
|
||||
debug: '%kernel.debug%'
|
||||
strict_variables: '%kernel.debug%'
|
3
config/routes.yaml
Normal file
3
config/routes.yaml
Normal file
@ -0,0 +1,3 @@
|
||||
#index:
|
||||
# path: /
|
||||
# controller: App\Controller\DefaultController::index
|
3
config/routes/annotations.yaml
Normal file
3
config/routes/annotations.yaml
Normal file
@ -0,0 +1,3 @@
|
||||
controllers:
|
||||
resource: ../../src/Controller/
|
||||
type: annotation
|
3
config/routes/dev/twig.yaml
Normal file
3
config/routes/dev/twig.yaml
Normal file
@ -0,0 +1,3 @@
|
||||
_errors:
|
||||
resource: '@TwigBundle/Resources/config/routing/errors.xml'
|
||||
prefix: /_error
|
40
config/services.yaml
Normal file
40
config/services.yaml
Normal file
@ -0,0 +1,40 @@
|
||||
# This file is the entry point to configure your own services.
|
||||
# Files in the packages/ subdirectory configure your dependencies.
|
||||
|
||||
# Put parameters here that don't need to change on each machine where the app is deployed
|
||||
# https://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration
|
||||
parameters:
|
||||
locale: 'en'
|
||||
|
||||
services:
|
||||
# default configuration for services in *this* file
|
||||
_defaults:
|
||||
autowire: true # Automatically injects dependencies in your services.
|
||||
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
|
||||
public: false # Allows optimizing the container by removing unused services; this also means
|
||||
# fetching services directly from the container via $container->get() won't work.
|
||||
# The best practice is to be explicit about your dependencies anyway.
|
||||
|
||||
# makes classes in src/ available to be used as services
|
||||
# this creates a service per class whose id is the fully-qualified class name
|
||||
App\:
|
||||
resource: '../src/*'
|
||||
exclude: '../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php}'
|
||||
|
||||
# controllers are imported separately to make sure services can be injected
|
||||
# as action arguments even if you don't extend any base controller class
|
||||
App\Controller\:
|
||||
resource: '../src/Controller'
|
||||
tags: ['controller.service_arguments']
|
||||
|
||||
App\Provider\:
|
||||
resource: '../src/Provider'
|
||||
public: true
|
||||
|
||||
serializer.datetime_normalizer:
|
||||
class: Symfony\Component\Serializer\Normalizer\DateTimeNormalizer
|
||||
arguments: [!php/const DateTime::ATOM]
|
||||
tags: [serializer.normalizer]
|
||||
|
||||
# add more service definitions when explicit configuration is needed
|
||||
# please note that last definitions always *replace* previous ones
|
36
package.json
Normal file
36
package.json
Normal file
@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "czydojade",
|
||||
"version": "1.0.0",
|
||||
"author": "Kacper Donat <kadet1090@gmail.com>",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"webpack": "^4.17.0",
|
||||
"webpack-cli": "^3.1.0"
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.4",
|
||||
"@fortawesome/pro-light-svg-icons": "^5.3.1",
|
||||
"@fortawesome/pro-regular-svg-icons": "^5.3.1",
|
||||
"@fortawesome/pro-solid-svg-icons": "^5.3.1",
|
||||
"@fortawesome/vue-fontawesome": "^0.1.1",
|
||||
"@types/bootstrap": "^4.1.2",
|
||||
"@types/jquery": "^3.3.6",
|
||||
"@types/moment": "^2.13.0",
|
||||
"@types/popper.js": "^1.11.0",
|
||||
"bootstrap": "^4.1.3",
|
||||
"css-loader": "^1.0.0",
|
||||
"file-loader": "^2.0.0",
|
||||
"jquery": "^3.3.1",
|
||||
"moment": "^2.22.2",
|
||||
"node-sass": "^4.9.3",
|
||||
"popper.js": "^1.14.4",
|
||||
"raw-loader": "^0.5.1",
|
||||
"sass-loader": "^7.1.0",
|
||||
"style-loader": "^0.22.1",
|
||||
"ts-loader": "^4.5.0",
|
||||
"typescript": "^3.0.1",
|
||||
"vue": "^2.5.17",
|
||||
"vue-class-component": "^6.2.0",
|
||||
"vue-property-decorator": "^7.0.0",
|
||||
"xmldom": "^0.1.27",
|
||||
"xpath": "^0.0.27"
|
||||
}
|
||||
}
|
39
public/index.php
Normal file
39
public/index.php
Normal file
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
use App\Kernel;
|
||||
use Symfony\Component\Debug\Debug;
|
||||
use Symfony\Component\Dotenv\Dotenv;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
require __DIR__.'/../vendor/autoload.php';
|
||||
|
||||
// The check is to ensure we don't use .env in production
|
||||
if (!isset($_SERVER['APP_ENV'])) {
|
||||
if (!class_exists(Dotenv::class)) {
|
||||
throw new \RuntimeException('APP_ENV environment variable is not defined. You need to define environment variables for configuration or add "symfony/dotenv" as a Composer dependency to load variables from a .env file.');
|
||||
}
|
||||
(new Dotenv())->load(__DIR__.'/../.env');
|
||||
}
|
||||
|
||||
$env = $_SERVER['APP_ENV'] ?? 'dev';
|
||||
$debug = (bool) ($_SERVER['APP_DEBUG'] ?? ('prod' !== $env));
|
||||
|
||||
if ($debug) {
|
||||
umask(0000);
|
||||
|
||||
Debug::enable();
|
||||
}
|
||||
|
||||
if ($trustedProxies = $_SERVER['TRUSTED_PROXIES'] ?? false) {
|
||||
Request::setTrustedProxies(explode(',', $trustedProxies), Request::HEADER_X_FORWARDED_ALL ^ Request::HEADER_X_FORWARDED_HOST);
|
||||
}
|
||||
|
||||
if ($trustedHosts = $_SERVER['TRUSTED_HOSTS'] ?? false) {
|
||||
Request::setTrustedHosts(explode(',', $trustedHosts));
|
||||
}
|
||||
|
||||
$kernel = new Kernel($env, $debug);
|
||||
$request = Request::createFromGlobals();
|
||||
$response = $kernel->handle($request);
|
||||
$response->send();
|
||||
$kernel->terminate($request, $response);
|
47
resources/components/departures.html
Normal file
47
resources/components/departures.html
Normal file
@ -0,0 +1,47 @@
|
||||
<div class="departures">
|
||||
<div class="departures__actions">
|
||||
<div class="departures__auto-refresh">
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" class="custom-control-input" v-model="autoRefresh" :id="`autorefresh_${_uid}`">
|
||||
<label class="custom-control-label" :for="`autorefresh_${_uid}`">auto odświeżanie:</label>
|
||||
</div>
|
||||
<div v-if="autoRefresh" class="d-flex align-items-center">
|
||||
<input v-model="interval" class="form-control form-control-sm mx-1"/>
|
||||
s
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button @click="update" class="flex-space-left btn btn-sm btn-outline-action">
|
||||
<fa :icon="['far', 'sync']" /> odśwież
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<ul class="departures__list list-underlined">
|
||||
<li class="departure" v-for="departure in departures">
|
||||
<div class="departure__line line">
|
||||
<div class="line__symbol d-flex align-items-center">
|
||||
<fa :icon="['fac', departure.line.type]" fixed-width class="mr-1"/>
|
||||
{{ departure.line.symbol }}
|
||||
</div>
|
||||
<div class="line__display">{{ departure.display }}</div>
|
||||
</div>
|
||||
|
||||
<div class="departure__time">
|
||||
<span class="departure__scheduled" v-if="departure.scheduled.format('HH:mm') !== departure.estimated.format('HH:mm')">
|
||||
{{ departure.scheduled.format('HH:mm') }}
|
||||
</span>
|
||||
<span class="badge" :class="[departure.delay < 0 ? 'badge-danger' : 'badge-warning']"
|
||||
v-if="departure.delay < 0 || departure.delay > 30">
|
||||
{{ departure.delay|signed }}s
|
||||
</span>
|
||||
<span class="departure__estimated">{{ departure.estimated.format('HH:mm') }}</span>
|
||||
</div>
|
||||
|
||||
<stop :stop="departure.stop" class="departure__stop">
|
||||
<template slot="actions">
|
||||
<fa :icon="['fal', 'sign']" fixed-width class="mr-1"/>
|
||||
</template>
|
||||
</stop>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
42
resources/components/finder.html
Normal file
42
resources/components/finder.html
Normal file
@ -0,0 +1,42 @@
|
||||
<div class="finder">
|
||||
<input class="form-control" v-model="filter" />
|
||||
|
||||
<div v-if="state === 'fetching'">
|
||||
<fa icon="spinner-third" pulse/>
|
||||
</div>
|
||||
<div class="finder__stops" v-else-if="filter.length > 2 && Object.keys(filtered).length > 0">
|
||||
<div class="stop-group" v-for="(group, name) in filtered">
|
||||
<div class="stop-group__header">
|
||||
<button class="btn btn-action">
|
||||
<fa :icon="['fal', 'chevron-down']"></fa>
|
||||
</button>
|
||||
|
||||
<h3 class="stop-group__name">{{ name }}</h3>
|
||||
|
||||
<div class="actions flex-space-left">
|
||||
<button class="btn btn-action" @click="group.forEach(select)">
|
||||
<fa :icon="['fal', 'check-double']"></fa>
|
||||
wybierz wszystkie
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="stop-group__stops list-unstyled">
|
||||
<li v-for="stop in group" :key="stop.id">
|
||||
<stop :stop="stop">
|
||||
<template slot="actions">
|
||||
<button @click="select(stop, $event)" class="btn btn-action">
|
||||
<fa :icon="['fal', 'check']" />
|
||||
</button>
|
||||
</template>
|
||||
</stop>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert alert-warning" v-else-if="filter.length > 2">
|
||||
Nie znaleziono więcej przystanków, spełniających te kryteria.
|
||||
</div>
|
||||
<div class="alert alert-info" v-else>
|
||||
Wprowadź zapytanie powyżej, aby wyszukać przystanek.
|
||||
</div>
|
||||
</div>
|
17
resources/components/picker.html
Normal file
17
resources/components/picker.html
Normal file
@ -0,0 +1,17 @@
|
||||
<div class="picker">
|
||||
<departures :stops="stops"/>
|
||||
|
||||
<ul class="picker__stops list-underlined">
|
||||
<li v-for="stop in stops" :key="stop.id">
|
||||
<stop :stop="stop">
|
||||
<template slot="actions">
|
||||
<button @click="remove(stop)" class="btn btn-action">
|
||||
<fa :icon="['fal', 'times']" />
|
||||
</button>
|
||||
</template>
|
||||
</stop>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<stop-finder @select="add" :blacklist="stops"/>
|
||||
</div>
|
19
resources/components/stop.html
Normal file
19
resources/components/stop.html
Normal file
@ -0,0 +1,19 @@
|
||||
<div class="stop">
|
||||
<div class="stop__actions">
|
||||
<slot name="actions"/>
|
||||
</div>
|
||||
|
||||
<span class="stop__name">{{ stop.name }}</span>
|
||||
<span class="stop__description badge badge-dark" v-if="stop.variant">{{ stop.variant }}</span>
|
||||
<slot/>
|
||||
|
||||
<div class="stop__actions flex-space-left">
|
||||
<button class="btn btn-action">
|
||||
<fa :icon="['fal', 'info-circle']"/>
|
||||
</button>
|
||||
|
||||
<button class="btn btn-action">
|
||||
<fa :icon="['fal', 'map-marked-alt']"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
1
resources/icons/light/bus.svg
Normal file
1
resources/icons/light/bus.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><title>bus</title><path d="M128,384a32,32,0,1,0-32-32A32,32,0,0,0,128,384Zm256,0a32,32,0,1,0-32-32A32,32,0,0,0,384,384ZM488,128h-8V80c0-44.8-99.2-80-224-80S32,35.2,32,80v48H24A24,24,0,0,0,0,152v80a24,24,0,0,0,24,24h8V416a32,32,0,0,0,32,32v32a32,32,0,0,0,32,32h48a32,32,0,0,0,32-32V448H336v32a32,32,0,0,0,32,32h48a32,32,0,0,0,32-32V448a32,32,0,0,0,32-32V256h8a24,24,0,0,0,24-24V152A24,24,0,0,0,488,128ZM144,480H96V448h48Zm272,0H368V448h48Zm32-64H64V288H448Zm0-160H64V128H448Zm0-160H64V80.31C67.31,67,131.41,32,256,32S444.69,67,448,80.31Z"/></svg>
|
After Width: | Height: | Size: 640 B |
463
resources/icons/light/icons.ai
Normal file
463
resources/icons/light/icons.ai
Normal file
File diff suppressed because one or more lines are too long
1
resources/icons/light/metro.svg
Normal file
1
resources/icons/light/metro.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><title>metro</title><path d="M258,320a16,16,0,1,1-16,16,16,16,0,0,1,16-16m0-32a48,48,0,1,0,48,48A48,48,0,0,0,258,288ZM354,0H162C98,0,34,43,34,96V352c0,47.17,50.66,86.39,106.9,94.47L85.62,501.76A6,6,0,0,0,89.86,512h25.8a12,12,0,0,0,8.48-3.51L184.63,448H331.37l60.49,60.48a12,12,0,0,0,8.48,3.52h25.8a6,6,0,0,0,4.24-10.24L375.1,446.47C431.34,438.39,482,399.17,482,352V96C482,43,419,0,354,0ZM66,128H450v96H66Zm96-96H354c58.24,0,96,37.88,96,64H66C66,63.7,113.55,32,162,32ZM354,416H162c-48.45,0-96-31.7-96-64V256H450v96C450,384.3,402.45,416,354,416Z"/></svg>
|
After Width: | Height: | Size: 647 B |
1
resources/icons/light/train.svg
Normal file
1
resources/icons/light/train.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><title>train</title><path d="M144,384a48,48,0,1,0-48-48A48,48,0,0,0,144,384Zm0-64a16,16,0,1,1-16,16A16,16,0,0,1,144,320Zm224,64a48,48,0,1,0-48-48A48,48,0,0,0,368,384Zm0-64a16,16,0,1,1-16,16A16,16,0,0,1,368,320ZM352,0H160C96,0,32,43,32,96V352c0,47.17,50.66,86.39,106.9,94.47L83.62,501.76A6,6,0,0,0,87.86,512h25.8a12,12,0,0,0,8.48-3.51L182.63,448H329.37l60.49,60.48a12,12,0,0,0,8.48,3.52h25.8a6,6,0,0,0,4.24-10.24L373.1,446.47C429.34,438.39,480,399.17,480,352V96C480,43,417,0,352,0ZM64,128H240v96H64ZM448,352c0,32.3-47.55,64-96,64H160c-48.45,0-96-31.7-96-64V256H448Zm0-128H272V128H448ZM64,96c0-32.3,47.55-64,96-64H352c58.24,0,96,37.88,96,64Z"/></svg>
|
After Width: | Height: | Size: 743 B |
1
resources/icons/light/tram.svg
Normal file
1
resources/icons/light/tram.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><title>tram</title><path d="M352,384a32,32,0,1,0-32-32A32,32,0,0,0,352,384ZM456,128H445c-9.24-23.77-39.56-43.48-89.25-54.49l26.77-26.77a257.81,257.81,0,0,1,33.11,11.12,8.16,8.16,0,0,0,8.95-1.75l11.86-11.87a8.17,8.17,0,0,0-2.27-13.16C393.42,12.07,329.1,0,256,0S118.59,12.07,77.85,31.08a8.16,8.16,0,0,0-2.28,13.16L87.44,56.11a8.16,8.16,0,0,0,9,1.76,256.5,256.5,0,0,1,33.1-11.13l26.77,26.77c-49.69,11-80,30.72-89.25,54.49H56a24,24,0,0,0-24,24v80a24,24,0,0,0,24,24h8V416a32,32,0,0,0,32,32h41.37L83.62,501.76A6,6,0,0,0,87.86,512h25.8a12,12,0,0,0,8.48-3.51L182.63,448H329.37l60.49,60.48a12,12,0,0,0,8.48,3.52h25.8a6,6,0,0,0,4.24-10.24L374.63,448H416a32,32,0,0,0,32-32V256h8a24,24,0,0,0,24-24V152A24,24,0,0,0,456,128ZM256,32a551.75,551.75,0,0,1,89.21,6.79L316.83,67.17A547.06,547.06,0,0,0,256,64a547.06,547.06,0,0,0-60.83,3.17L166.79,38.79A551.75,551.75,0,0,1,256,32Zm0,64c87.19,0,129.08,17.14,147.52,32h-295C126.92,113.14,168.81,96,256,96ZM416,416H96V288H416Zm0-160H96V160H416ZM160,384a32,32,0,1,0-32-32A32,32,0,0,0,160,384Z"/></svg>
|
After Width: | Height: | Size: 1.1 KiB |
1
resources/icons/light/trolleybus.svg
Normal file
1
resources/icons/light/trolleybus.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><title>trolleybus</title><path d="M128,384a32,32,0,1,0-32-32A32,32,0,0,0,128,384Zm256,0a32,32,0,1,0-32-32A32,32,0,0,0,384,384ZM488,160h-8V128C480,92.28,417,62.67,328.13,52.13L370,10.24A6,6,0,0,0,365.77,0H340a12,12,0,0,0-8.48,3.51L286.3,48.7Q271.45,48,256,48c-19.76,0-38.88.88-57.07,2.55l40.3-40.31A6,6,0,0,0,235,0h-25.8a12,12,0,0,0-8.48,3.51L146.19,58C77.75,71.58,32,97.58,32,128v32H24A24,24,0,0,0,0,184v48a24,24,0,0,0,24,24h8V416a32,32,0,0,0,32,32v32a32,32,0,0,0,32,32h48a32,32,0,0,0,32-32V448H336v32a32,32,0,0,0,32,32h48a32,32,0,0,0,32-32V448a32,32,0,0,0,32-32V256h8a24,24,0,0,0,24-24V184A24,24,0,0,0,488,160ZM256,80c123.65,0,187.72,34.47,191.94,48H64.06C68.28,114.47,132.35,80,256,80ZM144,480H96V448h48Zm272,0H368V448h48Zm32-64H64V288H448Zm0-160H64V160H448Z"/></svg>
|
After Width: | Height: | Size: 864 B |
1
resources/icons/light/unknown.svg
Normal file
1
resources/icons/light/unknown.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><title>unknown</title><path d="M488,128h-8V80c0-44.8-99.2-80-224-80S32,35.2,32,80v48H24A24,24,0,0,0,0,152v80a24,24,0,0,0,24,24h8V416a32,32,0,0,0,32,32v32a32,32,0,0,0,32,32h48a32,32,0,0,0,32-32V448H336v32a32,32,0,0,0,32,32h48a32,32,0,0,0,32-32V448a32,32,0,0,0,32-32V256h8a24,24,0,0,0,24-24V152A24,24,0,0,0,488,128ZM144,480H96V448h48Zm272,0H368V448h48ZM448,96h0v32h0l0,128h0v32h0V416H64V288h0V256h0V128h0V96h0V80.31C67.31,67,131.41,32,256,32S444.69,67,448,80.31ZM254.48,308a28,28,0,1,0,28,28A28,28,0,0,0,254.48,308Zm-.33-224c-44.82,0-70,17.51-91.23,44.74a12,12,0,0,0,2.64,17.18l13.14,9.15a12,12,0,0,0,16.24-2.38C209.89,133.65,225,124,254.15,124c39.68,0,57.44,20.2,57.44,40.21,0,43.79-77.44,37.07-77.44,107.41V272a12,12,0,0,0,12,12h16a12,12,0,0,0,12-12v-.38c0-43.56,77.44-40,77.44-107.41C351.59,113.75,306.47,84,254.15,84Z"/></svg>
|
After Width: | Height: | Size: 923 B |
15
resources/styles/_common.scss
Normal file
15
resources/styles/_common.scss
Normal file
@ -0,0 +1,15 @@
|
||||
.list-underlined {
|
||||
@extend .list-unstyled;
|
||||
|
||||
li {
|
||||
border-bottom: 1px solid $text-muted;
|
||||
}
|
||||
}
|
||||
|
||||
.flex-space-left {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.flex-space-right {
|
||||
margin-right: auto;
|
||||
}
|
11
resources/styles/_controls.scss
Normal file
11
resources/styles/_controls.scss
Normal file
@ -0,0 +1,11 @@
|
||||
.btn {
|
||||
&.btn-action {
|
||||
@extend .btn-link;
|
||||
|
||||
color: black;
|
||||
}
|
||||
|
||||
&.btn-outline-action {
|
||||
@extend .btn-outline-dark;
|
||||
}
|
||||
}
|
53
resources/styles/_departure.scss
Normal file
53
resources/styles/_departure.scss
Normal file
@ -0,0 +1,53 @@
|
||||
.departure {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.departure__line {
|
||||
flex: 3 0;
|
||||
}
|
||||
|
||||
.departure__stop {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.departure__time {
|
||||
width: 9rem;
|
||||
text-align: right;
|
||||
|
||||
.departure__scheduled {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.departures__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.departures__auto-refresh {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
.departure__time {
|
||||
order: 2;
|
||||
}
|
||||
|
||||
.departure__stop {
|
||||
flex: 2 0;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
.departure__time {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
8
resources/styles/_line.scss
Normal file
8
resources/styles/_line.scss
Normal file
@ -0,0 +1,8 @@
|
||||
.line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.line__symbol {
|
||||
min-width: 4rem;
|
||||
}
|
||||
}
|
23
resources/styles/_stop.scss
Normal file
23
resources/styles/_stop.scss
Normal file
@ -0,0 +1,23 @@
|
||||
.stop, .stop-group__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.stop__name {
|
||||
}
|
||||
|
||||
.stop__description {
|
||||
margin: 0 .5rem;
|
||||
}
|
||||
|
||||
.stop__actions {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
|
||||
.stop-group__name {
|
||||
font-size: $font-size-base;
|
||||
font-weight: bold;
|
||||
|
||||
margin-bottom: 0;
|
||||
}
|
17
resources/styles/main.scss
Normal file
17
resources/styles/main.scss
Normal file
@ -0,0 +1,17 @@
|
||||
$border-radius: 2px;
|
||||
$border-radius-lg: $border-radius;
|
||||
$border-radius-sm: $border-radius;
|
||||
|
||||
@import "~bootstrap/scss/functions";
|
||||
@import "~bootstrap/scss/variables";
|
||||
|
||||
$custom-control-indicator-checked-bg: $dark;
|
||||
$custom-control-indicator-active-bg: $dark;
|
||||
|
||||
@import "~bootstrap/scss/bootstrap";
|
||||
|
||||
@import "common";
|
||||
@import "stop";
|
||||
@import "departure";
|
||||
@import "line";
|
||||
@import "controls";
|
13
resources/svg-icon-loader.js
Normal file
13
resources/svg-icon-loader.js
Normal file
@ -0,0 +1,13 @@
|
||||
const dom = require('xmldom');
|
||||
const xpath = require('xpath');
|
||||
|
||||
module.exports = function svgIconLoader(source) {
|
||||
const parser = new dom.DOMParser();
|
||||
const svg = parser.parseFromString(source, 'image/svg+xml');
|
||||
|
||||
const result = xpath.useNamespaces({
|
||||
'svg': 'http://www.w3.org/2000/svg'
|
||||
})('string(//svg:path/@d)', svg);
|
||||
|
||||
return result;
|
||||
};
|
26
resources/ts/app.ts
Normal file
26
resources/ts/app.ts
Normal file
@ -0,0 +1,26 @@
|
||||
/// <reference path="types/popper.js.d.ts"/>
|
||||
/// <reference path="types/webpack.d.ts"/>
|
||||
|
||||
import '../styles/main.scss'
|
||||
|
||||
import './font-awesome'
|
||||
import './filters'
|
||||
|
||||
import * as Popper from 'popper.js';
|
||||
import * as $ from "jquery";
|
||||
|
||||
import * as components from './components';
|
||||
|
||||
window['$'] = window['jQuery'] = $;
|
||||
window['popper'] = Popper;
|
||||
|
||||
// dependencies
|
||||
import 'bootstrap'
|
||||
import Vue from 'vue';
|
||||
|
||||
// here goes "public" API
|
||||
window['czydojade'] = {
|
||||
components
|
||||
};
|
||||
|
||||
window['app'] = new Vue({ el: '#app' });
|
55
resources/ts/components/departures.ts
Normal file
55
resources/ts/components/departures.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import Vue from 'vue'
|
||||
import { Departure, Stop } from "../model";
|
||||
import { Component, Prop, Watch } from "vue-property-decorator";
|
||||
import urls from '../urls';
|
||||
import template = require("../../components/departures.html");
|
||||
import moment = require("moment");
|
||||
import { Jsonified } from "../utils";
|
||||
import { debounce } from "../decorators";
|
||||
|
||||
@Component({template})
|
||||
export class Departures extends Vue {
|
||||
private _intervalId: number;
|
||||
|
||||
autoRefresh: boolean = false;
|
||||
departures: Departure[] = [];
|
||||
interval: number = 20;
|
||||
|
||||
@Prop(Array)
|
||||
stops: Stop[];
|
||||
|
||||
@Watch('stops')
|
||||
@debounce(300)
|
||||
async update() {
|
||||
const response = await fetch(urls.prepare(urls.departures, {
|
||||
stop: this.stops.map(stop => stop.id),
|
||||
provider: 'gdansk'
|
||||
}));
|
||||
|
||||
if (response.ok) {
|
||||
const departures = await response.json() as Jsonified<Departure>[];
|
||||
|
||||
this.departures = departures.map(departure => {
|
||||
departure.scheduled = moment.parseZone(departure.scheduled);
|
||||
departure.estimated = moment.parseZone(departure.estimated);
|
||||
|
||||
return departure as Departure;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Watch('interval')
|
||||
@Watch('autoRefresh')
|
||||
private setupAutoRefresh() {
|
||||
if (this._intervalId) {
|
||||
window.clearInterval(this._intervalId);
|
||||
this._intervalId = undefined;
|
||||
}
|
||||
|
||||
if (this.autoRefresh) {
|
||||
this._intervalId = window.setInterval(() => this.update(), this.interval * 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Vue.component('Departures', Departures);
|
2
resources/ts/components/index.ts
Normal file
2
resources/ts/components/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './picker'
|
||||
export * from './departures'
|
99
resources/ts/components/picker.ts
Normal file
99
resources/ts/components/picker.ts
Normal file
@ -0,0 +1,99 @@
|
||||
import Component from "vue-class-component";
|
||||
import Vue from "vue";
|
||||
import { Stop, StopGroup, StopGroups } from "../model";
|
||||
import urls from '../urls';
|
||||
|
||||
import picker = require("../../components/picker.html");
|
||||
import finder = require('../../components/finder.html');
|
||||
import stop = require('../../components/stop.html');
|
||||
|
||||
import { Prop, Watch } from "vue-property-decorator";
|
||||
import { filter, map } from "../utils";
|
||||
import { debounce, throttle } from "../decorators";
|
||||
import { Departures } from "./departures";
|
||||
|
||||
@Component({ template: picker })
|
||||
export class PickerComponent extends Vue {
|
||||
protected stops?: Stop[] = [{
|
||||
"id": 2001,
|
||||
"name": "Dworzec Główny",
|
||||
"description": null,
|
||||
"location": [54.35544, 18.64565],
|
||||
"variant": "01",
|
||||
"onDemand": false
|
||||
}, {
|
||||
"id": 2002,
|
||||
"name": "Dworzec Główny",
|
||||
"description": null,
|
||||
"location": [54.35541, 18.64548],
|
||||
"variant": "02",
|
||||
"onDemand": false
|
||||
}];
|
||||
|
||||
private remove(stop: Stop) {
|
||||
this.stops = this.stops.filter(s => s != stop);
|
||||
}
|
||||
|
||||
private add(stop: Stop) {
|
||||
this.stops.push(stop);
|
||||
}
|
||||
}
|
||||
|
||||
type FinderState = 'fetching' | 'ready' | 'error';
|
||||
|
||||
@Component({ template: finder })
|
||||
export class FinderComponent extends Vue {
|
||||
protected found?: StopGroups = {};
|
||||
|
||||
public state: FinderState = 'ready';
|
||||
public filter: string = "";
|
||||
|
||||
@Prop({default: [], type: Array})
|
||||
public blacklist: Stop[];
|
||||
|
||||
get filtered(): StopGroups {
|
||||
const groups = map(
|
||||
this.found,
|
||||
(group: StopGroup, name: string) =>
|
||||
group.filter(stop => !this.blacklist.some(blacklisted => blacklisted.id == stop.id))
|
||||
) as StopGroups;
|
||||
|
||||
return filter(groups, group => group.length > 0);
|
||||
}
|
||||
|
||||
@Watch('filter')
|
||||
@debounce(400)
|
||||
async fetch() {
|
||||
if (this.filter.length < 3) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.state = 'fetching';
|
||||
|
||||
const response = await fetch(urls.prepare(urls.stops.search, {
|
||||
name: this.filter,
|
||||
provider: 'gdansk'
|
||||
}));
|
||||
|
||||
if (response.ok) {
|
||||
this.found = await response.json();
|
||||
this.state = 'ready';
|
||||
} else {
|
||||
this.state = 'error';
|
||||
}
|
||||
}
|
||||
|
||||
private select(stop) {
|
||||
this.$emit('select', stop);
|
||||
}
|
||||
}
|
||||
|
||||
@Component({ template: stop })
|
||||
export class StopComponent extends Vue {
|
||||
@Prop(Object)
|
||||
public stop: Stop;
|
||||
}
|
||||
|
||||
Vue.component('StopPicker', PickerComponent);
|
||||
Vue.component('StopFinder', FinderComponent);
|
||||
Vue.component('Stop', StopComponent);
|
45
resources/ts/decorators.ts
Normal file
45
resources/ts/decorators.ts
Normal file
@ -0,0 +1,45 @@
|
||||
export interface Decorator<TArgs extends any[], FArgs extends any[], TRet, FRet> {
|
||||
decorate(f: (...farg: FArgs) => FRet, ...args: TArgs): (...farg: FArgs) => TRet;
|
||||
|
||||
(...args: TArgs): (target, name: string | symbol, descriptor: TypedPropertyDescriptor<(...farg: FArgs) => FRet>) => void;
|
||||
}
|
||||
|
||||
export function decorator<TArgs extends any[], FArgs extends any[], TRet, FRet>
|
||||
(decorate: (f: (...fargs: FArgs) => FRet, ...args: TArgs) => (...fargs: FArgs) => TRet)
|
||||
: Decorator<TArgs, FArgs, TRet, FRet> {
|
||||
|
||||
const factory = function (this: Decorator<TArgs, FArgs, TRet, FRet>, ...args: TArgs) {
|
||||
return (target, name: string | symbol, descriptor: PropertyDescriptor) => {
|
||||
descriptor.value = decorate(descriptor.value, ...args);
|
||||
}
|
||||
} as Decorator<TArgs, FArgs, TRet, FRet>;
|
||||
factory.decorate = decorate;
|
||||
|
||||
return factory;
|
||||
}
|
||||
|
||||
export const throttle = decorator(function (decorated, time: number) {
|
||||
let timeout;
|
||||
return function (this: any, ...args) {
|
||||
if (typeof timeout === 'undefined') {
|
||||
timeout = window.setTimeout(() => {
|
||||
decorated.call(this, ...args);
|
||||
timeout = undefined;
|
||||
}, time);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export const debounce = decorator(function (decorated, time: number, max: number = time * 3) {
|
||||
let timeout;
|
||||
return function (this: any, ...args) {
|
||||
if (typeof timeout !== 'undefined') {
|
||||
window.clearTimeout(timeout);
|
||||
}
|
||||
|
||||
timeout = window.setTimeout(() => {
|
||||
timeout = undefined;
|
||||
decorated.call(this, ...args);
|
||||
}, time);
|
||||
}
|
||||
});
|
4
resources/ts/filters.ts
Normal file
4
resources/ts/filters.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { signed } from "./utils";
|
||||
import Vue from 'vue';
|
||||
|
||||
Vue.filter('signed', signed);
|
14
resources/ts/font-awesome.ts
Normal file
14
resources/ts/font-awesome.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import Vue from 'vue'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
|
||||
import { far } from "@fortawesome/pro-regular-svg-icons";
|
||||
import { fas } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { fal } from "@fortawesome/pro-light-svg-icons";
|
||||
import { fac } from "./icons";
|
||||
|
||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||
|
||||
library.add(far, fas, fal, fac);
|
||||
|
||||
Vue.component('fa', FontAwesomeIcon);
|
48
resources/ts/icons.ts
Normal file
48
resources/ts/icons.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { IconPack, IconDefinition } from '@fortawesome/fontawesome-svg-core';
|
||||
|
||||
import bus = require("../icons/light/bus.svg");
|
||||
import tram = require("../icons/light/tram.svg");
|
||||
import trolleybus = require("../icons/light/trolleybus.svg");
|
||||
import metro = require("../icons/light/metro.svg");
|
||||
import train = require("../icons/light/train.svg");
|
||||
import unknown = require("../icons/light/unknown.svg");
|
||||
|
||||
export const faBus: IconDefinition = <any>{
|
||||
prefix: 'fac',
|
||||
iconName: 'bus',
|
||||
icon: [ 512, 512, [], null, bus]
|
||||
};
|
||||
|
||||
export const faTram = <any>{
|
||||
prefix: 'fac',
|
||||
iconName: 'tram',
|
||||
icon: [ 512, 512, [], null, tram]
|
||||
};
|
||||
|
||||
export const faTrain = <any>{
|
||||
prefix: 'fac',
|
||||
iconName: 'train',
|
||||
icon: [ 512, 512, [], null, train]
|
||||
};
|
||||
|
||||
export const faTrolleybus = <any>{
|
||||
prefix: 'fac',
|
||||
iconName: 'trolleybus',
|
||||
icon: [ 512, 512, [], null, trolleybus]
|
||||
};
|
||||
|
||||
export const faMetro = <any>{
|
||||
prefix: 'fac',
|
||||
iconName: 'metro',
|
||||
icon: [ 512, 512, [], null, metro]
|
||||
};
|
||||
|
||||
export const faUnknown = <any>{
|
||||
prefix: 'fac',
|
||||
iconName: 'unknown',
|
||||
icon: [ 512, 512, [], null, unknown]
|
||||
};
|
||||
|
||||
export const fac: IconPack = {
|
||||
faBus, faTram, faTrain, faTrolleybus, faMetro, faUnknown
|
||||
};
|
14
resources/ts/model/departure.ts
Normal file
14
resources/ts/model/departure.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { Stop } from "./stop";
|
||||
import { Line } from "./line";
|
||||
import { Moment } from "moment";
|
||||
|
||||
export interface Departure {
|
||||
display: string;
|
||||
estimated: Moment;
|
||||
scheduled: Moment;
|
||||
stop: Stop;
|
||||
line: Line;
|
||||
delay: number;
|
||||
|
||||
vehicle?: string;
|
||||
}
|
3
resources/ts/model/index.ts
Normal file
3
resources/ts/model/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './stop'
|
||||
export * from './departure'
|
||||
export * from './line'
|
8
resources/ts/model/line.ts
Normal file
8
resources/ts/model/line.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export type LineType = "tram" | "bus" | "trolleybus" | "train" | "other";
|
||||
|
||||
export interface Line {
|
||||
id: any;
|
||||
symbol: string;
|
||||
variant?: string;
|
||||
type: LineType;
|
||||
}
|
14
resources/ts/model/stop.ts
Normal file
14
resources/ts/model/stop.ts
Normal file
@ -0,0 +1,14 @@
|
||||
export interface Stop {
|
||||
id: any;
|
||||
name: string;
|
||||
description?: string;
|
||||
location?: [ number, number ];
|
||||
onDemand?: boolean;
|
||||
variant?: string;
|
||||
}
|
||||
|
||||
export type StopGroup = Stop[];
|
||||
|
||||
export type StopGroups = {
|
||||
[name: string]: StopGroup;
|
||||
}
|
131
resources/ts/types/popper.js.d.ts
vendored
Normal file
131
resources/ts/types/popper.js.d.ts
vendored
Normal file
@ -0,0 +1,131 @@
|
||||
declare class Popper {
|
||||
static modifiers: (Popper.BaseModifier & { name: string })[];
|
||||
static placements: Popper.Placement[];
|
||||
static Defaults: Popper.PopperOptions;
|
||||
|
||||
options: Popper.PopperOptions;
|
||||
|
||||
constructor(reference: Element | Popper.ReferenceObject, popper: Element, options?: Popper.PopperOptions);
|
||||
|
||||
destroy(): void;
|
||||
update(): void;
|
||||
scheduleUpdate(): void;
|
||||
enableEventListeners(): void;
|
||||
disableEventListeners(): void;
|
||||
}
|
||||
|
||||
declare namespace Popper {
|
||||
export type Position = 'top' | 'right' | 'bottom' | 'left';
|
||||
|
||||
export type Placement = 'auto-start'
|
||||
| 'auto'
|
||||
| 'auto-end'
|
||||
| 'top-start'
|
||||
| 'top'
|
||||
| 'top-end'
|
||||
| 'right-start'
|
||||
| 'right'
|
||||
| 'right-end'
|
||||
| 'bottom-end'
|
||||
| 'bottom'
|
||||
| 'bottom-start'
|
||||
| 'left-end'
|
||||
| 'left'
|
||||
| 'left-start';
|
||||
|
||||
export type Boundary = 'scrollParent' | 'viewport' | 'window';
|
||||
|
||||
export type Behavior = 'flip' | 'clockwise' | 'counterclockwise';
|
||||
|
||||
export type ModifierFn = (data: Data, options: Object) => Data;
|
||||
|
||||
export interface BaseModifier {
|
||||
order?: number;
|
||||
enabled?: boolean;
|
||||
fn?: ModifierFn;
|
||||
}
|
||||
|
||||
export interface Modifiers {
|
||||
shift?: BaseModifier;
|
||||
offset?: BaseModifier & {
|
||||
offset?: number | string,
|
||||
};
|
||||
preventOverflow?: BaseModifier & {
|
||||
priority?: Position[],
|
||||
padding?: number,
|
||||
boundariesElement?: Boundary | Element,
|
||||
escapeWithReference?: boolean
|
||||
};
|
||||
keepTogether?: BaseModifier;
|
||||
arrow?: BaseModifier & {
|
||||
element?: string | Element,
|
||||
};
|
||||
flip?: BaseModifier & {
|
||||
behavior?: Behavior | Position[],
|
||||
padding?: number,
|
||||
boundariesElement?: Boundary | Element,
|
||||
};
|
||||
inner?: BaseModifier;
|
||||
hide?: BaseModifier;
|
||||
applyStyle?: BaseModifier & {
|
||||
onLoad?: Function,
|
||||
gpuAcceleration?: boolean,
|
||||
};
|
||||
computeStyle?: BaseModifier & {
|
||||
gpuAcceleration?: boolean;
|
||||
x?: 'bottom' | 'top',
|
||||
y?: 'left' | 'right'
|
||||
};
|
||||
|
||||
[name: string]: (BaseModifier & Record<string, any>) | undefined;
|
||||
}
|
||||
|
||||
export interface Offset {
|
||||
top: number;
|
||||
left: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export interface Data {
|
||||
instance: Popper;
|
||||
placement: Placement;
|
||||
originalPlacement: Placement;
|
||||
flipped: boolean;
|
||||
hide: boolean;
|
||||
arrowElement: Element;
|
||||
styles: CSSStyleDeclaration;
|
||||
boundaries: Object;
|
||||
offsets: {
|
||||
popper: Offset,
|
||||
reference: Offset,
|
||||
arrow: {
|
||||
top: number,
|
||||
left: number,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export interface PopperOptions {
|
||||
placement?: Placement;
|
||||
positionFixed?: boolean;
|
||||
eventsEnabled?: boolean;
|
||||
modifiers?: Modifiers;
|
||||
removeOnDestroy?: boolean;
|
||||
|
||||
onCreate?(data: Data): void;
|
||||
|
||||
onUpdate?(data: Data): void;
|
||||
}
|
||||
|
||||
export interface ReferenceObject {
|
||||
clientHeight: number;
|
||||
clientWidth: number;
|
||||
|
||||
getBoundingClientRect(): ClientRect;
|
||||
}
|
||||
}
|
||||
|
||||
declare module "popper.js" {
|
||||
export = Popper;
|
||||
}
|
9
resources/ts/types/webpack.d.ts
vendored
Normal file
9
resources/ts/types/webpack.d.ts
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
declare module "*.html" {
|
||||
const content: string;
|
||||
export = content;
|
||||
}
|
||||
|
||||
declare module "*.svg" {
|
||||
const content: string;
|
||||
export = content;
|
||||
}
|
56
resources/ts/urls.ts
Normal file
56
resources/ts/urls.ts
Normal file
@ -0,0 +1,56 @@
|
||||
export type UrlParams = {
|
||||
[name: string]: any
|
||||
}
|
||||
|
||||
type ParamValuePair = [string, string];
|
||||
|
||||
export function query(params: UrlParams = { }) {
|
||||
function *simplify(name: string, param: any): IterableIterator<ParamValuePair> {
|
||||
if (typeof param === 'string') {
|
||||
yield [ name, param ];
|
||||
} else if (typeof param === 'number') {
|
||||
yield [ name, param.toString() ];
|
||||
} else if (param instanceof Array) {
|
||||
for (let entry of param) {
|
||||
yield* simplify(`${name}[]`, entry);
|
||||
}
|
||||
} else if (typeof param === "object") {
|
||||
for (let [key, entry] of Object.entries(param)) {
|
||||
yield* simplify(`${name}[${key}]`, entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let simplified: ParamValuePair[] = [];
|
||||
for (const [key, entry] of Object.entries(params)) {
|
||||
for (const pair of simplify(key, entry)) {
|
||||
simplified.push(pair);
|
||||
}
|
||||
}
|
||||
|
||||
return Object.values(simplified).map(entry => entry.map(encodeURIComponent).join('=')).join('&');
|
||||
}
|
||||
|
||||
export function prepare(url: string, params: UrlParams = { }) {
|
||||
const regex = /\{([\w-]+)\}/gi;
|
||||
|
||||
let group;
|
||||
while (group = regex.exec(url)) {
|
||||
const name = group[1];
|
||||
|
||||
url = url.replace(new RegExp(`\{${name}\}`, 'gi'), params[name]);
|
||||
delete params[name];
|
||||
}
|
||||
|
||||
return Object.keys(params).length > 0 ? `${url}?${query(params)}` : url;
|
||||
}
|
||||
|
||||
export default {
|
||||
departures: '/{provider}/departures',
|
||||
stops: {
|
||||
all: '/{provider}/stops',
|
||||
search: '/{provider}/stops/search',
|
||||
get: '/{provider}/stop/{id}'
|
||||
},
|
||||
prepare
|
||||
}
|
37
resources/ts/utils.ts
Normal file
37
resources/ts/utils.ts
Normal file
@ -0,0 +1,37 @@
|
||||
type Simplify<T, K = any> = string |
|
||||
T extends string ? string :
|
||||
T extends number ? number :
|
||||
T extends boolean ? boolean :
|
||||
T extends Array<K> ? Array<K> :
|
||||
T extends Object ? Object : any;
|
||||
|
||||
export type Jsonified<T> = { [K in keyof T]: Simplify<T[K]> }
|
||||
export type Optionalify<T> = { [K in keyof T]?: T[K] }
|
||||
|
||||
export type Index = string | symbol | number;
|
||||
|
||||
export function map<T extends {}, KT extends keyof T, R extends { [KR in keyof T] }>(source: T, mapper: (value: T[KT], key: KT) => R[KT]): R {
|
||||
const result: R = {} as R;
|
||||
|
||||
for (const [key, value] of Object.entries(source)) {
|
||||
result[key] = mapper(value as T[KT], key as KT);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function filter<T, KT extends keyof T>(source: T, filter: (value: T[KT], key: KT) => boolean): Optionalify<T> {
|
||||
const result: Optionalify<T> = {};
|
||||
|
||||
for (const [key, value] of Object.entries(source)) {
|
||||
if (filter(value as T[KT], key as KT)) {
|
||||
result[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function signed(number: number): string {
|
||||
return number > 0 ? `+${number}` : number.toString();
|
||||
}
|
0
src/Controller/.gitignore
vendored
Normal file
0
src/Controller/.gitignore
vendored
Normal file
11
src/Controller/Controller.php
Normal file
11
src/Controller/Controller.php
Normal file
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\Controller as SymfonyController;
|
||||
|
||||
abstract class Controller extends SymfonyController
|
||||
{
|
||||
}
|
43
src/Controller/DeparturesController.php
Normal file
43
src/Controller/DeparturesController.php
Normal file
@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Model\Departure;
|
||||
use App\Provider\DepartureRepository;
|
||||
use App\Provider\StopRepository;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
* Class DeparturesController
|
||||
*
|
||||
* @Route("/{provider}/departures")
|
||||
*/
|
||||
class DeparturesController extends Controller
|
||||
{
|
||||
/**
|
||||
* @Route("/{id}")
|
||||
*/
|
||||
public function stop(DepartureRepository $departures, StopRepository $stops, $id)
|
||||
{
|
||||
$stop = $stops->getById($id);
|
||||
|
||||
return $this->json($departures->getForStop($stop));
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/")
|
||||
*/
|
||||
public function stops(DepartureRepository $departures, StopRepository $stops, Request $request)
|
||||
{
|
||||
$stops = collect($request->query->get('stop'))
|
||||
->map([ $stops, 'getById' ])
|
||||
->flatMap([ $departures, 'getForStop' ])
|
||||
->sortBy(function (Departure $departure) {
|
||||
return $departure->getEstimated();
|
||||
});
|
||||
|
||||
return $this->json($stops->values());
|
||||
}
|
||||
}
|
17
src/Controller/HomepageController.php
Normal file
17
src/Controller/HomepageController.php
Normal file
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
class HomepageController extends Controller
|
||||
{
|
||||
/**
|
||||
* @Route("/", name="home")
|
||||
*/
|
||||
public function homepage()
|
||||
{
|
||||
return $this->render('base.html.twig');
|
||||
}
|
||||
}
|
49
src/Controller/StopsController.php
Normal file
49
src/Controller/StopsController.php
Normal file
@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Provider\StopRepository;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
* Class StopsController
|
||||
*
|
||||
* @package App\Controller
|
||||
* @Route("/{provider}/stops")
|
||||
*/
|
||||
class StopsController extends Controller
|
||||
{
|
||||
/**
|
||||
* @Route("/", methods={"GET"})
|
||||
*/
|
||||
public function index(Request $request, StopRepository $stops)
|
||||
{
|
||||
$result = $request->query->has('id')
|
||||
? $stops->getManyById($request->query->get('id'))
|
||||
: $stops->getAllGroups();
|
||||
|
||||
return $this->json($result->all());
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/search", methods={"GET"})
|
||||
*/
|
||||
public function find(Request $request, StopRepository $stops)
|
||||
{
|
||||
$result = $request->query->has('name')
|
||||
? $stops->findGroupsByName($request->query->get('name'))
|
||||
: $stops->getAllGroups();
|
||||
|
||||
return $this->json($result->all());
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/{id}", methods={"GET"})
|
||||
*/
|
||||
public function one(Request $request, StopRepository $stops, $id)
|
||||
{
|
||||
return $this->json($stops->getById($id));
|
||||
}
|
||||
}
|
10
src/Exception/NonExistentServiceException.php
Normal file
10
src/Exception/NonExistentServiceException.php
Normal file
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace App\Exception;
|
||||
|
||||
|
||||
class NonExistentServiceException extends \Exception
|
||||
{
|
||||
|
||||
}
|
61
src/Kernel.php
Normal file
61
src/Kernel.php
Normal file
@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
|
||||
use Symfony\Component\Config\Loader\LoaderInterface;
|
||||
use Symfony\Component\Config\Resource\FileResource;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
|
||||
use Symfony\Component\Routing\RouteCollectionBuilder;
|
||||
|
||||
class Kernel extends BaseKernel
|
||||
{
|
||||
use MicroKernelTrait;
|
||||
|
||||
const CONFIG_EXTS = '.{php,xml,yaml,yml}';
|
||||
|
||||
public function getCacheDir()
|
||||
{
|
||||
return $this->getProjectDir().'/var/cache/'.$this->environment;
|
||||
}
|
||||
|
||||
public function getLogDir()
|
||||
{
|
||||
return $this->getProjectDir().'/var/log';
|
||||
}
|
||||
|
||||
public function registerBundles()
|
||||
{
|
||||
$contents = require $this->getProjectDir().'/config/bundles.php';
|
||||
foreach ($contents as $class => $envs) {
|
||||
if (isset($envs['all']) || isset($envs[$this->environment])) {
|
||||
yield new $class();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader)
|
||||
{
|
||||
$container->addResource(new FileResource($this->getProjectDir().'/config/bundles.php'));
|
||||
// Feel free to remove the "container.autowiring.strict_mode" parameter
|
||||
// if you are using symfony/dependency-injection 4.0+ as it's the default behavior
|
||||
$container->setParameter('container.autowiring.strict_mode', true);
|
||||
$container->setParameter('container.dumper.inline_class_loader', true);
|
||||
$confDir = $this->getProjectDir().'/config';
|
||||
|
||||
$loader->load($confDir.'/{packages}/*'.self::CONFIG_EXTS, 'glob');
|
||||
$loader->load($confDir.'/{packages}/'.$this->environment.'/**/*'.self::CONFIG_EXTS, 'glob');
|
||||
$loader->load($confDir.'/{services}'.self::CONFIG_EXTS, 'glob');
|
||||
$loader->load($confDir.'/{services}_'.$this->environment.self::CONFIG_EXTS, 'glob');
|
||||
}
|
||||
|
||||
protected function configureRoutes(RouteCollectionBuilder $routes)
|
||||
{
|
||||
$confDir = $this->getProjectDir().'/config';
|
||||
|
||||
$routes->import($confDir.'/{routes}/*'.self::CONFIG_EXTS, '/', 'glob');
|
||||
$routes->import($confDir.'/{routes}/'.$this->environment.'/**/*'.self::CONFIG_EXTS, '/', 'glob');
|
||||
$routes->import($confDir.'/{routes}'.self::CONFIG_EXTS, '/', 'glob');
|
||||
}
|
||||
}
|
26
src/Model/AbstractModel.php
Normal file
26
src/Model/AbstractModel.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace App\Model;
|
||||
|
||||
|
||||
abstract class AbstractModel implements Fillable, Referable
|
||||
{
|
||||
use FillTrait;
|
||||
|
||||
private $id;
|
||||
|
||||
protected function setId($id)
|
||||
{
|
||||
$this->id = $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
}
|
116
src/Model/Departure.php
Normal file
116
src/Model/Departure.php
Normal file
@ -0,0 +1,116 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace App\Model;
|
||||
|
||||
|
||||
use Carbon\Carbon;
|
||||
|
||||
class Departure implements Fillable
|
||||
{
|
||||
use FillTrait;
|
||||
|
||||
/**
|
||||
* Information about line
|
||||
*
|
||||
* @var \App\Model\Line
|
||||
*/
|
||||
private $line;
|
||||
|
||||
/**
|
||||
* Information about stop
|
||||
* @var \App\Model\Stop
|
||||
*/
|
||||
private $stop;
|
||||
|
||||
/**
|
||||
* Vehicle identification
|
||||
* @var string|null
|
||||
*/
|
||||
private $vehicle;
|
||||
|
||||
/**
|
||||
* Displayed destination
|
||||
* @var string|null
|
||||
*/
|
||||
private $display;
|
||||
|
||||
/**
|
||||
* Estimated time of departure, null if case of no realtime data
|
||||
* @var Carbon|null
|
||||
*/
|
||||
private $estimated;
|
||||
|
||||
/**
|
||||
* Scheduled time of departure
|
||||
* @var Carbon
|
||||
*/
|
||||
private $scheduled;
|
||||
|
||||
public function getLine(): Line
|
||||
{
|
||||
return $this->line;
|
||||
}
|
||||
|
||||
public function setLine(Line $line): void
|
||||
{
|
||||
$this->line = $line;
|
||||
}
|
||||
|
||||
public function getVehicle(): ?string
|
||||
{
|
||||
return $this->vehicle;
|
||||
}
|
||||
|
||||
public function setVehicle(?string $vehicle): void
|
||||
{
|
||||
$this->vehicle = $vehicle;
|
||||
}
|
||||
|
||||
public function getDisplay(): ?string
|
||||
{
|
||||
return $this->display;
|
||||
}
|
||||
|
||||
public function setDisplay(?string $display): void
|
||||
{
|
||||
$this->display = $display;
|
||||
}
|
||||
|
||||
public function getEstimated(): ?Carbon
|
||||
{
|
||||
return $this->estimated;
|
||||
}
|
||||
|
||||
public function setEstimated(?Carbon $estimated): void
|
||||
{
|
||||
$this->estimated = $estimated;
|
||||
}
|
||||
|
||||
public function getScheduled(): Carbon
|
||||
{
|
||||
return $this->scheduled;
|
||||
}
|
||||
|
||||
public function setScheduled(Carbon $scheduled): void
|
||||
{
|
||||
$this->scheduled = $scheduled;
|
||||
}
|
||||
|
||||
public function getStop(): Stop
|
||||
{
|
||||
return $this->stop;
|
||||
}
|
||||
|
||||
public function setStop(Stop $stop): void
|
||||
{
|
||||
$this->stop = $stop;
|
||||
}
|
||||
|
||||
public function getDelay(): ?int
|
||||
{
|
||||
return $this->getEstimated()
|
||||
? $this->getScheduled()->diffInSeconds($this->getEstimated(), false)
|
||||
: null;
|
||||
}
|
||||
}
|
34
src/Model/FillTrait.php
Normal file
34
src/Model/FillTrait.php
Normal file
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace App\Model;
|
||||
|
||||
|
||||
trait FillTrait
|
||||
{
|
||||
public function fill(array $vars = [])
|
||||
{
|
||||
foreach ($vars as $name => $value) {
|
||||
switch (true) {
|
||||
case method_exists($this, $setter = 'set' . strtoupper($name)):
|
||||
$this->{$setter}($value);
|
||||
break;
|
||||
|
||||
case property_exists($this, $name) && (new \ReflectionProperty($this, $name))->isPublic():
|
||||
$this->$name = $value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static function createFromArray(array $vars = [], ...$args)
|
||||
{
|
||||
$object = empty($args)
|
||||
? (new \ReflectionClass(static::class))->newInstanceWithoutConstructor()
|
||||
: new static(...$args);
|
||||
|
||||
$object->fill($vars);
|
||||
|
||||
return $object;
|
||||
}
|
||||
}
|
11
src/Model/Fillable.php
Normal file
11
src/Model/Fillable.php
Normal file
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace App\Model;
|
||||
|
||||
|
||||
interface Fillable
|
||||
{
|
||||
public function fill(array $vars = []);
|
||||
public static function createFromArray(array $vars = [], ...$args);
|
||||
}
|
81
src/Model/Line.php
Normal file
81
src/Model/Line.php
Normal file
@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace App\Model;
|
||||
|
||||
|
||||
class Line implements Fillable, Referable
|
||||
{
|
||||
use FillTrait, ReferenceTrait;
|
||||
|
||||
const TYPE_TRAM = 'tram';
|
||||
const TYPE_BUS = 'bus';
|
||||
const TYPE_TRAIN = 'train';
|
||||
const TYPE_METRO = 'metro';
|
||||
const TYPE_TROLLEYBUS = 'trolleybus';
|
||||
const TYPE_UNKNOWN = 'unknown';
|
||||
|
||||
/**
|
||||
* Some kind of identification for provider
|
||||
* @var mixed
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* Line symbol, for example '10', or 'A'
|
||||
* @var string
|
||||
*/
|
||||
private $symbol;
|
||||
|
||||
/**
|
||||
* Line variant, for example 'a'
|
||||
* @var string|null
|
||||
*/
|
||||
private $variant;
|
||||
|
||||
/**
|
||||
* Line type tram, bus or whatever.
|
||||
* @var string
|
||||
*/
|
||||
private $type;
|
||||
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function setId($id): void
|
||||
{
|
||||
$this->id = $id;
|
||||
}
|
||||
|
||||
public function getSymbol(): string
|
||||
{
|
||||
return $this->symbol;
|
||||
}
|
||||
|
||||
public function setSymbol(string $symbol): void
|
||||
{
|
||||
$this->symbol = $symbol;
|
||||
}
|
||||
|
||||
public function getVariant(): ?string
|
||||
{
|
||||
return $this->variant;
|
||||
}
|
||||
|
||||
public function setVariant(?string $variant): void
|
||||
{
|
||||
$this->variant = $variant;
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
public function setType(string $type): void
|
||||
{
|
||||
$this->type = $type;
|
||||
}
|
||||
}
|
58
src/Model/Message.php
Normal file
58
src/Model/Message.php
Normal file
@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace App\Model;
|
||||
|
||||
|
||||
class Message implements Fillable
|
||||
{
|
||||
use FillTrait;
|
||||
|
||||
const TYPE_INFO = 'info';
|
||||
const TYPE_BREAKDOWN = 'breakdown';
|
||||
const TYPE_UNKNOWN = 'unknown';
|
||||
|
||||
/**
|
||||
* Message content.
|
||||
* @var string
|
||||
*/
|
||||
public $message;
|
||||
|
||||
/**
|
||||
* Message type, see TYPE_* constants
|
||||
* @var
|
||||
*/
|
||||
public $type = self::TYPE_UNKNOWN;
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getMessage(): string
|
||||
{
|
||||
return $this->message;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $message
|
||||
*/
|
||||
public function setMessage(string $message): void
|
||||
{
|
||||
$this->message = $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getType()
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $type
|
||||
*/
|
||||
public function setType($type): void
|
||||
{
|
||||
$this->type = $type;
|
||||
}
|
||||
}
|
10
src/Model/Referable.php
Normal file
10
src/Model/Referable.php
Normal file
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace App\Model;
|
||||
|
||||
|
||||
interface Referable
|
||||
{
|
||||
public function getId();
|
||||
}
|
18
src/Model/ReferenceTrait.php
Normal file
18
src/Model/ReferenceTrait.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace App\Model;
|
||||
|
||||
|
||||
trait ReferenceTrait
|
||||
{
|
||||
abstract protected function setId($id);
|
||||
|
||||
public static function reference($id)
|
||||
{
|
||||
$reference = new static();
|
||||
$reference->setId($id);
|
||||
|
||||
return $reference;
|
||||
}
|
||||
}
|
142
src/Model/Stop.php
Normal file
142
src/Model/Stop.php
Normal file
@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace App\Model;
|
||||
|
||||
|
||||
class Stop implements Fillable, Referable
|
||||
{
|
||||
use FillTrait, ReferenceTrait;
|
||||
|
||||
/**
|
||||
* Some unique stop identification
|
||||
* @var mixed
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* Stop name
|
||||
* @var string
|
||||
*/
|
||||
private $name;
|
||||
|
||||
/**
|
||||
* Optional stop description
|
||||
* @var string|null
|
||||
*/
|
||||
private $description;
|
||||
|
||||
/**
|
||||
* Optional stop variant - for example number of shed
|
||||
* @var string|null
|
||||
*/
|
||||
private $variant;
|
||||
|
||||
/**
|
||||
* Tuple of lat and long
|
||||
* @var [float, float]
|
||||
*/
|
||||
private $location;
|
||||
|
||||
/**
|
||||
* True if stop is available only on demand
|
||||
* @var bool
|
||||
*/
|
||||
private $onDemand;
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $id
|
||||
*/
|
||||
public function setId($id): void
|
||||
{
|
||||
$this->id = $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
*/
|
||||
public function setName(string $name): void
|
||||
{
|
||||
$this->name = $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return null|string
|
||||
*/
|
||||
public function getDescription(): ?string
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param null|string $description
|
||||
*/
|
||||
public function setDescription(?string $description): void
|
||||
{
|
||||
$this->description = $description;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getLocation()
|
||||
{
|
||||
return $this->location;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $location
|
||||
*/
|
||||
public function setLocation($location): void
|
||||
{
|
||||
$this->location = $location;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return null|string
|
||||
*/
|
||||
public function getVariant(): ?string
|
||||
{
|
||||
return $this->variant;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param null|string $variant
|
||||
*/
|
||||
public function setVariant(?string $variant): void
|
||||
{
|
||||
$this->variant = $variant;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isOnDemand(): bool
|
||||
{
|
||||
return $this->onDemand;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $onDemand
|
||||
*/
|
||||
public function setOnDemand(bool $onDemand): void
|
||||
{
|
||||
$this->onDemand = $onDemand;
|
||||
}
|
||||
}
|
32
src/Model/StopGroup.php
Normal file
32
src/Model/StopGroup.php
Normal file
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace App\Model;
|
||||
|
||||
|
||||
use Tightenco\Collect\Support\Collection;
|
||||
|
||||
class StopGroup extends Collection
|
||||
{
|
||||
/**
|
||||
* Name of stop group
|
||||
* @var string
|
||||
*/
|
||||
private $name;
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
*/
|
||||
public function setName(string $name): void
|
||||
{
|
||||
$this->name = $name;
|
||||
}
|
||||
}
|
13
src/Provider/DepartureRepository.php
Normal file
13
src/Provider/DepartureRepository.php
Normal file
@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace App\Provider;
|
||||
|
||||
|
||||
use App\Model\Stop;
|
||||
use Tightenco\Collect\Support\Collection;
|
||||
|
||||
interface DepartureRepository extends Repository
|
||||
{
|
||||
public function getForStop(Stop $stop): Collection;
|
||||
}
|
16
src/Provider/LineRepository.php
Normal file
16
src/Provider/LineRepository.php
Normal file
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace App\Provider;
|
||||
|
||||
|
||||
use App\Model\Line;
|
||||
use Tightenco\Collect\Support\Collection;
|
||||
|
||||
interface LineRepository extends Repository
|
||||
{
|
||||
public function getAll(): Collection;
|
||||
|
||||
public function getById($id): ?Line;
|
||||
public function getManyById($ids): Collection;
|
||||
}
|
14
src/Provider/MessageRepository.php
Normal file
14
src/Provider/MessageRepository.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace App\Provider;
|
||||
|
||||
|
||||
use App\Model\Stop;
|
||||
use Tightenco\Collect\Support\Collection;
|
||||
|
||||
interface MessageRepository
|
||||
{
|
||||
public function getAll(): Collection;
|
||||
public function getForStop(Stop $stop): Collection;
|
||||
}
|
11
src/Provider/Provider.php
Normal file
11
src/Provider/Provider.php
Normal file
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Provider;
|
||||
|
||||
interface Provider
|
||||
{
|
||||
public function getDepartureRepository(): DepartureRepository;
|
||||
public function getLineRepository(): LineRepository;
|
||||
public function getStopRepository(): StopRepository;
|
||||
public function getMessageRepository(): MessageRepository;
|
||||
}
|
10
src/Provider/Repository.php
Normal file
10
src/Provider/Repository.php
Normal file
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace App\Provider;
|
||||
|
||||
|
||||
interface Repository
|
||||
{
|
||||
|
||||
}
|
18
src/Provider/StopRepository.php
Normal file
18
src/Provider/StopRepository.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace App\Provider;
|
||||
|
||||
|
||||
use App\Model\Stop;
|
||||
use Tightenco\Collect\Support\Collection;
|
||||
|
||||
interface StopRepository extends Repository
|
||||
{
|
||||
public function getAllGroups(): Collection;
|
||||
|
||||
public function getById($id): ?Stop;
|
||||
public function getManyById($ids): Collection;
|
||||
|
||||
public function findGroupsByName(string $name): Collection;
|
||||
}
|
56
src/Provider/ZtmGdansk/ZtmGdanskDepartureRepository.php
Normal file
56
src/Provider/ZtmGdansk/ZtmGdanskDepartureRepository.php
Normal file
@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace App\Provider\ZtmGdansk;
|
||||
|
||||
|
||||
use App\Model\Departure;
|
||||
use App\Model\Line;
|
||||
use App\Model\Stop;
|
||||
use App\Provider\DepartureRepository;
|
||||
use Carbon\Carbon;
|
||||
use Tightenco\Collect\Support\Collection;
|
||||
|
||||
class ZtmGdanskDepartureRepository implements DepartureRepository
|
||||
{
|
||||
const ESTIMATES_URL = 'http://87.98.237.99:88/delays';
|
||||
|
||||
private $lines;
|
||||
|
||||
/**
|
||||
* ZtmGdanskDepartureRepository constructor.
|
||||
*
|
||||
* @param $lines
|
||||
*/
|
||||
public function __construct(ZtmGdanskLineRepository $lines)
|
||||
{
|
||||
$this->lines = $lines;
|
||||
}
|
||||
|
||||
public function getForStop(Stop $stop): Collection
|
||||
{
|
||||
try {
|
||||
$estimates = json_decode(file_get_contents(static::ESTIMATES_URL . "?stopId=" . $stop->getId()), true)['delay'];
|
||||
|
||||
return collect($estimates)->map(function ($delay) use ($stop) {
|
||||
$scheduled = new Carbon($delay['theoreticalTime']);
|
||||
$estimated = (clone $scheduled)->addSeconds($delay['delayInSeconds']);
|
||||
|
||||
return Departure::createFromArray([
|
||||
'scheduled' => $scheduled,
|
||||
'estimated' => $estimated,
|
||||
'stop' => $stop,
|
||||
'display' => trim($delay['headsign']),
|
||||
'vehicle' => $delay['vehicleCode'],
|
||||
'line' => Line::createFromArray([
|
||||
'id' => $delay['id'],
|
||||
'symbol' => $delay['routeId'],
|
||||
'type' => $delay['routeId'] > 1 && $delay['routeId'] <= 12 ? Line::TYPE_TRAM : Line::TYPE_BUS
|
||||
])
|
||||
]);
|
||||
})->values();
|
||||
} catch (\Throwable $error) {
|
||||
return collect();
|
||||
}
|
||||
}
|
||||
}
|
27
src/Provider/ZtmGdansk/ZtmGdanskLineRepository.php
Normal file
27
src/Provider/ZtmGdansk/ZtmGdanskLineRepository.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace App\Provider\ZtmGdansk;
|
||||
|
||||
|
||||
use App\Model\Line;
|
||||
use App\Provider\LineRepository;
|
||||
use Tightenco\Collect\Support\Collection;
|
||||
|
||||
class ZtmGdanskLineRepository implements LineRepository
|
||||
{
|
||||
public function getAll(): Collection
|
||||
{
|
||||
// TODO: Implement getAll() method.
|
||||
}
|
||||
|
||||
public function getById($id): Line
|
||||
{
|
||||
// TODO: Implement getById() method.
|
||||
}
|
||||
|
||||
public function getManyById($ids): Collection
|
||||
{
|
||||
// TODO: Implement getManyById() method.
|
||||
}
|
||||
}
|
23
src/Provider/ZtmGdansk/ZtmGdanskMessageRepository.php
Normal file
23
src/Provider/ZtmGdansk/ZtmGdanskMessageRepository.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace App\Provider\ZtmGdansk;
|
||||
|
||||
|
||||
use App\Model\Stop;
|
||||
use App\Provider\MessageRepository;
|
||||
use Tightenco\Collect\Support\Collection;
|
||||
|
||||
class ZtmGdanskMessageRepository implements MessageRepository
|
||||
{
|
||||
|
||||
public function getAll(): Collection
|
||||
{
|
||||
return collect();
|
||||
}
|
||||
|
||||
public function getForStop(Stop $stop): Collection
|
||||
{
|
||||
return collect();
|
||||
}
|
||||
}
|
108
src/Provider/ZtmGdansk/ZtmGdanskStopRepository.php
Normal file
108
src/Provider/ZtmGdansk/ZtmGdanskStopRepository.php
Normal file
@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
namespace App\Provider\ZtmGdansk;
|
||||
|
||||
use App\Model\Stop;
|
||||
use App\Model\StopGroup;
|
||||
use App\Provider\StopRepository;
|
||||
use Symfony\Component\Cache\Adapter\AdapterInterface;
|
||||
use Tightenco\Collect\Support\Collection;
|
||||
|
||||
class ZtmGdanskStopRepository implements StopRepository
|
||||
{
|
||||
const STOPS_URL = 'http://91.244.248.19/dataset/c24aa637-3619-4dc2-a171-a23eec8f2172/resource/cd4c08b5-460e-40db-b920-ab9fc93c1a92/download/stops.json';
|
||||
|
||||
private $cache;
|
||||
|
||||
/**
|
||||
* ZtmGdanskStopRepository constructor.
|
||||
*/
|
||||
public function __construct(AdapterInterface $cache)
|
||||
{
|
||||
$this->cache = $cache;
|
||||
}
|
||||
|
||||
public function getAllGroups(): Collection
|
||||
{
|
||||
$stops = $this->getAllStops();
|
||||
return $this->group($stops);
|
||||
}
|
||||
|
||||
public function findGroupsByName(string $name): Collection
|
||||
{
|
||||
if (empty($name)) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
$stops = $this->getAllStops();
|
||||
$stops = $stops->filter(function (Stop $stop) use ($name) {
|
||||
return stripos($stop->getName(), $name) !== false;
|
||||
});
|
||||
|
||||
return $this->group($stops);
|
||||
}
|
||||
|
||||
public function getAllStops(): Collection
|
||||
{
|
||||
static $stops = null;
|
||||
if ($stops === null) {
|
||||
$stops = collect($this->queryZtmApi())->map(function ($stop) {
|
||||
return Stop::createFromArray([
|
||||
'id' => $stop['stopId'],
|
||||
'name' => trim($stop['stopName'] ?? $stop['stopDesc']),
|
||||
'variant' => trim($stop['zoneName'] == 'Gdańsk' ? $stop['subName'] : null),
|
||||
'location' => [$stop['stopLat'], $stop['stopLon']],
|
||||
'onDemand' => (bool)$stop['onDemand'],
|
||||
]);
|
||||
})->keyBy(function (Stop $stop) {
|
||||
return $stop->getId();
|
||||
})->sort(function (Stop $a, Stop $b) {
|
||||
return (int)$a->getVariant() <=> (int)$b->getVariant();
|
||||
});
|
||||
}
|
||||
|
||||
return $stops;
|
||||
}
|
||||
|
||||
public function getById($id): ?Stop
|
||||
{
|
||||
return $this->getAllStops()->get($id);
|
||||
}
|
||||
|
||||
public function getManyById($ids): Collection
|
||||
{
|
||||
$stops = $this->getAllStops();
|
||||
|
||||
return collect($ids)->mapWithKeys(function ($id) use ($stops) {
|
||||
return [$id => $stops[$id]];
|
||||
});
|
||||
}
|
||||
|
||||
private function queryZtmApi()
|
||||
{
|
||||
$item = $this->cache->getItem('ztm-gdansk.stops');
|
||||
|
||||
if (!$item->isHit()) {
|
||||
$stops = json_decode(file_get_contents(static::STOPS_URL), true);
|
||||
|
||||
$item->expiresAfter(24 * 60 * 60);
|
||||
$item->set($stops[date('Y-m-d')]['stops']);
|
||||
|
||||
$this->cache->save($item);
|
||||
}
|
||||
|
||||
return $item->get();
|
||||
}
|
||||
|
||||
private function group(Collection $stops)
|
||||
{
|
||||
return $stops->groupBy(function (Stop $stop) {
|
||||
return $stop->getName();
|
||||
})->map(function ($group, $key) {
|
||||
$group = new StopGroup($group);
|
||||
$group->setName($key);
|
||||
|
||||
return $group;
|
||||
});
|
||||
}
|
||||
}
|
49
src/Provider/ZtmGdanskProvider.php
Normal file
49
src/Provider/ZtmGdanskProvider.php
Normal file
@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace App\Provider;
|
||||
|
||||
use App\Provider\ZtmGdansk\{ZtmGdanskDepartureRepository,
|
||||
ZtmGdanskLineRepository,
|
||||
ZtmGdanskMessageRepository,
|
||||
ZtmGdanskStopRepository};
|
||||
|
||||
class ZtmGdanskProvider implements Provider
|
||||
{
|
||||
private $lines;
|
||||
private $departures;
|
||||
private $stops;
|
||||
private $messages;
|
||||
|
||||
public function __construct(
|
||||
ZtmGdanskLineRepository $lines,
|
||||
ZtmGdanskDepartureRepository $departures,
|
||||
ZtmGdanskStopRepository $stops,
|
||||
ZtmGdanskMessageRepository $messages
|
||||
) {
|
||||
$this->lines = $lines;
|
||||
$this->departures = $departures;
|
||||
$this->stops = $stops;
|
||||
$this->messages = $messages;
|
||||
}
|
||||
|
||||
public function getDepartureRepository(): DepartureRepository
|
||||
{
|
||||
return $this->departures;
|
||||
}
|
||||
|
||||
public function getLineRepository(): LineRepository
|
||||
{
|
||||
return $this->lines;
|
||||
}
|
||||
|
||||
public function getStopRepository(): StopRepository
|
||||
{
|
||||
return $this->stops;
|
||||
}
|
||||
|
||||
public function getMessageRepository(): MessageRepository
|
||||
{
|
||||
return $this->messages;
|
||||
}
|
||||
}
|
43
src/Service/ProviderParameterConverter.php
Normal file
43
src/Service/ProviderParameterConverter.php
Normal file
@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
|
||||
use App\Exception\NonExistentServiceException;
|
||||
use App\Provider\Provider;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Request\ParamConverter\ParamConverterInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
class ProviderParameterConverter implements ParamConverterInterface
|
||||
{
|
||||
private $resolver;
|
||||
|
||||
/**
|
||||
* ProviderParameterConverter constructor.
|
||||
*
|
||||
* @param $resolver
|
||||
*/
|
||||
public function __construct(ProviderResolver $resolver)
|
||||
{
|
||||
$this->resolver = $resolver;
|
||||
}
|
||||
|
||||
public function apply(Request $request, ParamConverter $configuration)
|
||||
{
|
||||
$provider = $request->get('provider');
|
||||
|
||||
try {
|
||||
$request->attributes->set('provider', $this->resolver->resolve($provider));
|
||||
} catch (NonExistentServiceException $exception) {
|
||||
throw new NotFoundHttpException("There is no such provider as '$provider'.", $exception);
|
||||
}
|
||||
}
|
||||
|
||||
public function supports(ParamConverter $configuration)
|
||||
{
|
||||
return $configuration->getName() === 'provider' && is_a($configuration->getClass(), Provider::class, true);
|
||||
}
|
||||
}
|
46
src/Service/ProviderResolver.php
Normal file
46
src/Service/ProviderResolver.php
Normal file
@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
|
||||
use App\Exception\NonExistentServiceException;
|
||||
use App\Provider\Provider;
|
||||
use App\Provider\ZtmGdanskProvider;
|
||||
|
||||
class ProviderResolver
|
||||
{
|
||||
private const PROVIDER = [
|
||||
'gdansk' => ZtmGdanskProvider::class
|
||||
];
|
||||
|
||||
/**
|
||||
* @var \Symfony\Component\DependencyInjection\ContainerInterface
|
||||
*/
|
||||
private $container;
|
||||
|
||||
/**
|
||||
* ProviderResolver constructor.
|
||||
*
|
||||
* @param \Symfony\Component\DependencyInjection\ContainerInterface $container
|
||||
*/
|
||||
public function __construct(\Symfony\Component\DependencyInjection\ContainerInterface $container)
|
||||
{
|
||||
$this->container = $container;
|
||||
}
|
||||
|
||||
/**\
|
||||
* @param string $name
|
||||
*
|
||||
* @return \App\Provider\Provider
|
||||
* @throws \App\Exception\NonExistentServiceException
|
||||
*/
|
||||
public function resolve(string $name): Provider
|
||||
{
|
||||
if (!array_key_exists($name, static::PROVIDER)) {
|
||||
throw new NonExistentServiceException();
|
||||
}
|
||||
|
||||
return $this->container->get(static::PROVIDER[$name]);
|
||||
}
|
||||
}
|
77
src/Service/RepositoryParameterConverter.php
Normal file
77
src/Service/RepositoryParameterConverter.php
Normal file
@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
|
||||
use App\Exception\NonExistentServiceException;
|
||||
use App\Provider\DepartureRepository;
|
||||
use App\Provider\LineRepository;
|
||||
use App\Provider\StopRepository;
|
||||
use const Kadet\Functional\_;
|
||||
use function Kadet\Functional\any;
|
||||
use function Kadet\Functional\curry;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Request\ParamConverter\ParamConverterInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
class RepositoryParameterConverter implements ParamConverterInterface
|
||||
{
|
||||
private $resolver;
|
||||
|
||||
/**
|
||||
* ProviderParameterConverter constructor.
|
||||
*
|
||||
* @param $resolver
|
||||
*/
|
||||
public function __construct(ProviderResolver $resolver)
|
||||
{
|
||||
$this->resolver = $resolver;
|
||||
}
|
||||
|
||||
public function apply(Request $request, ParamConverter $configuration)
|
||||
{
|
||||
if (!$request->attributes->has('provider')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$provider = $request->attributes->get('provider');
|
||||
|
||||
try {
|
||||
$provider = $this->resolver->resolve($provider);
|
||||
$class = $configuration->getClass();
|
||||
switch (true) {
|
||||
case is_a($class, StopRepository::class, true):
|
||||
$request->attributes->set($configuration->getName(), $provider->getStopRepository());
|
||||
break;
|
||||
|
||||
case is_a($class, LineRepository::class, true):
|
||||
$request->attributes->set($configuration->getName(), $provider->getLineRepository());
|
||||
break;
|
||||
|
||||
case is_a($class, DepartureRepository::class, true):
|
||||
$request->attributes->set($configuration->getName(), $provider->getDepartureRepository());
|
||||
break;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (NonExistentServiceException $exception) {
|
||||
throw new NotFoundHttpException("There is no such provider as '$provider'.", $exception);
|
||||
}
|
||||
}
|
||||
|
||||
public function supports(ParamConverter $configuration)
|
||||
{
|
||||
$instance = curry('is_a', 3)(_, _, true);
|
||||
|
||||
return any(
|
||||
$instance(StopRepository::class),
|
||||
$instance(LineRepository::class),
|
||||
$instance(DepartureRepository::class)
|
||||
)($configuration->getClass());
|
||||
}
|
||||
}
|
206
symfony.lock
Normal file
206
symfony.lock
Normal file
@ -0,0 +1,206 @@
|
||||
{
|
||||
"doctrine/annotations": {
|
||||
"version": "1.0",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "master",
|
||||
"version": "1.0",
|
||||
"ref": "cb4152ebcadbe620ea2261da1a1c5a9b8cea7672"
|
||||
}
|
||||
},
|
||||
"doctrine/cache": {
|
||||
"version": "v1.7.1"
|
||||
},
|
||||
"doctrine/collections": {
|
||||
"version": "v1.5.0"
|
||||
},
|
||||
"doctrine/common": {
|
||||
"version": "v2.9.0"
|
||||
},
|
||||
"doctrine/event-manager": {
|
||||
"version": "v1.0.0"
|
||||
},
|
||||
"doctrine/inflector": {
|
||||
"version": "v1.3.0"
|
||||
},
|
||||
"doctrine/lexer": {
|
||||
"version": "v1.0.1"
|
||||
},
|
||||
"doctrine/persistence": {
|
||||
"version": "v1.0.0"
|
||||
},
|
||||
"doctrine/reflection": {
|
||||
"version": "v1.0.0"
|
||||
},
|
||||
"kadet/functional": {
|
||||
"version": "dev-master"
|
||||
},
|
||||
"nesbot/carbon": {
|
||||
"version": "1.33.0"
|
||||
},
|
||||
"phpdocumentor/reflection-common": {
|
||||
"version": "1.0.1"
|
||||
},
|
||||
"phpdocumentor/reflection-docblock": {
|
||||
"version": "4.3.0"
|
||||
},
|
||||
"phpdocumentor/type-resolver": {
|
||||
"version": "0.4.0"
|
||||
},
|
||||
"psr/cache": {
|
||||
"version": "1.0.1"
|
||||
},
|
||||
"psr/container": {
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"psr/log": {
|
||||
"version": "1.0.2"
|
||||
},
|
||||
"psr/simple-cache": {
|
||||
"version": "1.0.1"
|
||||
},
|
||||
"sensio/framework-extra-bundle": {
|
||||
"version": "5.2",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "master",
|
||||
"version": "5.2",
|
||||
"ref": "fb7e19da7f013d0d422fa9bce16f5c510e27609b"
|
||||
}
|
||||
},
|
||||
"symfony/cache": {
|
||||
"version": "v4.1.3"
|
||||
},
|
||||
"symfony/config": {
|
||||
"version": "v4.1.3"
|
||||
},
|
||||
"symfony/console": {
|
||||
"version": "3.3",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "master",
|
||||
"version": "3.3",
|
||||
"ref": "e3868d2f4a5104f19f844fe551099a00c6562527"
|
||||
}
|
||||
},
|
||||
"symfony/debug": {
|
||||
"version": "v4.1.3"
|
||||
},
|
||||
"symfony/dependency-injection": {
|
||||
"version": "v4.1.3"
|
||||
},
|
||||
"symfony/dotenv": {
|
||||
"version": "v4.1.3"
|
||||
},
|
||||
"symfony/event-dispatcher": {
|
||||
"version": "v4.1.3"
|
||||
},
|
||||
"symfony/filesystem": {
|
||||
"version": "v4.1.3"
|
||||
},
|
||||
"symfony/finder": {
|
||||
"version": "v4.1.3"
|
||||
},
|
||||
"symfony/flex": {
|
||||
"version": "1.0",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "master",
|
||||
"version": "1.0",
|
||||
"ref": "e921bdbfe20cdefa3b82f379d1cd36df1bc8d115"
|
||||
}
|
||||
},
|
||||
"symfony/framework-bundle": {
|
||||
"version": "3.3",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "master",
|
||||
"version": "3.3",
|
||||
"ref": "87c585d24de9f43bca80ebcfd5cf5cb39445d95f"
|
||||
}
|
||||
},
|
||||
"symfony/http-foundation": {
|
||||
"version": "v4.1.3"
|
||||
},
|
||||
"symfony/http-kernel": {
|
||||
"version": "v4.1.3"
|
||||
},
|
||||
"symfony/inflector": {
|
||||
"version": "v4.1.3"
|
||||
},
|
||||
"symfony/polyfill-mbstring": {
|
||||
"version": "v1.9.0"
|
||||
},
|
||||
"symfony/polyfill-php72": {
|
||||
"version": "v1.9.0"
|
||||
},
|
||||
"symfony/process": {
|
||||
"version": "v4.1.3"
|
||||
},
|
||||
"symfony/property-access": {
|
||||
"version": "v4.1.3"
|
||||
},
|
||||
"symfony/property-info": {
|
||||
"version": "v4.1.3"
|
||||
},
|
||||
"symfony/routing": {
|
||||
"version": "4.0",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "master",
|
||||
"version": "4.0",
|
||||
"ref": "cda8b550123383d25827705d05a42acf6819fe4e"
|
||||
}
|
||||
},
|
||||
"symfony/serializer": {
|
||||
"version": "v4.1.3"
|
||||
},
|
||||
"symfony/serializer-pack": {
|
||||
"version": "v1.0.1"
|
||||
},
|
||||
"symfony/translation": {
|
||||
"version": "3.3",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "master",
|
||||
"version": "3.3",
|
||||
"ref": "6bcd6c570c017ea6ae5a7a6a027c929fd3542cd8"
|
||||
}
|
||||
},
|
||||
"symfony/twig-bridge": {
|
||||
"version": "v4.1.3"
|
||||
},
|
||||
"symfony/twig-bundle": {
|
||||
"version": "3.3",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "master",
|
||||
"version": "3.3",
|
||||
"ref": "f75ac166398e107796ca94cc57fa1edaa06ec47f"
|
||||
}
|
||||
},
|
||||
"symfony/var-dumper": {
|
||||
"version": "v4.1.3"
|
||||
},
|
||||
"symfony/web-server-bundle": {
|
||||
"version": "3.3",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "master",
|
||||
"version": "3.3",
|
||||
"ref": "dae9b39fd6717970be7601101ce5aa960bf53d9a"
|
||||
}
|
||||
},
|
||||
"symfony/yaml": {
|
||||
"version": "v4.1.3"
|
||||
},
|
||||
"tightenco/collect": {
|
||||
"version": "v5.6.33"
|
||||
},
|
||||
"twig/twig": {
|
||||
"version": "v2.5.0"
|
||||
},
|
||||
"webmozart/assert": {
|
||||
"version": "1.3.0"
|
||||
}
|
||||
}
|
17
templates/base.html.twig
Normal file
17
templates/base.html.twig
Normal file
@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{% block title %}Welcome!{% endblock %}</title>
|
||||
{% block stylesheets %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
{% block body %}
|
||||
<main id="app" class="container">
|
||||
<stop-picker></stop-picker>
|
||||
</main>
|
||||
{% endblock %}
|
||||
<script src="bundle.js"></script>
|
||||
{% block javascripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
0
translations/.gitignore
vendored
Normal file
0
translations/.gitignore
vendored
Normal file
12
tsconfig.json
Normal file
12
tsconfig.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["dom", "es2015", "es2016.array.include", "es2017.object"],
|
||||
"experimentalDecorators": true,
|
||||
"target": "es5",
|
||||
"sourceMap": true,
|
||||
"noImplicitThis": true,
|
||||
"moduleResolution": "node",
|
||||
"downlevelIteration": true
|
||||
},
|
||||
"files": ["resources/ts/app.ts"]
|
||||
}
|
57
webpack.config.js
Normal file
57
webpack.config.js
Normal file
@ -0,0 +1,57 @@
|
||||
const path = require('path');
|
||||
const config = {
|
||||
entry: {
|
||||
main: ['./resources/ts/app.ts'],
|
||||
},
|
||||
output: {
|
||||
path: path.resolve('./public/'),
|
||||
filename: "bundle.js",
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.tsx', '.ts', '.js'],
|
||||
alias: {
|
||||
'vue$': 'vue/dist/vue.esm.js'
|
||||
}
|
||||
},
|
||||
module: {
|
||||
rules: [{
|
||||
test: /\.svg$/,
|
||||
include: [
|
||||
path.resolve('./resources/icons')
|
||||
],
|
||||
use: ['raw-loader', {
|
||||
loader: path.resolve('./resources/svg-icon-loader.js')
|
||||
}]
|
||||
},{
|
||||
test: /\.s[ac]ss$/,
|
||||
use: ["style-loader", "css-loader?sourceMap", "sass-loader?sourceMap"]
|
||||
}, {
|
||||
test: /\.css$/,
|
||||
use: ["style-loader", "css-loader"]
|
||||
}, {
|
||||
test: /\.tsx?$/,
|
||||
use: 'ts-loader',
|
||||
exclude: /node_modules/
|
||||
}, {
|
||||
test: /\.(png|svg|jpg|gif)$/,
|
||||
use: 'file-loader',
|
||||
exclude: [
|
||||
path.resolve('./resources/icons')
|
||||
]
|
||||
}, {
|
||||
test: /\.(woff|woff2|eot|ttf|otf)$/,
|
||||
use: 'file-loader'
|
||||
}, {
|
||||
test: /\.html?$/,
|
||||
use: 'raw-loader'
|
||||
}]
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = (env, argv) => {
|
||||
if (argv.mode === 'development') {
|
||||
config.devtool = 'inline-source-map';
|
||||
}
|
||||
|
||||
return config;
|
||||
};
|
Loading…
Reference in New Issue
Block a user