neiam
/apollos_crib
It's Apollo's Crib
public · Issues · Pulls 1 · Labels · Forks · Compare · Actions failure · Badges · Packages
Log in to mark this repository.
Break a Plani out by stop, and ring its radius
11bc288 · Gabriel Morell · 2026-09-03 18:02
Message
{commit_body(@commit)}
Files changed
modified
?
+0
−0
Binary file — not rendered.
modified
?
+0
−0
Binary file — not rendered.
modified
apps/room_sanctum/lib/room_sanctum/configuration/plani.ex
+8
−1
| 36 | 36 | field :radius, :integer, default: 800 |
| 37 | 37 | field :limit, :integer, default: 5 |
| 38 | 38 | |
| 39 | + # One entry per stop rather than one per source. A vision publishes an | |
| 40 | + # entry per query and a query is one stop, so a client that draws a card | |
| 41 | + # per entry needs to know nothing about grouping. Blended is fewer entries | |
| 42 | + # and asks the client to render a list of its own. | |
| 43 | + field :break_out, :boolean, default: false | |
| 44 | + | |
| 39 | 45 | timestamps() |
| 40 | 46 | end |
| 41 | 47 |
| 51 | 57 | :sources, |
| 52 | 58 | :follow_tint, |
| 53 | 59 | :radius, |
| 54 | − :limit | |
| 60 | + :limit, | |
| 61 | + :break_out | |
| 55 | 62 | ]) |
| 56 | 63 | |> validate_required([:name, :home_foci_id]) |
| 57 | 64 | # The same bounds an area query keeps, and for the same reason: past a |
modified
apps/room_sanctum/lib/room_sanctum/workers/plani_worker.ex
+72
−16
| 133 | 133 | Logger.warning("plani #{state.id} source #{source_id}: #{message}") |
| 134 | 134 | {data, queries, places, Map.put(notes, source_id, {:error, message})} |
| 135 | 135 | |
| 136 | − {key, results, described, found} -> | |
| 137 | − {Map.put(data, key, results), [described | queries], found ++ places, | |
| 138 | − Map.put(notes, source_id, {:ok, length(results)})} | |
| 136 | + {entries, found} -> | |
| 137 | + # One source can answer with several entries now: broken out, a | |
| 138 | + # stop is an entry of its own. | |
| 139 | + data = | |
| 140 | + Enum.reduce(entries, data, fn {key, results, _q}, acc -> | |
| 141 | + Map.put(acc, key, results) | |
| 142 | + end) | |
| 143 | + | |
| 144 | + described = Enum.map(entries, fn {_key, _results, q} -> q end) | |
| 145 | + counted = entries |> Enum.map(fn {_k, results, _q} -> length(results) end) |> Enum.sum() | |
| 146 | + | |
| 147 | + {data, Enum.reverse(described) ++ queries, found ++ places, | |
| 148 | + Map.put(notes, source_id, {:ok, counted})} | |
| 139 | 149 | end |
| 140 | 150 | end) |
| 141 | 151 |
| 195 | 205 | :gtfs -> |
| 196 | 206 | stops = Storage.nearby_stops(source_id, anchor, plani.limit, plani.radius) |
| 197 | 207 | |
| 198 | − arrivals = | |
| 199 | − stops | |
| 200 | − |> Enum.flat_map(&Storage.get_upcoming_arrivals_for_stop(source_id, &1.stop_id, 8, :now, source.config.tz)) | |
| 201 | − |> Storage.fix_arrival_times() | |
| 202 | − |> Enum.sort_by(& &1.arrival_time) | |
| 203 | − |> Enum.take(16) | |
| 204 | − | |
| 205 | − {{source_id, :gtfs}, arrivals, described(source), places(stops, source, :stop)} | |
| 208 | + by_stop = | |
| 209 | + Enum.map(stops, fn stop -> | |
| 210 | + arrivals = | |
| 211 | + source_id | |
| 212 | + |> Storage.get_upcoming_arrivals_for_stop(stop.stop_id, 8, :now, source.config.tz) | |
| 213 | + |> Storage.fix_arrival_times() | |
| 214 | + |> Enum.sort_by(& &1.arrival_time) | |
| 215 | + | |
| 216 | + {stop, arrivals} | |
| 217 | + end) | |
| 218 | + | |
| 219 | + entries = | |
| 220 | + if plani.break_out do | |
| 221 | + # One entry per stop, named after it, the way a vision's queries | |
| 222 | + # are. A stop with nothing due is left out rather than published | |
| 223 | + # empty -- the same call the clients already make. | |
| 224 | + for {stop, arrivals} <- by_stop, arrivals != [] do | |
| 225 | + key = "#{source_id}:#{stop.stop_id}" | |
| 226 | + {{key, :gtfs}, arrivals, described(source, key, stop.stop_name)} | |
| 227 | + end | |
| 228 | + else | |
| 229 | + blended = | |
| 230 | + by_stop | |
| 231 | + |> Enum.flat_map(fn {_stop, arrivals} -> arrivals end) | |
| 232 | + |> Enum.sort_by(& &1.arrival_time) | |
| 233 | + |> Enum.take(16) | |
| 234 | + | |
| 235 | + [{{source_id, :gtfs}, blended, described(source)}] | |
| 236 | + end | |
| 237 | + | |
| 238 | + {entries, places(stops, source, :stop)} | |
| 206 | 239 | |
| 207 | 240 | :gbfs -> |
| 208 | 241 | # Both, because a source does not say which kind it is. A docked |
| 211 | 244 | bikes = Storage.free_bikes_near(source_id, anchor, plani.radius) |
| 212 | 245 | docks = Storage.stations_near(source_id, anchor, plani.radius) |
| 213 | 246 | |
| 214 | − {{source_id, :gbfs}, bikes ++ docks, described(source), | |
| 215 | − places(bikes, source, :bike) ++ places(docks, source, :dock)} | |
| 247 | + entries = | |
| 248 | + if plani.break_out do | |
| 249 | + # A dock is a place, so it gets its own entry. Loose bikes are not | |
| 250 | + # -- they are a count of what is lying about nearby -- so they stay | |
| 251 | + # together under the source. | |
| 252 | + per_dock = | |
| 253 | + for dock <- docks do | |
| 254 | + key = "#{source_id}:#{dock.station_id}" | |
| 255 | + {{key, :gbfs}, [dock], described(source, key, dock.name)} | |
| 256 | + end | |
| 257 | + | |
| 258 | + if bikes == [], | |
| 259 | + do: per_dock, | |
| 260 | + else: [{{source_id, :gbfs}, bikes, described(source)} | per_dock] | |
| 261 | + else | |
| 262 | + [{{source_id, :gbfs}, bikes ++ docks, described(source)}] | |
| 263 | + end | |
| 264 | + | |
| 265 | + {entries, places(bikes, source, :bike) ++ places(docks, source, :dock)} | |
| 216 | 266 | |
| 217 | 267 | :aqi -> |
| 218 | 268 | readings = Storage.nearby_aqi_stations(source_id, anchor, plani.limit) |
| 219 | − {{source_id, :aqi}, readings, described(source), places(readings, source, :monitor)} | |
| 269 | + {[{{source_id, :aqi}, readings, described(source)}], places(readings, source, :monitor)} | |
| 220 | 270 | |
| 221 | 271 | other -> |
| 222 | 272 | # Sources with nothing located in them, or nothing near-able yet. Left |
| 242 | 292 | # so a Plani hands them something query-shaped. The empty `query` matters -- |
| 243 | 293 | # the GTFS condenser reads a stop out of it to look up stop-specific alerts, |
| 244 | 294 | # and a Plani has no one stop any more than an area query does. |
| 245 | − defp described(source) do | |
| 246 | − %{id: source.id, name: source.name, meta: %{}, query: %{}} | |
| 295 | + # What the condensers expect alongside the data: a vision hands them a query, | |
| 296 | + # so a Plani hands them something query-shaped. The id has to match the key | |
| 297 | + # the data went under -- that is how the two are paired again -- so a broken | |
| 298 | + # out entry carries its own, or its name is dropped on the way out. | |
| 299 | + defp described(source), do: described(source, source.id, source.name) | |
| 300 | + | |
| 301 | + defp described(source, id, name) do | |
| 302 | + %{id: id, name: name, meta: %{}, query: %{}} | |
| 247 | 303 | end |
| 248 | 304 | |
| 249 | 305 | # Where the things it found actually are, for anything drawing a map. |
modified
apps/room_sanctum/lib/room_sanctum_web/live/cfg_plani_live/form_component.ex
+12
−0
| 123 | 123 | <.input field={@form[:radius]} type="number" label="Radius (metres, 50-3000)" /> |
| 124 | 124 | <.input field={@form[:limit]} type="number" label="How many of each (1-20)" /> |
| 125 | 125 | |
| 126 | + <.input | |
| 127 | + field={@form[:break_out]} | |
| 128 | + type="checkbox" | |
| 129 | + label="One card per stop" | |
| 130 | + /> | |
| 131 | + <p class="text-xs text-base-content/60"> | |
| 132 | + Publishes an entry per stop rather than one per source, which is how a | |
| 133 | + vision looks on the wire — so a client draws a card each and needs to | |
| 134 | + know nothing about grouping. Off, the stops near you arrive blended | |
| 135 | + into one entry per source. | |
| 136 | + </p> | |
| 137 | + | |
| 126 | 138 | <:actions> |
| 127 | 139 | <.button phx-disable-with="Saving...">Save Plani</.button> |
| 128 | 140 | </:actions> |
modified
apps/room_sanctum/lib/room_sanctum_web/live/cfg_plani_live/show.ex
+27
−0
Click to load diff…
modified
apps/room_sanctum/lib/room_sanctum_web/live/cfg_plani_live/show.html.heex
+2
−0
Click to load diff…
added
apps/room_sanctum/priv/repo/migrations/20260903160000_add_break_out_to_plani.exs
+19
−0
Click to load diff…
modified
apps/room_sanctum/test/room_sanctum_web/live/plani_live_test.exs
+19
−0
Click to load diff…
modified
clients/android/app/src/main/java/io/neiam/apolloscrib/types/Wire.kt
+7
−4
Click to load diff…
modified
clients/android/app/src/test/java/io/neiam/apolloscrib/types/WireTest.kt
+36
−0
Click to load diff…
Parents: fa3ea0b