From 38e6ae52e1a490cb42dcaaf132f0c95f91a8ecd1 Mon Sep 17 00:00:00 2001
From: Kacper Donat <kadet1090@gmail.com>
Date: Thu, 6 Feb 2020 20:09:06 +0100
Subject: [PATCH] Add final stop info to track

---
 resources/components/finder.html              | 13 +++--
 resources/components/picker/stop.html         | 34 ++++++-----
 resources/styles/_favourites.scss             |  1 +
 resources/styles/_stop.scss                   | 14 ++++-
 resources/ts/model/stop.ts                    |  1 +
 src/Entity/StopInTrack.php                    | 22 +++++--
 src/Entity/TrackEntity.php                    | 17 ++++--
 src/Migrations/Version20200206183956.php      | 57 +++++++++++++++++++
 src/Model/ReferableTrait.php                  |  3 +-
 .../Database/GenericStopRepository.php        | 15 ++---
 .../ZtmGdanskDataUpdateSubscriber.php         | 49 +++++++++-------
 src/Service/IterableUtils.php                 | 28 +++++++++
 templates/app.html.twig                       | 15 +++--
 13 files changed, 203 insertions(+), 66 deletions(-)
 create mode 100644 src/Migrations/Version20200206183956.php
 create mode 100644 src/Service/IterableUtils.php

diff --git a/resources/components/finder.html b/resources/components/finder.html
index 0674533..9f26c5a 100644
--- a/resources/components/finder.html
+++ b/resources/components/finder.html
@@ -22,11 +22,14 @@
             </div>
             <ul class="stop-group__stops list-underlined">
                 <li v-for="stop in group" :key="stop.id" class="d-flex">
-                    <button @click="select(stop, $event)" class="btn btn-action align-self-start">
-                        <tooltip>dodaj przystanek</tooltip>
-                        <fa :icon="['fal', 'check']" />
-                    </button>
-                    <picker-stop :stop="stop" class="flex-grow-1"></picker-stop>
+                    <picker-stop :stop="stop" class="flex-grow-1">
+                        <template v-slot:primary-action>
+                            <button @click="select(stop, $event)" class="btn btn-action align-self-start">
+                                <tooltip>dodaj przystanek</tooltip>
+                                <fa :icon="['fal', 'check']" />
+                            </button>
+                        </template>
+                    </picker-stop>
                 </li>
             </ul>
         </div>
diff --git a/resources/components/picker/stop.html b/resources/components/picker/stop.html
index 787a63a..1e9de69 100644
--- a/resources/components/picker/stop.html
+++ b/resources/components/picker/stop.html
@@ -1,19 +1,27 @@
-<div class="d-flex flex-wrap">
-    <stop :stop="stop" />
+<div class="finder__stop">
+    <div class="d-flex">
+        <slot name="primary-action" />
+        <div style="overflow: hidden">
+            <stop :stop="stop" />
+<!--            <div style="white-space: nowrap">Lorem ipsum dolor sit amet, consectetur adipisicing elit. Earum, ut!</div>-->
+            <ul class="stop__destinations">
+                <li class="stop__destination" v-for="destination in stop.destinations" :key="destination.id"><stop :stop="destination"/></li>
+            </ul>
+        </div>
 
-    <div class="stop__actions flex-space-left">
-        <slot name="actions">
-            <button class="btn btn-action" ref="action-info" @click="details = !details">
-                <tooltip>dodatkowe informacje</tooltip>
-                <fa :icon="['fal', details ? 'chevron-circle-up' : 'info-circle']"/>
-            </button>
+        <div class="stop__actions flex-space-left">
+            <slot name="actions">
+                <button class="btn btn-action" ref="action-info" @click="details = !details">
+                    <tooltip>dodatkowe informacje</tooltip>
+                    <fa :icon="['fal', details ? 'chevron-circle-up' : 'info-circle']"/>
+                </button>
 
-            <button class="btn btn-action" ref="action-map" v-hover:map>
-                <fa :icon="['fal', 'map-marker-alt']"/>
-            </button>
-        </slot>
+                <button class="btn btn-action" ref="action-map" v-hover:map>
+                    <fa :icon="['fal', 'map-marker-alt']"/>
+                </button>
+            </slot>
+        </div>
     </div>
-
     <fold :visible="details" class="stop__details-fold" lazy>
         <stop-details :stop="stop"/>
     </fold>
diff --git a/resources/styles/_favourites.scss b/resources/styles/_favourites.scss
index bbb50c9..27f3d79 100644
--- a/resources/styles/_favourites.scss
+++ b/resources/styles/_favourites.scss
@@ -11,6 +11,7 @@
   font-size: $small-font-size;
   overflow-x: hidden;
   text-overflow: ellipsis;
+  max-width: 100%;
 
   &:last-child {
     margin-bottom: 0;
diff --git a/resources/styles/_stop.scss b/resources/styles/_stop.scss
index f8191cc..e0c48fe 100644
--- a/resources/styles/_stop.scss
+++ b/resources/styles/_stop.scss
@@ -5,7 +5,7 @@
 }
 
 .stop__name {
-  flex: 1 0;
+  //flex: 1 0;
   line-height: 1.1;
   margin-right: .5em;
 }
@@ -46,3 +46,15 @@
     }
   }
 }
+
+.stop__destinations {
+  @extend .favourite__stops;
+}
+
+.stop__destination {
+  @extend .favourite__stop;
+}
+
+.finder__stop {
+  max-width: 100%;
+}
diff --git a/resources/ts/model/stop.ts b/resources/ts/model/stop.ts
index ebb3bfb..fa6bc33 100644
--- a/resources/ts/model/stop.ts
+++ b/resources/ts/model/stop.ts
@@ -8,6 +8,7 @@ export interface Stop {
     };
     onDemand?: boolean;
     variant?: string;
+    destinations?: Stop[];
 }
 
 export type StopGroup = Stop[];
diff --git a/src/Entity/StopInTrack.php b/src/Entity/StopInTrack.php
index 608327b..26cbd55 100644
--- a/src/Entity/StopInTrack.php
+++ b/src/Entity/StopInTrack.php
@@ -4,32 +4,42 @@ namespace App\Entity;
 
 use App\Model\Fillable;
 use App\Model\FillTrait;
+use App\Model\Referable;
+use App\Model\ReferableTrait;
 use Doctrine\ORM\Mapping as ORM;
 
 /**
  * @ORM\Entity
- * @ORM\Table("track_stop")
+ * @ORM\Table("track_stop", uniqueConstraints={
+ *     @ORM\UniqueConstraint(name="stop_in_track_idx", columns={"stop_id", "track_id", "sequence"})
+ * })
  */
-class StopInTrack implements Fillable
+class StopInTrack implements Fillable, Referable
 {
-    use FillTrait;
+    use FillTrait, ReferableEntityTrait;
+
+    /**
+     * Identifier for stop coming from provider
+     *
+     * @ORM\Column(type="integer")
+     * @ORM\Id
+     * @ORM\GeneratedValue
+     */
+    private $id;
 
     /**
      * @ORM\ManyToOne(targetEntity=StopEntity::class, fetch="EAGER")
-     * @ORM\Id
      */
     private $stop;
 
     /**
      * @ORM\ManyToOne(targetEntity=TrackEntity::class, fetch="EAGER", inversedBy="stopsInTrack")
-     * @ORM\Id
      */
     private $track;
 
     /**
      * Order in track
      * @var int
-     * @ORM\Id
      * @ORM\Column(name="sequence", type="integer")
      */
     private $order;
diff --git a/src/Entity/TrackEntity.php b/src/Entity/TrackEntity.php
index a56fd2e..a0eaa1d 100644
--- a/src/Entity/TrackEntity.php
+++ b/src/Entity/TrackEntity.php
@@ -4,6 +4,7 @@ namespace App\Entity;
 
 use App\Model\Fillable;
 use App\Model\FillTrait;
+use App\Service\IterableUtils;
 use Doctrine\Common\Collections\ArrayCollection;
 use Doctrine\Common\Collections\Collection;
 use Doctrine\ORM\Mapping as ORM;
@@ -50,6 +51,12 @@ class TrackEntity implements Entity, Fillable
      */
     private $stopsInTrack;
 
+    /**
+     * Final stop in this track.
+     * @var StopInTrack
+     * @ORM\OneToOne(targetEntity=StopInTrack::class, fetch="LAZY")
+     */
+    private $final;
 
     /**
      * Track constructor.
@@ -98,15 +105,17 @@ class TrackEntity implements Entity, Fillable
     }
 
     /**
-     * @param Collection $stopsInTrack
+     * @param iterable $stopsInTrack
      */
-    public function setStopsInTrack(array $stopsInTrack): void
+    public function setStopsInTrack(iterable $stopsInTrack): void
     {
-        $this->stopsInTrack = new ArrayCollection($stopsInTrack);
+        $this->stopsInTrack = IterableUtils::toArrayCollection($stopsInTrack);
+
+        $this->final = $this->stopsInTrack->last();
     }
 
     public function getFinal(): StopInTrack
     {
-        return $this->getStopsInTrack()->last();
+        return $this->final;
     }
 }
diff --git a/src/Migrations/Version20200206183956.php b/src/Migrations/Version20200206183956.php
new file mode 100644
index 0000000..d7485ab
--- /dev/null
+++ b/src/Migrations/Version20200206183956.php
@@ -0,0 +1,57 @@
+<?php
+
+declare(strict_types=1);
+
+namespace DoctrineMigrations;
+
+use Doctrine\DBAL\Schema\Schema;
+use Doctrine\Migrations\AbstractMigration;
+
+/**
+ * Auto-generated Migration: Please modify to your needs!
+ */
+final class Version20200206183956 extends AbstractMigration
+{
+    public function getDescription() : string
+    {
+        return '';
+    }
+
+    public function up(Schema $schema) : void
+    {
+        // this up() migration is auto-generated, please modify it to your needs
+        $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'sqlite', 'Migration can only be executed safely on \'sqlite\'.');
+
+        $this->addSql('CREATE TEMPORARY TABLE __temp__track AS SELECT id, line_id, provider_id, variant, description FROM track');
+        $this->addSql('DROP TABLE track');
+        $this->addSql('CREATE TABLE track (id VARCHAR(255) NOT NULL COLLATE BINARY, line_id VARCHAR(255) DEFAULT NULL COLLATE BINARY, provider_id VARCHAR(255) DEFAULT NULL COLLATE BINARY, variant VARCHAR(16) DEFAULT NULL COLLATE BINARY, description VARCHAR(256) DEFAULT NULL COLLATE BINARY, final_id INTEGER DEFAULT NULL, PRIMARY KEY(id))');
+        $this->addSql('INSERT INTO track (id, line_id, provider_id, variant, description) SELECT id, line_id, provider_id, variant, description FROM __temp__track');
+        $this->addSql('DROP TABLE __temp__track');
+        $this->addSql('CREATE UNIQUE INDEX UNIQ_D6E3F8A613D41B2D ON track (final_id)');
+        $this->addSql('CREATE TEMPORARY TABLE __temp__track_stop AS SELECT stop_id, track_id, sequence FROM track_stop');
+        $this->addSql('DROP TABLE track_stop');
+        $this->addSql('CREATE TABLE track_stop (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, sequence INTEGER NOT NULL, stop_id VARCHAR(255) DEFAULT NULL, track_id VARCHAR(255) DEFAULT NULL)');
+        $this->addSql('INSERT INTO track_stop (stop_id, track_id, sequence) SELECT stop_id, track_id, sequence FROM __temp__track_stop');
+        $this->addSql('DROP TABLE __temp__track_stop');
+        $this->addSql('CREATE UNIQUE INDEX stop_in_track_idx ON track_stop (stop_id, track_id, sequence)');
+    }
+
+    public function down(Schema $schema) : void
+    {
+        // this down() migration is auto-generated, please modify it to your needs
+        $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'sqlite', 'Migration can only be executed safely on \'sqlite\'.');
+
+        $this->addSql('DROP INDEX UNIQ_D6E3F8A613D41B2D');
+        $this->addSql('CREATE TEMPORARY TABLE __temp__track AS SELECT id, variant, description, line_id, provider_id FROM track');
+        $this->addSql('DROP TABLE track');
+        $this->addSql('CREATE TABLE track (id VARCHAR(255) NOT NULL, variant VARCHAR(16) DEFAULT NULL, description VARCHAR(256) DEFAULT NULL, line_id VARCHAR(255) DEFAULT NULL, provider_id VARCHAR(255) DEFAULT NULL, PRIMARY KEY(id))');
+        $this->addSql('INSERT INTO track (id, variant, description, line_id, provider_id) SELECT id, variant, description, line_id, provider_id FROM __temp__track');
+        $this->addSql('DROP TABLE __temp__track');
+        $this->addSql('DROP INDEX stop_in_track_idx');
+        $this->addSql('CREATE TEMPORARY TABLE __temp__track_stop AS SELECT sequence, stop_id, track_id FROM track_stop');
+        $this->addSql('DROP TABLE track_stop');
+        $this->addSql('CREATE TABLE track_stop (sequence INTEGER NOT NULL, stop_id VARCHAR(255) NOT NULL COLLATE BINARY, track_id VARCHAR(255) NOT NULL COLLATE BINARY, PRIMARY KEY(stop_id, track_id, sequence))');
+        $this->addSql('INSERT INTO track_stop (sequence, stop_id, track_id) SELECT sequence, stop_id, track_id FROM __temp__track_stop');
+        $this->addSql('DROP TABLE __temp__track_stop');
+    }
+}
diff --git a/src/Model/ReferableTrait.php b/src/Model/ReferableTrait.php
index 70280dc..bb4397f 100644
--- a/src/Model/ReferableTrait.php
+++ b/src/Model/ReferableTrait.php
@@ -2,6 +2,7 @@
 
 namespace App\Model;
 
+use Doctrine\ORM\Mapping as ORM;
 use JMS\Serializer\Annotation as Serializer;
 
 trait ReferableTrait
@@ -38,4 +39,4 @@ trait ReferableTrait
 
         return $result;
     }
-}
\ No newline at end of file
+}
diff --git a/src/Provider/Database/GenericStopRepository.php b/src/Provider/Database/GenericStopRepository.php
index 2d3aa9a..737b11f 100644
--- a/src/Provider/Database/GenericStopRepository.php
+++ b/src/Provider/Database/GenericStopRepository.php
@@ -3,10 +3,8 @@
 namespace App\Provider\Database;
 
 use App\Entity\StopEntity;
-use App\Entity\StopInTrack;
 use App\Entity\TrackEntity;
 use App\Model\Stop;
-use App\Model\Track;
 use App\Provider\StopRepository;
 use Tightenco\Collect\Support\Collection;
 use Kadet\Functional as f;
@@ -23,7 +21,7 @@ class GenericStopRepository extends DatabaseRepository implements StopRepository
 
     public function getById($id): ?Stop
     {
-        $id = $this->id->generate($this->provider, $id);
+        $id   = $this->id->generate($this->provider, $id);
         $stop = $this->em->getRepository(StopEntity::class)->find($id);
 
         return $this->convert($stop);
@@ -31,7 +29,7 @@ class GenericStopRepository extends DatabaseRepository implements StopRepository
 
     public function getManyById($ids): Collection
     {
-        $ids = collect($ids)->map(f\apply(f\ref([$this->id, 'generate']), $this->provider));
+        $ids   = collect($ids)->map(f\apply(f\ref([$this->id, 'generate']), $this->provider));
         $stops = $this->em->getRepository(StopEntity::class)->findBy(['id' => $ids->all()]);
 
         return collect($stops)->map(f\ref([$this, 'convert']));
@@ -48,12 +46,12 @@ class GenericStopRepository extends DatabaseRepository implements StopRepository
         $stops = collect($query->execute([':name' => "%$name%"]));
 
         $destinations = collect($this->em->createQueryBuilder()
-            ->select('t', 'ts', 'ts2', 's')
+            ->select('t', 'f', 'fs', 'ts')
             ->from(TrackEntity::class, 't')
             ->join('t.stopsInTrack', 'ts')
-            ->join('t.stopsInTrack', 'ts2')
-            ->join('ts2.stop', 's')
             ->where('ts.stop IN (:stops)')
+            ->join('t.final', 'f')
+            ->join('f.stop', 'fs')
             ->getQuery()
             ->execute(['stops' => $stops->map(t\property('id'))->all()]))
             ->reduce(function ($grouped, TrackEntity $track) {
@@ -67,8 +65,7 @@ class GenericStopRepository extends DatabaseRepository implements StopRepository
                 return $tracks->map(function (TrackEntity $track) {
                     return $this->convert($track->getFinal()->getStop());
                 })->unique()->values();
-            })
-            ;
+            });
 
         return collect($stops)->map(f\ref([$this, 'convert']))->each(function (Stop $stop) use ($destinations) {
             $stop->setDestinations($destinations[$this->id->generate($this->provider, $stop->getId())]);
diff --git a/src/Provider/ZtmGdansk/ZtmGdanskDataUpdateSubscriber.php b/src/Provider/ZtmGdansk/ZtmGdanskDataUpdateSubscriber.php
index abf6553..146ef55 100644
--- a/src/Provider/ZtmGdansk/ZtmGdanskDataUpdateSubscriber.php
+++ b/src/Provider/ZtmGdansk/ZtmGdanskDataUpdateSubscriber.php
@@ -12,7 +12,6 @@ use App\Entity\TripEntity;
 use App\Entity\TripStopEntity;
 use App\Event\DataUpdateEvent;
 use App\Model\Line as LineModel;
-use App\Model\Location;
 use App\Service\DataUpdater;
 use App\Service\IdUtils;
 use Carbon\Carbon;
@@ -20,13 +19,10 @@ use Cerbero\JsonObjects\JsonObjects;
 use Doctrine\ORM\EntityManagerInterface;
 use Symfony\Component\Console\Helper\ProgressBar;
 use Psr\Log\LoggerInterface;
-use Symfony\Component\Console\Output\ConsoleOutputInterface;
-use Symfony\Component\Console\Output\ConsoleSectionOutput;
-use Symfony\Component\Console\Output\NullOutput;
 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
 use Tightenco\Collect\Support\Collection;
-use function Cerbero\JsonObjects\JsonObjects;
 use function Kadet\Functional\ref;
+use function Kadet\Functional\Transforms\property;
 
 class ZtmGdanskDataUpdateSubscriber implements EventSubscriberInterface
 {
@@ -45,6 +41,8 @@ class ZtmGdanskDataUpdateSubscriber implements EventSubscriberInterface
     private $logger;
     private $provider;
 
+    private $stopBlacklist = [];
+
     /**
      * ZtmGdanskDataUpdateSubscriber constructor.
      *
@@ -147,6 +145,7 @@ class ZtmGdanskDataUpdateSubscriber implements EventSubscriberInterface
 
     private function getStops(ProviderEntity $provider, DataUpdateEvent $event)
     {
+        $this->stopBlacklist = [];
         $output = $event->getOutput();
 
         $output->write('Obtaining stops from ZTM Gdańsk... ');
@@ -157,9 +156,12 @@ class ZtmGdanskDataUpdateSubscriber implements EventSubscriberInterface
         $this->logger->debug(sprintf("Saving %d stops tracks from ZTM Gdańsk.", count($stops)));
         return collect($stops)
             ->filter(function ($stop) {
-                return $stop['nonpassenger'] !== 1
-                    && $stop['virtual'] !== 1
-                    && $stop['depot'] !== 1;
+                if ($stop['nonpassenger'] === 1 || $stop['virtual'] === 1 || $stop['depot'] === 1) {
+                    $this->stopBlacklist[] = $stop['stopId'];
+                    return false;
+                }
+
+                return true;
             })
             ->map(function ($stop) use ($provider) {
                 $name = trim($stop['stopName'] ?? $stop['stopDesc']);
@@ -178,7 +180,7 @@ class ZtmGdanskDataUpdateSubscriber implements EventSubscriberInterface
         ;
     }
 
-    public function getTracks(ProviderEntity $provider, DataUpdateEvent $event, $stops = [])
+    public function getTracks(ProviderEntity $provider, DataUpdateEvent $event, Collection $stops = null)
     {
         $output = $event->getOutput();
 
@@ -209,19 +211,24 @@ class ZtmGdanskDataUpdateSubscriber implements EventSubscriberInterface
                 'provider'    => $provider,
             ]);
 
-            $stops = $stops->get($track['id'])->map(function ($stop) use ($entity, $provider) {
-                return StopInTrack::createFromArray([
-                    'stop'  => $this->em->getReference(
-                        StopEntity::class,
-                        $this->ids->generate($provider, $stop['stopId'])
-                    ),
-                    'track' => $entity,
-                    // HACK! Gdynia has 0 based sequence
-                    'order' => $stop['stopSequence'] + (int)($stop['stopId'] > 30000),
-                ]);
-            });
+            $stops = $stops->get($track['id'])
+                ->filter(function ($stop) {
+                    return !in_array($stop['stopId'], $this->stopBlacklist);
+                })
+                ->map(function ($stop) use ($entity, $provider) {
+                    return StopInTrack::createFromArray([
+                        'stop'  => $this->em->getReference(
+                            StopEntity::class,
+                            $this->ids->generate($provider, $stop['stopId'])
+                        ),
+                        'track' => $entity,
+                        // HACK! Gdynia has 0 based sequence
+                        'order' => $stop['stopSequence'] + (int)($stop['stopId'] > 30000),
+                    ]);
+                })
+                ->sortBy(property("order"));
 
-            $entity->setStopsInTrack($stops->all());
+            $entity->setStopsInTrack($stops);
 
             return $entity;
         });
diff --git a/src/Service/IterableUtils.php b/src/Service/IterableUtils.php
new file mode 100644
index 0000000..5018060
--- /dev/null
+++ b/src/Service/IterableUtils.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace App\Service;
+
+use Doctrine\Common\Collections\ArrayCollection;
+use Tightenco\Collect\Support\Collection;
+
+final class IterableUtils
+{
+    public static function toArray(iterable $iterable): array
+    {
+        if (is_array($iterable)) {
+            return $iterable;
+        }
+
+        return iterator_to_array($iterable);
+    }
+
+    public static function toArrayCollection(iterable $iterable): ArrayCollection
+    {
+        return new ArrayCollection(static::toArray($iterable));
+    }
+
+    public static function toCollection(iterable $iterable): Collection
+    {
+        return collect($iterable);
+    }
+}
diff --git a/templates/app.html.twig b/templates/app.html.twig
index d2f9802..f364419 100644
--- a/templates/app.html.twig
+++ b/templates/app.html.twig
@@ -101,11 +101,14 @@
 
                 <ul class="picker__stops list-underlined">
                     <li v-for="stop in stops" :key="stop.id" class="d-flex align-items-center">
-                        <button @click="remove(stop)" class="btn btn-action align-self-start">
-                            <tooltip>usuń przystanek</tooltip>
-                            <fa :icon="['fal', 'times']"></fa>
-                        </button>
-                        <picker-stop :stop="stop" class="flex-grow-1"></picker-stop>
+                        <picker-stop :stop="stop" class="flex-grow-1">
+                            <template v-slot:primary-action>
+                                <button @click="remove(stop)" class="btn btn-action align-self-start">
+                                    <tooltip>usuń przystanek</tooltip>
+                                    <fa :icon="['fal', 'times']"></fa>
+                                </button>
+                            </template>
+                        </picker-stop>
                     </li>
                 </ul>
 
@@ -143,7 +146,7 @@
                         </button>
                     </template>
                 </header>
-                <div class="transition-box" style="overflow: hidden;">
+                <div class="transition-box">
                     <transition name="fade">
                         <stop-finder @select="add" :blacklist="stops" v-if="visibility.picker === 'search'"></stop-finder>
                         <favourites v-else-if="visibility.picker === 'favourites'"></favourites>