Add lines to destinations

This is useful when we have few different stops, with same destinations and different lines.
This commit is contained in:
Kacper Donat 2020-02-08 18:16:04 +01:00
parent 9ecadfd4d1
commit 66e45c8112
13 changed files with 173 additions and 26 deletions

View File

@ -1,12 +1,16 @@
<span class="line__symbol flex" :class="{ [`line--${line.type}`]: true, 'line--night': line.night, 'line--fast': line.fast }">
<span class="flex align-items-stretch">
<span class="icon">
<fa :icon="['fac', line.type]" fixed-width/>
</span>
<span class="badge badge-dark flex">
<fa :icon="['fas', 'walking']" fixed-width v-if="line.fast"/>
<fa :icon="['fal', 'moon']" fixed-width v-if="line.night"/>
{{ line.symbol }}
</span>
<slot name="icon" v-if="!simple">
<span class="icon">
<fa :icon="['fac', line.type]" fixed-width/>
</span>
</slot>
<slot name="badge">
<span class="badge badge-dark flex">
<fa :icon="['fal', 'moon']" fixed-width v-if="line.night && !simple"/>
{{ line.symbol }}
<fa :icon="['fas', 'walking']" v-if="line.fast"/>
</span>
</slot>
</span>
</span>
</span>

View File

@ -2,11 +2,17 @@
<div class="d-flex">
<slot name="primary-action" />
<div class="overflow-hidden align-self-center">
<stop :stop="stop" class="my-1"/>
<stop :stop="stop" />
<div class="stop__destinations" v-if="destinations && destinations.length > 0">
<fa :icon="['far', 'chevron-right']" />
<ul class="ml-1">
<li class="stop__destination" v-for="destination in destinations" :key="destination.id">{{ destination.name }}</li>
<li class="stop__destination destination" v-for="destination in destinations" :key="destination.stop.id">
<ul class="destination__lines">
<li v-for="line in destination.lines">
<line-symbol :line="line" :key="line.symbol" simple/>
</li>
</ul>
<span class="destination__name ml-1">{{ destination.stop.name }}</span>
</li>
</ul>
</div>
</div>

View File

@ -53,6 +53,16 @@
.stop__destination {
@extend .favourite__stop;
align-items: center;
}
.destination__line {
@extend .line__symbol;
}
.destination__lines li {
display: inline-block;
@include spacing;
}
.finder__stop {

View File

@ -43,6 +43,13 @@ $grid-gutter-width: $spacer * 2;
}
}
@mixin spacing($spacing: .25em) {
margin-left: $spacing;
&:first-child {
margin-left: 0;
}
}
@mixin no-scrollbars {
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* Internet Explorer 10+ */

View File

@ -6,6 +6,9 @@ import { Line } from "../model";
export class LineComponent extends Vue {
@Prop(Object)
public line: Line;
@Prop(Boolean)
public simple: boolean;
}
Vue.component('LineSymbol', LineComponent);
Vue.component('LineSymbol', LineComponent);

View File

@ -1,8 +1,8 @@
import Component from "vue-class-component";
import Vue from "vue";
import { Stop, StopGroup, StopGroups } from "../model";
import { Destination, Line, Stop, StopGroup, StopGroups } from "../model";
import { Prop, Watch } from "vue-property-decorator";
import { FetchingState, filter, map, unique } from "../utils";
import { FetchingState, filter, map, match, unique } from "../utils";
import { debounce } from "../decorators";
import urls from '../urls';
@ -20,7 +20,29 @@ export class PickerStopComponent extends Vue {
}
get destinations() {
return unique(this.stop.destinations, stop => stop.name);
const compactLines = destination => ({
...destination,
lines: Object.entries(groupLinesByType(destination.lines || [])).map(([type, lines]) => ({
type: type,
symbol: joinedSymbol(lines),
night: lines.every(line => line.night),
fast: lines.every(line => line.fast),
})),
all: destination.lines
});
const groupLinesByType = (lines: Line[]) => lines.reduce<{ [kind: string]: Line[]}>((groups, line) => ({
...groups,
[line.type]: [ ...(groups[line.type] || []), line ]
}), {});
const joinedSymbol = match<string, [Line[]]>(
[lines => lines.length === 1, lines => lines[0].symbol],
[lines => lines.length === 2, ([first, second]) => `${first.symbol}, ${second.symbol}`],
[lines => lines.length > 2, ([first]) => `${first.symbol}`],
);
return unique(this.stop.destinations || [], destination => destination.stop.name).map(compactLines);
}
}

View File

@ -1,7 +1,6 @@
import Vue from 'vue';
import { Component, Prop, Watch } from "vue-property-decorator";
import Popper, { Placement } from "popper.js";
import { Portal } from "portal-vue";
import vueRemovedHookMixin from "vue-removed-hook-mixin";
@Component({
@ -155,3 +154,9 @@ export class LazyComponent extends Vue {
Vue.component('Popper', PopperComponent);
Vue.component('Fold', FoldComponent);
Vue.component('Lazy', LazyComponent);
// https://github.com/vuejs/vue/issues/7829
Vue.component('Empty', {
functional: true,
render: (h, { data }) => h('template', data, '')
});

View File

@ -1,3 +1,5 @@
import { Line } from "./line";
export interface Stop {
id: any;
name: string;
@ -8,7 +10,12 @@ export interface Stop {
};
onDemand?: boolean;
variant?: string;
destinations?: Stop[];
destinations?: Destination[];
}
export type Destination = {
stop: Stop;
lines: Line[]
}
export type StopGroup = Stop[];

View File

@ -95,3 +95,22 @@ export function unique<T, U>(array: T[], criterion: (item: T) => U = identity) {
return result;
}
type Pattern<TResult, TArgs extends any[]> = [
(...args: TArgs) => boolean,
((...args: TArgs) => TResult) | TResult,
]
export function match<TResult, TArgs extends any[]>(...patterns: Pattern<TResult, TArgs>[]): (...args: TArgs) => TResult {
return (...args: TArgs) => {
for (let [pattern, action] of patterns) {
if (pattern(...args)) {
return typeof action === "function" ? (action as (...args: TArgs) => TResult)(...args) : action;
}
}
throw new Error(`No pattern matches args: ${JSON.stringify(args)}`);
}
}
match.default = (...args: any[]) => true;

View File

@ -14,7 +14,7 @@ use Symfony\Component\Routing\Annotation\Route;
class TripController extends Controller
{
/**
* @Route("/{id}")
* @Route("/{id}", methods={"GET"})
*/
public function one($id, TripRepository $repository)
{

52
src/Model/Destination.php Normal file
View File

@ -0,0 +1,52 @@
<?php
namespace App\Model;
use JMS\Serializer\Annotation as Serializer;
use Nelmio\ApiDocBundle\Annotation\Model;
use Swagger\Annotations as SWG;
use Tightenco\Collect\Support\Collection;
class Destination implements Fillable
{
use FillTrait;
/**
* Stop associated with destination.
* @Serializer\Type(Stop::class)
* @var Stop
*/
private $stop;
/**
* @Serializer\Type("Collection")
* @SWG\Property(type="array", @SWG\Items(ref=@Model(type=Line::class, groups={"Default"})))
* @var Line[]|Collection<Line>
*/
private $lines;
public function __construct()
{
$this->lines = collect();
}
public function getStop(): Stop
{
return $this->stop;
}
public function setStop(Stop $stop): void
{
$this->stop = $stop;
}
public function getLines(): Collection
{
return $this->lines;
}
public function setLines(iterable $lines): void
{
$this->lines = collect($lines);
}
}

View File

@ -73,9 +73,9 @@ class Stop implements Referable, Fillable
*
* @Serializer\Groups({"WithDestinations"})
* @Serializer\Type("Collection")
* @SWG\Property(type="array", @SWG\Items(ref=@Model(type=Stop::class, groups={"Default"})))
* @SWG\Property(type="array", @SWG\Items(ref=@Model(type=Destination::class, groups={"Default"})))
*
* @var Collection<Stop>
* @var Collection<Destination>
*/
private $destinations;

View File

@ -4,11 +4,12 @@ namespace App\Provider\Database;
use App\Entity\StopEntity;
use App\Entity\TrackEntity;
use App\Model\Destination;
use App\Model\Stop;
use App\Provider\StopRepository;
use Tightenco\Collect\Support\Collection;
use Kadet\Functional as f;
use Kadet\Functional\Transforms as t;
use Tightenco\Collect\Support\Collection;
class GenericStopRepository extends DatabaseRepository implements StopRepository
{
@ -46,9 +47,10 @@ class GenericStopRepository extends DatabaseRepository implements StopRepository
$stops = collect($query->execute([':name' => "%$name%"]));
$destinations = collect($this->em->createQueryBuilder()
->select('t', 'f', 'fs', 'ts')
->select('t', 'tl', 'f', 'fs', 'ts')
->from(TrackEntity::class, 't')
->join('t.stopsInTrack', 'ts')
->join('t.line', 'tl')
->where('ts.stop IN (:stops)')
->join('t.final', 'f')
->join('f.stop', 'fs')
@ -62,9 +64,19 @@ class GenericStopRepository extends DatabaseRepository implements StopRepository
return $grouped;
}, collect())
->map(function (Collection $tracks) {
return $tracks->map(function (TrackEntity $track) {
return $this->convert($track->getFinal()->getStop());
})->unique()->values();
return $tracks
->groupBy(function (TrackEntity $track) {
return $track->getFinal()->getStop()->getId();
})->map(function (Collection $tracks, $id) {
return Destination::createFromArray([
'stop' => $this->convert($tracks->first()->getFinal()->getStop()),
'lines' => $tracks
->map(t\property('line'))
->unique(t\property('id'))
->map(f\ref([$this, 'convert']))
->values(),
]);
})->values();
});
return collect($stops)->map(f\ref([$this, 'convert']))->each(function (Stop $stop) use ($destinations) {