diff --git a/lib/dotcom/trip_plan/itinerary_group.ex b/lib/dotcom/trip_plan/itinerary_group.ex
new file mode 100644
index 0000000000..a357005d66
--- /dev/null
+++ b/lib/dotcom/trip_plan/itinerary_group.ex
@@ -0,0 +1,74 @@
+defmodule Dotcom.TripPlan.ItineraryGroup do
+ @moduledoc """
+ A single group of related itineraries
+ """
+
+ alias Dotcom.TripPlan.Itinerary
+
+ defstruct [:itineraries, :representative_index, :representative_time, :summary]
+
+ @type summarized_leg :: %{
+ routes: [Routes.Route.t()],
+ walk_minutes: non_neg_integer()
+ }
+
+ @type summary :: %{
+ accessible?: boolean() | nil,
+ duration: non_neg_integer(),
+ start: DateTime.t(),
+ stop: DateTime.t(),
+ summarized_legs: [summarized_leg()],
+ tag: String.t(),
+ total_cost: non_neg_integer(),
+ walk_distance: float()
+ }
+
+ @type t :: %__MODULE__{
+ itineraries: [Itinerary.t()],
+ representative_index: non_neg_integer(),
+ representative_time: :start | :stop,
+ summary: summary()
+ }
+
+ @doc """
+ List of either start times or stop times for this group
+ """
+ @spec options_text(t()) :: String.t() | nil
+ def options_text(%__MODULE__{itineraries: []}), do: nil
+ def options_text(%__MODULE__{itineraries: [_single]}), do: nil
+
+ def options_text(
+ %__MODULE__{
+ representative_index: representative_index,
+ representative_time: start_or_stop
+ } =
+ group
+ ) do
+ {_, other_times} =
+ group
+ |> all_times()
+ |> List.pop_at(representative_index)
+
+ phrase = options_phrase(start_or_stop == :stop, Enum.count(other_times))
+
+ formatted_times =
+ other_times
+ |> Enum.map(&Timex.format!(&1, "%-I:%M", :strftime))
+ |> Enum.join(", ")
+
+ "Similar #{phrase} #{formatted_times}"
+ end
+
+ defp options_phrase(true, 1), do: "trip arrives by"
+ defp options_phrase(true, _), do: "trips arrive by"
+ defp options_phrase(false, 1), do: "trip departs at"
+ defp options_phrase(_, _), do: "trips depart at"
+
+ @doc """
+ List of either start times or stop times for this group
+ """
+ @spec all_times(t()) :: [DateTime.t()]
+ def all_times(%__MODULE__{itineraries: itineraries, representative_time: start_or_stop}) do
+ Enum.map(itineraries, &Map.get(&1, start_or_stop))
+ end
+end
diff --git a/lib/dotcom/trip_plan/itinerary_groups.ex b/lib/dotcom/trip_plan/itinerary_groups.ex
index b1fb9d3f32..927f0876af 100644
--- a/lib/dotcom/trip_plan/itinerary_groups.ex
+++ b/lib/dotcom/trip_plan/itinerary_groups.ex
@@ -8,48 +8,34 @@ defmodule Dotcom.TripPlan.ItineraryGroups do
import DotcomWeb.TripPlanView, only: [get_one_way_total_by_type: 2]
- alias Dotcom.TripPlan.{Itinerary, Leg, PersonalDetail, TransitDetail}
+ alias Dotcom.TripPlan.{Itinerary, ItineraryGroup, Leg, PersonalDetail, TransitDetail}
alias OpenTripPlannerClient.ItineraryTag
@short_walk_threshold_minutes 5
- @max_per_group 5
-
- @type summarized_leg :: %{
- routes: [Routes.Route.t()],
- walk_minutes: non_neg_integer()
- }
- @type summary :: %{
- accessible?: boolean() | nil,
- duration: non_neg_integer(),
- first_start: DateTime.t(),
- first_stop: DateTime.t(),
- next_starts: [DateTime.t()],
- summarized_legs: [summarized_leg()],
- tag: String.t(),
- total_cost: non_neg_integer(),
- walk_distance: float()
- }
-
- @spec from_itineraries([Itinerary.t()]) :: [
- %{itineraries: [Itinerary.t()], summary: summary()}
- ]
- def from_itineraries(itineraries) do
+ @max_per_group 4
+
+ @doc """
+ From a large list of itineraries, collect them into 5 groups of at most
+ #{@max_per_group} itineraries each, sorting the groups in favor of tagged
+ groups first
+ """
+ @spec from_itineraries([Itinerary.t()], boolean()) :: [ItineraryGroup.t()]
+ def from_itineraries(itineraries, take_from_end? \\ false) do
itineraries
|> Enum.group_by(&unique_legs_to_hash/1)
|> Enum.map(&drop_hash/1)
|> Enum.reject(&Enum.empty?/1)
- |> Enum.take(5)
- |> Enum.map(&limit_itinerary_count/1)
- |> Enum.map(&to_summarized_group/1)
+ |> Enum.map(&limit_itinerary_count(&1, take_from_end?))
+ |> Enum.map(&to_group(&1, take_from_end?))
|> Enum.sort_by(fn
- %{itineraries: [%{tag: tag} | _] = _} ->
+ %ItineraryGroup{summary: %{tag: tag}} ->
Enum.find_index(ItineraryTag.tag_priority_order(), &(&1 == tag))
-
- _ ->
- -1
end)
+ |> Enum.take(5)
end
+ def max_per_group, do: @max_per_group
+
defp unique_legs_to_hash(%Itinerary{legs: legs}) do
legs
|> Enum.reject(&short_walking_leg?/1)
@@ -69,22 +55,44 @@ defmodule Dotcom.TripPlan.ItineraryGroups do
grouped_itineraries
end
- defp to_summarized_group(grouped_itineraries) do
- %{
- itineraries: ItineraryTag.sort_tagged(grouped_itineraries),
- summary: summary(grouped_itineraries)
- }
+ defp limit_itinerary_count(itineraries, take_from_end?) do
+ if take_from_end? do
+ Enum.take(itineraries, -@max_per_group)
+ else
+ Enum.take(itineraries, @max_per_group)
+ end
end
- defp limit_itinerary_count(itineraries) do
- Enum.take(itineraries, @max_per_group)
+ # Summarize a group of chronologically-sorted itineraries, noting the
+ # - representative_index: Which itinerary should represent the group? It will
+ # be either the first or final one.
+ # - representative_time: An itinerary departs at the :start time and arrives
+ # by the :stop time. This denotes which of those is relevant to the group.
+ defp to_group(grouped_itineraries, take_from_end?) do
+ representative_index = if(take_from_end?, do: Enum.count(grouped_itineraries) - 1, else: 0)
+
+ summary =
+ grouped_itineraries
+ |> Enum.at(representative_index)
+ |> to_summary(grouped_itineraries)
+
+ %ItineraryGroup{
+ # TODO: use second arg to sort by end time instead of start time
+ itineraries: ItineraryTag.sort_tagged(grouped_itineraries),
+ representative_index: representative_index,
+ representative_time: if(take_from_end?, do: :stop, else: :start),
+ summary: summary
+ }
end
- defp summary(itineraries) do
- itineraries
- |> Enum.map(&to_map_with_fare/1)
- |> to_summary()
- |> Map.put(:summarized_legs, to_summarized_legs(itineraries))
+ @doc """
+ The itinerary summary additionally includes a fare and summarized legs
+ """
+ @spec to_summary(Itinerary.t(), [Itinerary.t()]) :: map()
+ def to_summary(representative_itinerary, grouped_itineraries) do
+ representative_itinerary
+ |> to_map_with_fare()
+ |> Map.put(:summarized_legs, to_summarized_legs(grouped_itineraries))
end
defp to_summarized_legs(itineraries) do
@@ -109,7 +117,7 @@ defmodule Dotcom.TripPlan.ItineraryGroups do
defp to_map_with_fare(itinerary) do
itinerary
- |> Map.take([:start, :stop, :tag, :duration, :accessible?, :walk_distance])
+ |> Map.take([:accessible?, :duration, :start, :stop, :tag, :walk_distance])
|> Map.put(
:total_cost,
get_one_way_total_by_type(itinerary, :highest_one_way_fare)
@@ -126,34 +134,6 @@ defmodule Dotcom.TripPlan.ItineraryGroups do
|> Enum.map(fn {leg, _} -> leg end)
end
- defp to_summary(itinerary_maps) do
- # for most of the summary we can reflect the first itinerary
- [
- %{
- tag: tag,
- accessible?: accessible,
- total_cost: total_cost,
- duration: duration,
- walk_distance: walk_distance,
- stop: first_stop
- }
- | _
- ] = itinerary_maps
-
- [first_start | next_starts] = Enum.map(itinerary_maps, & &1.start)
-
- %{
- first_start: first_start,
- first_stop: first_stop,
- next_starts: next_starts,
- tag: if(tag, do: Atom.to_string(tag) |> String.replace("_", " ")),
- duration: duration,
- accessible?: accessible,
- walk_distance: walk_distance,
- total_cost: total_cost
- }
- end
-
defp summarize_legs(%{walk_minutes: new}, %{walk_minutes: old} = summary) do
%{summary | walk_minutes: new + old}
end
diff --git a/lib/dotcom_web/components/trip_planner/itinerary_detail.ex b/lib/dotcom_web/components/trip_planner/itinerary_detail.ex
index 82e8d9b17d..9254f00c70 100644
--- a/lib/dotcom_web/components/trip_planner/itinerary_detail.ex
+++ b/lib/dotcom_web/components/trip_planner/itinerary_detail.ex
@@ -14,48 +14,6 @@ defmodule DotcomWeb.Components.TripPlanner.ItineraryDetail do
alias Dotcom.TripPlan.Alerts
def itinerary_detail(assigns) do
- itinerary_group =
- Enum.at(assigns.results.itinerary_groups, assigns.results.itinerary_group_selection || 0)
-
- itinerary = Enum.at(itinerary_group.itineraries, assigns.results.itinerary_selection || 0)
-
- assigns = %{
- itinerary: itinerary,
- itinerary_selection: assigns.results.itinerary_selection,
- itineraries: itinerary_group.itineraries
- }
-
- ~H"""
-
- <.depart_at_buttons itineraries={@itineraries} itinerary_selection={@itinerary_selection} />
- <.specific_itinerary_detail itinerary={@itinerary} />
-
- """
- end
-
- defp depart_at_buttons(assigns) do
- ~H"""
- 1}>
-
-
Depart at
-
- <.button
- :for={{itinerary, index} <- Enum.with_index(@itineraries)}
- type="button"
- class={if(@itinerary_selection == index, do: "bg-brand-primary-lightest")}
- size="small"
- variant="secondary"
- phx-click="select_itinerary"
- phx-value-index={index}
- >
- {Timex.format!(itinerary.start, "%-I:%M%p", :strftime) |> String.downcase()}
-
-
-
- """
- end
-
- defp specific_itinerary_detail(assigns) do
assigns =
assigns
|> assign_new(:alerts, fn -> Alerts.from_itinerary(assigns.itinerary) end)
diff --git a/lib/dotcom_web/components/trip_planner/itinerary_summary.ex b/lib/dotcom_web/components/trip_planner/itinerary_summary.ex
index 4a115dd877..065e90f024 100644
--- a/lib/dotcom_web/components/trip_planner/itinerary_summary.ex
+++ b/lib/dotcom_web/components/trip_planner/itinerary_summary.ex
@@ -5,12 +5,14 @@ defmodule DotcomWeb.Components.TripPlanner.ItinerarySummary do
use DotcomWeb, :component
+ attr :summary, :map, required: true
+
def itinerary_summary(assigns) do
~H"""
- {format_datetime_full(@summary.first_start)} - {format_datetime_full(@summary.first_stop)}
+ {format_datetime_full(@summary.start)} - {format_datetime_full(@summary.stop)}
{@summary.duration} min
diff --git a/lib/dotcom_web/components/trip_planner/results.ex b/lib/dotcom_web/components/trip_planner/results.ex
index 6b13ea783e..30cb6f25e7 100644
--- a/lib/dotcom_web/components/trip_planner/results.ex
+++ b/lib/dotcom_web/components/trip_planner/results.ex
@@ -8,6 +8,8 @@ defmodule DotcomWeb.Components.TripPlanner.Results do
import DotcomWeb.Components.TripPlanner.{ItineraryDetail, ItinerarySummary}
+ alias Dotcom.TripPlan.{ItineraryGroup, ItineraryGroups}
+
def results(assigns) do
~H"""
- {summary.tag}
+ {Phoenix.Naming.humanize(group.summary.tag)}
- <.itinerary_summary summary={summary} />
+ <.itinerary_summary summary={group.summary} />
-
0} class="grow text-sm text-grey-dark">
- Similar {if(Enum.count(summary.next_starts) == 1,
- do: "trip departs",
- else: "trips depart"
- )} at {Enum.map(
- summary.next_starts,
- &Timex.format!(&1, "%-I:%M", :strftime)
- )
- |> Enum.join(", ")}
+
+ {ItineraryGroup.options_text(group)}