Skip to content

Commit

Permalink
feat: Add a nicer summary view for trip planner output (#2204)
Browse files Browse the repository at this point in the history
---------

Co-authored-by: Cristen Jones <[email protected]>
  • Loading branch information
joshlarson and thecristen authored Nov 18, 2024
1 parent 5db761f commit a690232
Show file tree
Hide file tree
Showing 23 changed files with 764 additions and 207 deletions.
38 changes: 35 additions & 3 deletions assets/tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ module.exports = {
preflight: false
},
blocklist: ["container", "collapse"],
important: true,
content: [
...content,
"./js/**/*.js",
Expand All @@ -25,11 +24,44 @@ module.exports = {
"../lib/dotcom_web.ex",
"../lib/dotcom_web/**/*.*ex"
],
safelist: [...safelist],
safelist: [
...safelist,
{
pattern: /(bg|text|border|ring)-(logan-express|blue|green|orange|red|silver|bus|ferry|)./
}
],
theme: {
extend: {
colors: {
...colors
...colors,
gray: {
DEFAULT: "#494f5c",
dark: "#1c1e23",
light: "#788093",
lighter: "#b0b5c0"
},
"brand-primary": {
DEFAULT: "#165c96",
darkest: "#0b2f4c"
},
"logan-express": {
BB: "#f16823",
BT: "#0066cc",
DV: "#704c9f",
FH: "#e81d2d",
WO: "#00954c"
},
massport: "#104c8f",
subway: "#494f5c",
// These will come from the design system someday
"blue-line": "#003da5",
"green-line": "#00843d",
"orange-line": "#ed8b00",
"red-line": "#da291c",
"silver-line": "#7c878e",
"commuter-rail": "#80276c",
bus: "#ffc72c",
ferry: "#008eaa"
}
},
fontFamily: {
Expand Down
2 changes: 2 additions & 0 deletions config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ config :sentry,
root_source_code_paths: [File.cwd!()],
context_lines: 5

config :mbta_metro, custom_icons: ["#{File.cwd!()}/priv/static/icon-svg/*"]

for config_file <- Path.wildcard("config/{deps,dotcom}/*.exs") do
import_config("../#{config_file}")
end
Expand Down
69 changes: 0 additions & 69 deletions lib/dotcom/trip_plan/itinerary_group.ex

This file was deleted.

161 changes: 161 additions & 0 deletions lib/dotcom/trip_plan/itinerary_groups.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
defmodule Dotcom.TripPlan.ItineraryGroups do
@moduledoc """
Group itineraries by unique legs.
A unique leg is defined as a leg that has a unique combination of mode, from, and to.
But, this does not include walking legs that are less than 0.2 miles.
"""

alias Dotcom.TripPlan.{Itinerary, Leg, PersonalDetail, TransitDetail}
alias OpenTripPlannerClient.ItineraryTag

@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
itineraries
|> Enum.group_by(&unique_legs_to_hash/1)
|> Enum.map(&group_departures/1)
|> Enum.reject(&Enum.empty?(&1))
|> Enum.sort_by(fn
%{itineraries: [%{tag: tag} | _] = _} ->
Enum.find_index(ItineraryTag.tag_priority_order(), &(&1 == tag))

_ ->
-1
end)
end

defp unique_legs_to_hash(%Itinerary{legs: legs}) do
legs
|> Enum.reject(&short_walking_leg?/1)
|> Enum.map(&unique_leg_to_tuple/1)
|> :erlang.phash2()
end

defp unique_leg_to_tuple(%Leg{mode: %PersonalDetail{}} = leg) do
{:WALK, leg.from.name, leg.to.name}
end

defp unique_leg_to_tuple(%Leg{mode: %{route: route}} = leg) do
{Routes.Route.type_atom(route.type), leg.from.name, leg.to.name}
end

defp group_departures({_hash, grouped_itineraries}) do
summarized_legs =
grouped_itineraries
|> Enum.map(& &1.legs)
|> Enum.zip_with(&Function.identity/1)
|> Enum.map(fn legs ->
legs
|> Enum.uniq_by(&combined_leg_to_tuple/1)
|> Enum.map(&to_summarized_leg/1)
|> Enum.reduce(%{walk_minutes: 0, routes: []}, &summarize_legs/2)
end)
|> remove_short_intermediate_walks()

summary =
grouped_itineraries
|> Enum.map(fn itinerary ->
itinerary
|> Map.take([:start, :stop, :tag, :duration, :accessible?, :walk_distance])
|> Map.put(
:total_cost,
DotcomWeb.TripPlanView.get_one_way_total_by_type(itinerary, :highest_one_way_fare)
)
end)
|> summarize_itineraries()
|> Map.put(:summarized_legs, summarized_legs)

%{itineraries: ItineraryTag.sort_tagged(grouped_itineraries), summary: summary}
end

defp remove_short_intermediate_walks(summarized_legs) do
summarized_legs
|> Enum.with_index()
|> Enum.reject(fn {leg, index} ->
index > 0 && index < Kernel.length(summarized_legs) - 1 &&
(leg.routes == [] && leg.walk_minutes < 5)
end)
|> Enum.map(&elem(&1, 0))
end

defp summarize_itineraries(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

defp summarize_legs(
%{route: route},
%{routes: old_routes} = summary
) do
# should probably sort by sort_order, but the route data returned by OTP don't have sort orders! (yet?)
routes = [route | old_routes] |> Enum.sort_by(& &1.name)
%{summary | routes: routes}
end

defp to_summarized_leg(%Leg{mode: %PersonalDetail{}, duration: duration}) do
%{walk_minutes: duration}
end

defp to_summarized_leg(%Leg{mode: %TransitDetail{route: route}}) do
%{route: route}
end

defp short_walking_leg?(%Leg{mode: %PersonalDetail{}} = leg) do
leg.distance <= 0.2
end

defp short_walking_leg?(_), do: false

defp combined_leg_to_tuple(%Leg{mode: %PersonalDetail{}} = leg) do
unique_leg_to_tuple(leg)
end

defp combined_leg_to_tuple(%Leg{mode: %{route: route}} = leg) do
{route.id, leg.from.name, leg.to.name}
end
end
2 changes: 1 addition & 1 deletion lib/dotcom_web.ex
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ defmodule DotcomWeb do
use Gettext, backend: DotcomWeb.Gettext
use MbtaMetro

import DotcomWeb.{Components, ErrorHelpers}
import DotcomWeb.{Components, Components.RouteSymbols, ErrorHelpers}
import Phoenix.{HTML, LiveView.Helpers}
import PhoenixHTMLHelpers.Form, except: [label: 1]
import PhoenixHTMLHelpers.{Format, Link, Tag}
Expand Down
5 changes: 2 additions & 3 deletions lib/dotcom_web/components/components.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ defmodule DotcomWeb.Components do
"""
use Phoenix.Component

alias Heroicons

attr(:id, :string, required: true, doc: "A unique identifier for this search input.")

attr(:placeholder, :string,
Expand All @@ -29,10 +27,11 @@ defmodule DotcomWeb.Components do
]
)

slot :inner_block,
slot(:inner_block,
required: false,
doc:
"Additional content to render beneath the autocomplete component. With config_type='trip-planner', this can be used to render additional form elements to capture additional details about selected locations."
)

@doc """
Instantiates a search box using Algolia's Autocomplete.js library, configured
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ defmodule DotcomWeb.Components.LiveComponents.TripPlannerForm do
"""
use DotcomWeb, :live_component

import DotcomWeb.ViewHelpers, only: [svg: 1]
import MbtaMetro.Components.{Feedback, InputGroup}
import Phoenix.HTML.Form, only: [input_value: 2]

Expand Down Expand Up @@ -137,11 +136,9 @@ defmodule DotcomWeb.Components.LiveComponents.TripPlannerForm do
</:extra>
</.accordion>
</.fieldset>
<div class="inline-flex items-center gap-2">
<div class="inline-flex items-center gap-1">
<.input type="checkbox" field={f[:wheelchair]} label="Prefer accessible routes" />
<span class="mt-[.365em]" aria-hidden="true">
<%= svg("icon-accessible-small.svg") %>
</span>
<.icon type="icon-svg" name="icon-accessible-small" class="h-5 w-5" />
</div>
</div>
<div class="col-start-2 justify-self-end">
Expand Down
Loading

0 comments on commit a690232

Please sign in to comment.