#38 - Add lines to destinations
This is useful when we have few different stops, with same destinations and different lines.
This commit is contained in:
parent
0895b412d2
commit
f2f7b19380
@ -1,12 +1,16 @@
|
|||||||
<span class="line__symbol flex" :class="{ [`line--${line.type}`]: true, 'line--night': line.night, 'line--fast': line.fast }">
|
<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="flex align-items-stretch">
|
||||||
<span class="icon">
|
<slot name="icon" v-if="!simple">
|
||||||
<fa :icon="['fac', line.type]" fixed-width/>
|
<span class="icon">
|
||||||
</span>
|
<fa :icon="['fac', line.type]" fixed-width/>
|
||||||
<span class="badge badge-dark flex">
|
</span>
|
||||||
<fa :icon="['fas', 'walking']" fixed-width v-if="line.fast"/>
|
</slot>
|
||||||
<fa :icon="['fal', 'moon']" fixed-width v-if="line.night"/>
|
<slot name="badge">
|
||||||
{{ line.symbol }}
|
<span class="badge badge-dark flex">
|
||||||
</span>
|
<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>
|
</span>
|
||||||
|
@ -2,11 +2,17 @@
|
|||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
<slot name="primary-action" />
|
<slot name="primary-action" />
|
||||||
<div class="overflow-hidden align-self-center">
|
<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">
|
<div class="stop__destinations" v-if="destinations && destinations.length > 0">
|
||||||
<fa :icon="['far', 'chevron-right']" />
|
|
||||||
<ul class="ml-1">
|
<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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -53,6 +53,16 @@
|
|||||||
|
|
||||||
.stop__destination {
|
.stop__destination {
|
||||||
@extend .favourite__stop;
|
@extend .favourite__stop;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.destination__line {
|
||||||
|
@extend .line__symbol;
|
||||||
|
}
|
||||||
|
|
||||||
|
.destination__lines li {
|
||||||
|
display: inline-block;
|
||||||
|
@include spacing;
|
||||||
}
|
}
|
||||||
|
|
||||||
.finder__stop {
|
.finder__stop {
|
||||||
|
@ -43,6 +43,13 @@ $grid-gutter-width: $spacer * 2;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mixin spacing($spacing: .25em) {
|
||||||
|
margin-left: $spacing;
|
||||||
|
&:first-child {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@mixin no-scrollbars {
|
@mixin no-scrollbars {
|
||||||
scrollbar-width: none; /* Firefox */
|
scrollbar-width: none; /* Firefox */
|
||||||
-ms-overflow-style: none; /* Internet Explorer 10+ */
|
-ms-overflow-style: none; /* Internet Explorer 10+ */
|
||||||
|
@ -6,6 +6,9 @@ import { Line } from "../model";
|
|||||||
export class LineComponent extends Vue {
|
export class LineComponent extends Vue {
|
||||||
@Prop(Object)
|
@Prop(Object)
|
||||||
public line: Line;
|
public line: Line;
|
||||||
|
|
||||||
|
@Prop(Boolean)
|
||||||
|
public simple: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
Vue.component('LineSymbol', LineComponent);
|
Vue.component('LineSymbol', LineComponent);
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import Component from "vue-class-component";
|
import Component from "vue-class-component";
|
||||||
import Vue from "vue";
|
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 { 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 { debounce } from "../decorators";
|
||||||
import urls from '../urls';
|
import urls from '../urls';
|
||||||
|
|
||||||
@ -20,7 +20,29 @@ export class PickerStopComponent extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get destinations() {
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import { Component, Prop, Watch } from "vue-property-decorator";
|
import { Component, Prop, Watch } from "vue-property-decorator";
|
||||||
import Popper, { Placement } from "popper.js";
|
import Popper, { Placement } from "popper.js";
|
||||||
import { Portal } from "portal-vue";
|
|
||||||
import vueRemovedHookMixin from "vue-removed-hook-mixin";
|
import vueRemovedHookMixin from "vue-removed-hook-mixin";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -155,3 +154,9 @@ export class LazyComponent extends Vue {
|
|||||||
Vue.component('Popper', PopperComponent);
|
Vue.component('Popper', PopperComponent);
|
||||||
Vue.component('Fold', FoldComponent);
|
Vue.component('Fold', FoldComponent);
|
||||||
Vue.component('Lazy', LazyComponent);
|
Vue.component('Lazy', LazyComponent);
|
||||||
|
|
||||||
|
// https://github.com/vuejs/vue/issues/7829
|
||||||
|
Vue.component('Empty', {
|
||||||
|
functional: true,
|
||||||
|
render: (h, { data }) => h('template', data, '')
|
||||||
|
});
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { Line } from "./line";
|
||||||
|
|
||||||
export interface Stop {
|
export interface Stop {
|
||||||
id: any;
|
id: any;
|
||||||
name: string;
|
name: string;
|
||||||
@ -8,7 +10,12 @@ export interface Stop {
|
|||||||
};
|
};
|
||||||
onDemand?: boolean;
|
onDemand?: boolean;
|
||||||
variant?: string;
|
variant?: string;
|
||||||
destinations?: Stop[];
|
destinations?: Destination[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Destination = {
|
||||||
|
stop: Stop;
|
||||||
|
lines: Line[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type StopGroup = Stop[];
|
export type StopGroup = Stop[];
|
||||||
|
@ -95,3 +95,22 @@ export function unique<T, U>(array: T[], criterion: (item: T) => U = identity) {
|
|||||||
|
|
||||||
return result;
|
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;
|
||||||
|
@ -14,7 +14,7 @@ use Symfony\Component\Routing\Annotation\Route;
|
|||||||
class TripController extends Controller
|
class TripController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @Route("/{id}")
|
* @Route("/{id}", methods={"GET"})
|
||||||
*/
|
*/
|
||||||
public function one($id, TripRepository $repository)
|
public function one($id, TripRepository $repository)
|
||||||
{
|
{
|
||||||
|
52
src/Model/Destination.php
Normal file
52
src/Model/Destination.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@ -73,9 +73,9 @@ class Stop implements Referable, Fillable
|
|||||||
*
|
*
|
||||||
* @Serializer\Groups({"WithDestinations"})
|
* @Serializer\Groups({"WithDestinations"})
|
||||||
* @Serializer\Type("Collection")
|
* @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;
|
private $destinations;
|
||||||
|
|
||||||
|
@ -4,11 +4,12 @@ namespace App\Provider\Database;
|
|||||||
|
|
||||||
use App\Entity\StopEntity;
|
use App\Entity\StopEntity;
|
||||||
use App\Entity\TrackEntity;
|
use App\Entity\TrackEntity;
|
||||||
|
use App\Model\Destination;
|
||||||
use App\Model\Stop;
|
use App\Model\Stop;
|
||||||
use App\Provider\StopRepository;
|
use App\Provider\StopRepository;
|
||||||
use Tightenco\Collect\Support\Collection;
|
|
||||||
use Kadet\Functional as f;
|
use Kadet\Functional as f;
|
||||||
use Kadet\Functional\Transforms as t;
|
use Kadet\Functional\Transforms as t;
|
||||||
|
use Tightenco\Collect\Support\Collection;
|
||||||
|
|
||||||
class GenericStopRepository extends DatabaseRepository implements StopRepository
|
class GenericStopRepository extends DatabaseRepository implements StopRepository
|
||||||
{
|
{
|
||||||
@ -46,9 +47,10 @@ class GenericStopRepository extends DatabaseRepository implements StopRepository
|
|||||||
$stops = collect($query->execute([':name' => "%$name%"]));
|
$stops = collect($query->execute([':name' => "%$name%"]));
|
||||||
|
|
||||||
$destinations = collect($this->em->createQueryBuilder()
|
$destinations = collect($this->em->createQueryBuilder()
|
||||||
->select('t', 'f', 'fs', 'ts')
|
->select('t', 'tl', 'f', 'fs', 'ts')
|
||||||
->from(TrackEntity::class, 't')
|
->from(TrackEntity::class, 't')
|
||||||
->join('t.stopsInTrack', 'ts')
|
->join('t.stopsInTrack', 'ts')
|
||||||
|
->join('t.line', 'tl')
|
||||||
->where('ts.stop IN (:stops)')
|
->where('ts.stop IN (:stops)')
|
||||||
->join('t.final', 'f')
|
->join('t.final', 'f')
|
||||||
->join('f.stop', 'fs')
|
->join('f.stop', 'fs')
|
||||||
@ -62,9 +64,19 @@ class GenericStopRepository extends DatabaseRepository implements StopRepository
|
|||||||
return $grouped;
|
return $grouped;
|
||||||
}, collect())
|
}, collect())
|
||||||
->map(function (Collection $tracks) {
|
->map(function (Collection $tracks) {
|
||||||
return $tracks->map(function (TrackEntity $track) {
|
return $tracks
|
||||||
return $this->convert($track->getFinal()->getStop());
|
->groupBy(function (TrackEntity $track) {
|
||||||
})->unique()->values();
|
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) {
|
return collect($stops)->map(f\ref([$this, 'convert']))->each(function (Stop $stop) use ($destinations) {
|
||||||
|
Loading…
Reference in New Issue
Block a user