Credo: the Sanctum web pages in smaller pieces

063bd4c · Gabe Morell · 2026-11-04 21:04

12 files +262 -329
Message
{commit_body(@commit)}

Files changed

modified apps/room_sanctum/lib/room_sanctum_web/components/query_geospatial_map.ex
+123 −115
@@ -508,40 +508,40 @@ defmodule RoomSanctumWeb.Components.QueryGeospatialMap do
508 508 cond do
509 509 query.geom != nil ->
510 510 # Handle PostGIS geometry - you may need to adjust this based on your Geo setup
511 − case query.geom do
512 − %Geo.Point{coordinates: {lng, lat}} -> {lat, lng}
513 − # fallback
514 − _ -> {0, 0}
515 − end
511 + point_lat_lng(query.geom)
516 512
517 513 query.source && has_point_data?(query.source) ->
518 − source = query.source
519 − geom = Map.get(source, :geom, nil)
520 − lat = Map.get(source, :lat, nil)
521 − lng = Map.get(source, :lng, nil)
522 − latitude = Map.get(source, :latitude, nil)
523 − longitude = Map.get(source, :longitude, nil)
514 + source_coordinates(query.source)
524 515
525 − cond do
526 − geom != nil ->
527 − case geom do
528 − %Geo.Point{coordinates: {lng, lat}} -> {lat, lng}
529 − _ -> {0, 0}
530 − end
516 + # Handle data from the query's underlying stop/thing being queried
517 + query.query && extract_from_query_data(query) ->
518 + extract_from_query_data(query)
531 519
532 − lat != nil && lng != nil ->
533 − {lat, lng}
520 + true ->
521 + {0, 0}
522 + end
523 + end
534 524
535 − latitude != nil && longitude != nil ->
536 − {latitude, longitude}
525 + defp point_lat_lng(%Geo.Point{coordinates: {lng, lat}}), do: {lat, lng}
526 + # fallback
527 + defp point_lat_lng(_geom), do: {0, 0}
537 528
538 − true ->
539 − {0, 0}
540 − end
529 + defp source_coordinates(source) do
530 + geom = Map.get(source, :geom, nil)
531 + lat = Map.get(source, :lat, nil)
532 + lng = Map.get(source, :lng, nil)
533 + latitude = Map.get(source, :latitude, nil)
534 + longitude = Map.get(source, :longitude, nil)
541 535
542 − # Handle data from the query's underlying stop/thing being queried
543 − query.query && extract_from_query_data(query) ->
544 − extract_from_query_data(query)
536 + cond do
537 + geom != nil ->
538 + point_lat_lng(geom)
539 +
540 + lat != nil && lng != nil ->
541 + {lat, lng}
542 +
543 + latitude != nil && longitude != nil ->
544 + {latitude, longitude}
545 545
546 546 true ->
547 547 {0, 0}
@@ -552,38 +552,10 @@ defmodule RoomSanctumWeb.Components.QueryGeospatialMap do
552 552 defp extract_from_query_data(query) do
553 553 case query.source.type do
554 554 :gtfs ->
555 − # For GTFS, get coordinates from the stop being queried or vehicle positions
556 − cond do
557 − query.query && Map.has_key?(query.query, :stop) ->
558 − get_gtfs_stop_coordinates(query.source.id, query.query.stop)
559 −
560 − # Check if this is a vehicle position query
561 − query.source.config && Map.get(query.source.config, :url_rt_vp) ->
562 − get_gtfs_vehicle_coordinates(query.source.id, query)
563 −
564 − true ->
565 − false
566 − end
555 + gtfs_query_coordinates(query)
567 556
568 557 :gbfs ->
569 − # A station query is placed at its dock; an area query is placed at the
570 − # foci it is drawn around, which is also where its radius is centred.
571 − # Mode first: an area query still carries the stop_id field, it is just
572 − # empty, and asking about the field rather than the mode sent every
573 − # area query looking for a station called "".
574 − cond do
575 − query.query && Map.get(query.query, :mode) == :area ->
576 − foci_coordinates(query)
577 −
578 − query.query && Map.get(query.query, :stop_id) not in [nil, ""] ->
579 − get_gbfs_station_coordinates(query.source.id, query.query.stop_id)
580 −
581 − query.query && (Map.has_key?(query.query, :radius) || Map.has_key?(query.query, :point)) ->
582 − get_gbfs_free_bikes_in_area_coordinates(query.source.id, query.query)
583 −
584 − true ->
585 − false
586 − end
558 + gbfs_query_coordinates(query)
587 559
588 560 # Everything else that knows where it is says so with a foci: aqi,
589 561 # weather, tidal, ephem, pollen, drought and icarus area queries all
@@ -593,6 +565,42 @@ defmodule RoomSanctumWeb.Components.QueryGeospatialMap do
593 565 end
594 566 end
595 567
568 + # For GTFS, get coordinates from the stop being queried or vehicle positions
569 + defp gtfs_query_coordinates(query) do
570 + cond do
571 + query.query && Map.has_key?(query.query, :stop) ->
572 + get_gtfs_stop_coordinates(query.source.id, query.query.stop)
573 +
574 + # Check if this is a vehicle position query
575 + query.source.config && Map.get(query.source.config, :url_rt_vp) ->
576 + get_gtfs_vehicle_coordinates(query.source.id, query)
577 +
578 + true ->
579 + false
580 + end
581 + end
582 +
583 + # A station query is placed at its dock; an area query is placed at the
584 + # foci it is drawn around, which is also where its radius is centred.
585 + # Mode first: an area query still carries the stop_id field, it is just
586 + # empty, and asking about the field rather than the mode sent every
587 + # area query looking for a station called "".
588 + defp gbfs_query_coordinates(query) do
589 + cond do
590 + query.query && Map.get(query.query, :mode) == :area ->
591 + foci_coordinates(query)
592 +
593 + query.query && Map.get(query.query, :stop_id) not in [nil, ""] ->
594 + get_gbfs_station_coordinates(query.source.id, query.query.stop_id)
595 +
596 + query.query && (Map.has_key?(query.query, :radius) || Map.has_key?(query.query, :point)) ->
597 + get_gbfs_free_bikes_in_area_coordinates(query.source.id, query.query)
598 +
599 + true ->
600 + false
601 + end
602 + end
603 +
596 604 defp foci_coordinates(%{query: %{foci_id: foci_id}}) when not is_nil(foci_id) do
597 605 get_foci_coordinates(foci_id)
598 606 end
@@ -638,23 +646,21 @@ defmodule RoomSanctumWeb.Components.QueryGeospatialMap do
638 646 end
639 647 end
640 648
641 − # Get coordinates for GBFS free bikes in area
642 − defp get_gbfs_free_bikes_in_area_coordinates(source_id, query_params) do
643 − # Extract point and radius from query parameters
644 − {point, radius} =
645 − case query_params do
646 − %{point: %Geo.Point{} = geo_point, radius: radius} ->
647 − {geo_point, radius}
649 + defp area_of(%{point: %Geo.Point{} = geo_point, radius: radius}), do: {geo_point, radius}
648 650
649 − %{lat: lat, lng: lng, radius: radius} when is_number(lat) and is_number(lng) ->
650 − {%Geo.Point{coordinates: {lng, lat}}, radius}
651 + defp area_of(%{lat: lat, lng: lng, radius: radius}) when is_number(lat) and is_number(lng),
652 + do: {%Geo.Point{coordinates: {lng, lat}}, radius}
651 653
652 − %{latitude: lat, longitude: lng, radius: radius} when is_number(lat) and is_number(lng) ->
653 − {%Geo.Point{coordinates: {lng, lat}}, radius}
654 + defp area_of(%{latitude: lat, longitude: lng, radius: radius})
655 + when is_number(lat) and is_number(lng),
656 + do: {%Geo.Point{coordinates: {lng, lat}}, radius}
654 657
655 − _ ->
656 − {nil, nil}
657 − end
658 + defp area_of(_query_params), do: {nil, nil}
659 +
660 + # Get coordinates for GBFS free bikes in area
661 + defp get_gbfs_free_bikes_in_area_coordinates(source_id, query_params) do
662 + # Extract point and radius from query parameters
663 + {point, radius} = area_of(query_params)
658 664
659 665 case {point, radius} do
660 666 {%Geo.Point{} = geo_point, radius} when is_number(radius) ->
@@ -842,6 +848,19 @@ defmodule RoomSanctumWeb.Components.QueryGeospatialMap do
842 848 # with `Map.get`; these two were the exception.
843 849 defp point_of(bike), do: Map.get(bike, :point) || Map.get(bike, :place)
844 850
851 + defp bike_position(bike) do
852 + cond do
853 + point_of(bike) != nil ->
854 + point_lat_lng(point_of(bike))
855 +
856 + Map.get(bike, :lat) != nil && Map.get(bike, :lon) != nil ->
857 + {Map.get(bike, :lat), Map.get(bike, :lon)}
858 +
859 + true ->
860 + {0, 0}
861 + end
862 + end
863 +
845 864 # Format free bikes for map display
846 865 defp format_free_bikes(free_bikes) do
847 866 # A feed declares how far each of its vehicle types can go -- Bay Wheels
@@ -854,20 +873,7 @@ defmodule RoomSanctumWeb.Components.QueryGeospatialMap do
854 873 point_of(bike) != nil || (Map.get(bike, :lat) != nil && Map.get(bike, :lon) != nil)
855 874 end)
856 875 |> Enum.map(fn bike ->
857 − {lat, lng} =
858 − cond do
859 − point_of(bike) != nil ->
860 − case point_of(bike) do
861 − %Geo.Point{coordinates: {lng, lat}} -> {lat, lng}
862 − _ -> {0, 0}
863 − end
864 −
865 − Map.get(bike, :lat) != nil && Map.get(bike, :lon) != nil ->
866 − {Map.get(bike, :lat), Map.get(bike, :lon)}
867 −
868 − true ->
869 − {0, 0}
870 − end
876 + {lat, lng} = bike_position(bike)
871 877
872 878 # Extract additional fields with defaults
873 879 is_disabled = Map.get(bike, :is_disabled, false)
@@ -1091,20 +1097,7 @@ defmodule RoomSanctumWeb.Components.QueryGeospatialMap do
1091 1097 station.place != nil || (station.lat != nil && station.lon != nil)
1092 1098 end)
1093 1099 |> Enum.map(fn station ->
1094 − {lat, lng} =
1095 − cond do
1096 − station.place != nil ->
1097 − case station.place do
1098 − %Geo.Point{coordinates: {lng, lat}} -> {lat, lng}
1099 − _ -> {0, 0}
1100 − end
1101 −
1102 − station.lat != nil && station.lon != nil ->
1103 − {station.lat, station.lon}
1104 −
1105 − true ->
1106 − {0, 0}
1107 − end
1100 + {lat, lng} = station_position(station)
1108 1101
1109 1102 # Get associated station status
1110 1103 status = Map.get(status_map, station.station_id, nil)
@@ -1135,25 +1128,40 @@ defmodule RoomSanctumWeb.Components.QueryGeospatialMap do
1135 1128 }
1136 1129
1137 1130 # Add station status fields if available
1138 − if status do
1139 − station_data
1140 − # What the dock currently holds, for the popup -- which otherwise says
1141 − # only where the dock is, next to a button offering to query it.
1142 − |> Map.put(:summary, dock_summary(status, station))
1143 − |> Map.put(:num_bikes_available, status.num_bikes_available)
1144 − |> Map.put(:num_ebikes_available, status.num_ebikes_available)
1145 − |> Map.put(:num_docks_available, status.num_docks_available)
1146 − |> Map.put(:is_installed, status.is_installed)
1147 − |> Map.put(:is_renting, status.is_renting)
1148 − |> Map.put(:is_returning, status.is_returning)
1149 − |> Map.put(:station_status, status.station_status)
1150 − |> Map.put(:last_reported, status.last_reported)
1151 − else
1152 − station_data
1153 − end
1131 + with_status(station_data, status, station)
1154 1132 end)
1155 1133 end
1156 1134
1135 + defp station_position(station) do
1136 + cond do
1137 + station.place != nil ->
1138 + point_lat_lng(station.place)
1139 +
1140 + station.lat != nil && station.lon != nil ->
1141 + {station.lat, station.lon}
1142 +
1143 + true ->
1144 + {0, 0}
1145 + end
1146 + end
1147 +
1148 + defp with_status(station_data, nil, _station), do: station_data
1149 +
1150 + defp with_status(station_data, status, station) do
1151 + station_data
1152 + # What the dock currently holds, for the popup -- which otherwise says
1153 + # only where the dock is, next to a button offering to query it.
1154 + |> Map.put(:summary, dock_summary(status, station))
1155 + |> Map.put(:num_bikes_available, status.num_bikes_available)
1156 + |> Map.put(:num_ebikes_available, status.num_ebikes_available)
1157 + |> Map.put(:num_docks_available, status.num_docks_available)
1158 + |> Map.put(:is_installed, status.is_installed)
1159 + |> Map.put(:is_renting, status.is_renting)
1160 + |> Map.put(:is_returning, status.is_returning)
1161 + |> Map.put(:station_status, status.station_status)
1162 + |> Map.put(:last_reported, status.last_reported)
1163 + end
1164 +
1157 1165 # A dock in the same label/value shape a query summary uses, so one popup
1158 1166 # renderer serves both.
1159 1167 # Laid out as the preview card lays a dock out: standard bikes, electric
modified apps/room_sanctum/lib/room_sanctum_web/live/cfg_query_live/show.ex
+50 −107
@@ -105,113 +105,7 @@ defmodule RoomSanctumWeb.QueryLive.Show do
105 105 def handle_info(:update, socket) do
106 106 Process.send_after(self(), :update, 5000)
107 107
108 − result =
109 − case socket.assigns.query.source.type do
110 − :gtfs ->
111 − RoomGtfs.Worker.query_stop(socket.assigns.query.source.id, socket.assigns.query.query)
112 −
113 − :gbfs ->
114 − RoomGbfs.Worker.query_stop(socket.assigns.query.source.id, socket.assigns.query.query)
115 −
116 − :tidal ->
117 − RoomTidal.Worker.query_tides(socket.assigns.query.source.id, socket.assigns.query.query)
118 −
119 − :weather ->
120 − RoomWeather.Worker.query_weather(
121 − socket.assigns.query.source.id,
122 − socket.assigns.query.query
123 − )
124 −
125 − :aqi ->
126 − RoomAirQuality.Worker.query_place(
127 − socket.assigns.query.source.id,
128 − socket.assigns.query.query
129 − )
130 −
131 − :ephem ->
132 − RoomEphem.Worker.query_ephem(
133 − socket.assigns.query.source.id,
134 − socket.assigns.query.query
135 − )
136 −
137 − :calendar ->
138 − RoomCalendar.Worker.query_calendar(
139 − socket.assigns.query.source.id,
140 − socket.assigns.query.query
141 − )
142 −
143 − :cronos ->
144 − RoomCronos.Worker.query_cronos(
145 − # we want the query's name here
146 − socket.assigns.query.id,
147 − socket.assigns.query.query
148 − )
149 −
150 − :gitlab ->
151 − RoomGitlab.Worker.read_jobs(
152 − socket.assigns.query.source.id,
153 − socket.assigns.query.query
154 − )
155 −
156 − :github ->
157 − level = Map.get(socket.assigns.query.query, :level) || "runs"
158 −
159 − case level do
160 − "jobs" ->
161 − RoomGithub.Worker.read_jobs(
162 − socket.assigns.query.source.id,
163 − socket.assigns.query.query
164 − )
165 −
166 − _ ->
167 − RoomGithub.Worker.read_runs(
168 − socket.assigns.query.source.id,
169 − socket.assigns.query.query
170 − )
171 − end
172 −
173 − :packages ->
174 − RoomPackages.Worker.read(
175 − socket.assigns.query.source.id,
176 − socket.assigns.query.query
177 − )
178 −
179 − :drought ->
180 − RoomDrought.Worker.read(
181 − socket.assigns.query.source.id,
182 − socket.assigns.query.query
183 − )
184 −
185 − :pollen ->
186 − RoomPollen.Worker.read(
187 − socket.assigns.query.source.id,
188 − socket.assigns.query.query
189 − )
190 −
191 − :icarus ->
192 − RoomIcarus.Worker.read(
193 − socket.assigns.query.source.id,
194 − socket.assigns.query.query
195 − )
196 −
197 − :mailbox ->
198 − RoomHermes.Mail.ImapWorker.read(
199 − socket.assigns.query.source.id,
200 − socket.assigns.query.query
201 − )
202 −
203 − :treasury ->
204 − RoomTreasury.Worker.read(
205 − socket.assigns.query.source.id,
206 − socket.assigns.query.query
207 − )
208 −
209 − :bourse ->
210 − RoomBourse.Worker.read(
211 − socket.assigns.query.source.id,
212 − socket.assigns.query.query
213 − )
214 − end
108 + result = preview_for(socket.assigns.query)
215 109
216 110 query = socket.assigns.query
217 111
@@ -226,6 +120,55 @@ defmodule RoomSanctumWeb.QueryLive.Show do
226 120 |> assign(:nearby_stations, nearby_stations(query))}
227 121 end
228 122
123 + # Workers answering through read/2.
124 + @readers %{
125 + packages: RoomPackages.Worker,
126 + drought: RoomDrought.Worker,
127 + pollen: RoomPollen.Worker,
128 + icarus: RoomIcarus.Worker,
129 + mailbox: RoomHermes.Mail.ImapWorker,
130 + treasury: RoomTreasury.Worker,
131 + bourse: RoomBourse.Worker
132 + }
133 +
134 + defp preview_for(%{source: %{type: :gtfs}} = q),
135 + do: RoomGtfs.Worker.query_stop(q.source.id, q.query)
136 +
137 + defp preview_for(%{source: %{type: :gbfs}} = q),
138 + do: RoomGbfs.Worker.query_stop(q.source.id, q.query)
139 +
140 + defp preview_for(%{source: %{type: :tidal}} = q),
141 + do: RoomTidal.Worker.query_tides(q.source.id, q.query)
142 +
143 + defp preview_for(%{source: %{type: :weather}} = q),
144 + do: RoomWeather.Worker.query_weather(q.source.id, q.query)
145 +
146 + defp preview_for(%{source: %{type: :aqi}} = q),
147 + do: RoomAirQuality.Worker.query_place(q.source.id, q.query)
148 +
149 + defp preview_for(%{source: %{type: :ephem}} = q),
150 + do: RoomEphem.Worker.query_ephem(q.source.id, q.query)
151 +
152 + defp preview_for(%{source: %{type: :calendar}} = q),
153 + do: RoomCalendar.Worker.query_calendar(q.source.id, q.query)
154 +
155 + # we want the query's name here
156 + defp preview_for(%{source: %{type: :cronos}} = q),
157 + do: RoomCronos.Worker.query_cronos(q.id, q.query)
158 +
159 + defp preview_for(%{source: %{type: :gitlab}} = q),
160 + do: RoomGitlab.Worker.read_jobs(q.source.id, q.query)
161 +
162 + defp preview_for(%{source: %{type: :github}} = q) do
163 + case Map.get(q.query, :level) || "runs" do
164 + "jobs" -> RoomGithub.Worker.read_jobs(q.source.id, q.query)
165 + _ -> RoomGithub.Worker.read_runs(q.source.id, q.query)
166 + end
167 + end
168 +
169 + defp preview_for(%{source: %{type: type}} = q) when is_map_key(@readers, type),
170 + do: Map.fetch!(@readers, type).read(q.source.id, q.query)
171 +
229 172 # For an air quality query, the answer is one station -- so the neighbours
230 173 # are the context that makes it readable: whether the number is local or the
231 174 # whole city is like that. Ordered closest first, the query's own station
modified apps/room_sanctum/lib/room_sanctum_web/live/cfg_source_live/form_component.ex
+69 −73
@@ -261,20 +261,7 @@ defmodule RoomSanctumWeb.SourceLive.FormComponent do
261 261
262 262 watched = MapSet.union(repo_set, owner_repos) |> MapSet.size()
263 263
264 − {watched, source} =
265 − cond do
266 − watched > 0 ->
267 − {watched, if(owners == [], do: :exact, else: :partial)}
268 −
269 − owners != [] ->
270 − {0, :partial}
271 −
272 − repos == [] and owners == [] and available_repos != [] ->
273 − {length(available_repos), :exact}
274 −
275 − true ->
276 − {0, :unknown}
277 − end
264 + {watched, source} = watched_count(watched, repos, owners, available_repos)
278 265
279 266 cycles_per_hour = div(3600, max(poll_seconds, 1))
280 267 # 1 runs poll per repo per cycle + ~1 jobs poll per repo (one active run avg)
@@ -286,6 +273,25 @@ defmodule RoomSanctumWeb.SourceLive.FormComponent do
286 273 defp github_estimate(_, _, _),
287 274 do: %{watched: 0, calls: 0, source: :unknown, cycles: 0}
288 275
276 + # The watched count both estimates report, and how sure of it they are. Named
277 + # are the repos or projects listed one by one, groups the owners or
278 + # namespaces listed whole.
279 + defp watched_count(watched, named, groups, available) do
280 + cond do
281 + watched > 0 ->
282 + {watched, if(groups == [], do: :exact, else: :partial)}
283 +
284 + groups != [] ->
285 + {0, :partial}
286 +
287 + named == [] and groups == [] and available != [] ->
288 + {length(available), :exact}
289 +
290 + true ->
291 + {0, :unknown}
292 + end
293 + end
294 +
289 295 def github_estimate_class(calls) when calls >= 4500, do: "text-error"
290 296 def github_estimate_class(calls) when calls >= 2500, do: "text-warning"
291 297 def github_estimate_class(_), do: "text-base-content/70"
@@ -351,20 +357,7 @@ defmodule RoomSanctumWeb.SourceLive.FormComponent do
351 357
352 358 watched = MapSet.union(project_set, namespace_projects) |> MapSet.size()
353 359
354 − {watched, source} =
355 − cond do
356 − watched > 0 ->
357 − {watched, if(namespaces == [], do: :exact, else: :partial)}
358 −
359 − namespaces != [] ->
360 − {0, :partial}
361 −
362 − projects == [] and namespaces == [] and available_projects != [] ->
363 − {length(available_projects), :exact}
364 −
365 − true ->
366 − {0, :unknown}
367 − end
360 + {watched, source} = watched_count(watched, projects, namespaces, available_projects)
368 361
369 362 cycles_per_hour = div(3600, max(poll_seconds, 1))
370 363 # 1 jobs poll per project per cycle
@@ -507,12 +500,7 @@ defmodule RoomSanctumWeb.SourceLive.FormComponent do
507 500 |> then(fn s -> if is_binary(s), do: String.to_integer(s), else: s end)
508 501 |> seconds_to_idx()
509 502
510 − poll_secs =
511 − case get_in(source_params, ["config", "poll_seconds"]) do
512 − s when is_integer(s) -> s
513 − s when is_binary(s) -> String.to_integer(s)
514 − _ -> nil
515 − end
503 + poll_secs = poll_secs(get_in(source_params, ["config", "poll_seconds"]))
516 504
517 505 type =
518 506 source_params["type"] ||
@@ -526,31 +514,8 @@ defmodule RoomSanctumWeb.SourceLive.FormComponent do
526 514 config_for_estimate =
527 515 Ecto.Changeset.get_field(changeset, :config) || socket.assigns.source.config || %{}
528 516
529 − {gh_idx, gh_estimate} =
530 − if type == "github" do
531 − gh_secs = poll_secs || 60
532 −
533 − {github_secs_to_idx(gh_secs),
534 − github_estimate(config_for_estimate, socket.assigns[:gh_available_repos] || [], gh_secs)}
535 − else
536 − {socket.assigns[:github_poll_idx] || 2,
537 − socket.assigns[:github_estimate] || %{watched: 0, calls: 0, source: :unknown, cycles: 0}}
538 − end
539 −
540 − {gl_idx, gl_estimate} =
541 − if type == "gitlab" do
542 − gl_secs = poll_secs || 10
543 −
544 − {gitlab_secs_to_idx(gl_secs),
545 − gitlab_estimate(
546 − config_for_estimate,
547 − socket.assigns[:gl_available_projects] || [],
548 − gl_secs
549 − )}
550 − else
551 − {socket.assigns[:gitlab_poll_idx] || 1,
552 − socket.assigns[:gitlab_estimate] || %{watched: 0, calls: 0, source: :unknown, cycles: 0}}
553 − end
517 + {gh_idx, gh_estimate} = github_poll(type, poll_secs, config_for_estimate, socket.assigns)
518 + {gl_idx, gl_estimate} = gitlab_poll(type, poll_secs, config_for_estimate, socket.assigns)
554 519
555 520 # Read back from the converted params rather than the changeset: a slider
556 521 # the user has just dragged is in the params, and its label has to move with
@@ -559,20 +524,7 @@ defmodule RoomSanctumWeb.SourceLive.FormComponent do
559 524
560 525 rt_idxs =
561 526 Map.new(rt_groups, fn {_url, kinds} ->
562 − # From the converted params, not the changeset: a slider just dragged is
563 − # in the params, and its label has to move with it.
564 − seconds =
565 − kinds
566 − |> Enum.map(&get_in(source_params, ["config", "rt_period_#{&1}"]))
567 − |> Enum.reject(&is_nil/1)
568 −
569 − idx =
570 − case seconds do
571 − [] -> rt_group_idx(config_for_estimate, kinds)
572 − values -> values |> Enum.map(&to_seconds/1) |> Enum.min() |> rt_secs_to_idx()
573 − end
574 −
575 − {kinds, idx}
527 + {kinds, rt_idx(kinds, source_params, config_for_estimate)}
576 528 end)
577 529
578 530 {:noreply,
@@ -600,6 +552,50 @@ defmodule RoomSanctumWeb.SourceLive.FormComponent do
600 552 save_source(socket, socket.assigns.action, source_params)
601 553 end
602 554
555 + defp poll_secs(s) when is_integer(s), do: s
556 + defp poll_secs(s) when is_binary(s), do: String.to_integer(s)
557 + defp poll_secs(_), do: nil
558 +
559 + # The slider position and call estimate for a GitHub source; any other type
560 + # keeps what it had.
561 + defp github_poll("github", poll_secs, config, assigns) do
562 + gh_secs = poll_secs || 60
563 +
564 + {github_secs_to_idx(gh_secs),
565 + github_estimate(config, assigns[:gh_available_repos] || [], gh_secs)}
566 + end
567 +
568 + defp github_poll(_type, _poll_secs, _config, assigns) do
569 + {assigns[:github_poll_idx] || 2,
570 + assigns[:github_estimate] || %{watched: 0, calls: 0, source: :unknown, cycles: 0}}
571 + end
572 +
573 + defp gitlab_poll("gitlab", poll_secs, config, assigns) do
574 + gl_secs = poll_secs || 10
575 +
576 + {gitlab_secs_to_idx(gl_secs),
577 + gitlab_estimate(config, assigns[:gl_available_projects] || [], gl_secs)}
578 + end
579 +
580 + defp gitlab_poll(_type, _poll_secs, _config, assigns) do
581 + {assigns[:gitlab_poll_idx] || 1,
582 + assigns[:gitlab_estimate] || %{watched: 0, calls: 0, source: :unknown, cycles: 0}}
583 + end
584 +
585 + # From the converted params, not the changeset: a slider just dragged is
586 + # in the params, and its label has to move with it.
587 + defp rt_idx(kinds, source_params, config) do
588 + seconds =
589 + kinds
590 + |> Enum.map(&get_in(source_params, ["config", "rt_period_#{&1}"]))
591 + |> Enum.reject(&is_nil/1)
592 +
593 + case seconds do
594 + [] -> rt_group_idx(config, kinds)
595 + values -> values |> Enum.map(&to_seconds/1) |> Enum.min() |> rt_secs_to_idx()
596 + end
597 + end
598 +
603 599 defp save_source(socket, :edit, source_params) do
604 600 case Configuration.update_source(socket.assigns.source, source_params) do
605 601 {:ok, source} ->
modified apps/room_sanctum/lib/room_sanctum_web/live/live_preview.ex
+20 −34
@@ -5,20 +5,19 @@ defmodule RoomSanctumWeb.LivePreview do
5 5 use PhoenixHTMLHelpers
6 6 import Phoenix.LiveView.Helpers
7 7
8 − defp gtfs_icon(route_str) do
9 − case route_str do
10 − "LightRail" -> "fa-train-tram"
11 − "Subway" -> "fa-train-subway"
12 − "Rail" -> "fa-train"
13 − "Bus" -> "fa-bus"
14 − "Ferry" -> "fa-ferry"
15 − "CableCar" -> "fa-cable-car"
16 − "Funicular" -> "fa-mountain"
17 − "Trolleybus" -> "fa-bus-simple"
18 − "Monorail" -> "fa-magic"
19 − _otherwise -> "fa-train-tram"
20 − end
21 − end
8 + @gtfs_icons %{
9 + "LightRail" => "fa-train-tram",
10 + "Subway" => "fa-train-subway",
11 + "Rail" => "fa-train",
12 + "Bus" => "fa-bus",
13 + "Ferry" => "fa-ferry",
14 + "CableCar" => "fa-cable-car",
15 + "Funicular" => "fa-mountain",
16 + "Trolleybus" => "fa-bus-simple",
17 + "Monorail" => "fa-magic"
18 + }
19 +
20 + defp gtfs_icon(route_str), do: Map.get(@gtfs_icons, route_str, "fa-train-tram")
22 21
23 22 defp weather_icon(status) do
24 23 case status do
@@ -68,30 +67,17 @@ defmodule RoomSanctumWeb.LivePreview do
68 67 def tsls(%Time{} = t), do: tsl(t |> Time.to_string())
69 68 def tsls(st), do: st |> String.slice(0, 8)
70 69
70 + # Sixteen points, with N again at the end for the bearings that round up to 360.
71 + @compass {"N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE", "S", "SSW", "SW", "WSW", "W", "WNW",
72 + "NW", "NNW", "N"}
73 +
71 74 defp to_l(direction) do
72 75 idx = (direction / 22.5) |> Kernel.round()
73 −
74 − case idx do
75 − 0 -> "N"
76 − 1 -> "NNE"
77 − 2 -> "NE"
78 − 3 -> "ENE"
79 − 4 -> "E"
80 − 5 -> "ESE"
81 − 6 -> "SE"
82 − 7 -> "SSE"
83 − 8 -> "S"
84 − 9 -> "SSW"
85 − 10 -> "SW"
86 − 11 -> "WSW"
87 − 12 -> "W"
88 − 13 -> "WNW"
89 − 14 -> "NW"
90 − 15 -> "NNW"
91 − 16 -> "N"
92 − end
76 + compass_point(idx)
93 77 end
94 78
79 + defp compass_point(idx) when idx in 0..16, do: elem(@compass, idx)
80 +
95 81 @doc """
96 82 A route drawn as the agency draws it.
97 83

Parents: ca307c7