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.
Credo: dispatch tables as data and function heads
f1dab56 · Gabe Morell · 2026-11-04 20:49
Message
{commit_body(@commit)}
Files changed
modified
apps/room_sanctum/lib/room_sanctum/condensers/basic_mqtt.ex
+225
−235
@@ -3,21 +3,20 @@ defmodule RoomSanctum.Condenser.BasicMQTT do
| 3 | 3 | import MapMerge |
| 4 | 4 | |
| 5 | 5 | # GTFS route_type codes by name. PlusMQTT keeps its own copy of the table. |
| 6 | − defp gtfs_mode(route_type) do | |
| 7 | − case route_type do | |
| 8 | − "0" -> "LightRail" | |
| 9 | − "1" -> "Subway" | |
| 10 | − "2" -> "Rail" | |
| 11 | − "3" -> "Bus" | |
| 12 | − "4" -> "Ferry" | |
| 13 | − "5" -> "CableCar" | |
| 14 | − "6" -> "Gondola" | |
| 15 | − "7" -> "Funicular" | |
| 16 | − "11" -> "Trolleybus" | |
| 17 | − "12" -> "Monorail" | |
| 18 | − _ -> "Unknown" | |
| 19 | − end | |
| 20 | − end | |
| 6 | + @gtfs_modes %{ | |
| 7 | + "0" => "LightRail", | |
| 8 | + "1" => "Subway", | |
| 9 | + "2" => "Rail", | |
| 10 | + "3" => "Bus", | |
| 11 | + "4" => "Ferry", | |
| 12 | + "5" => "CableCar", | |
| 13 | + "6" => "Gondola", | |
| 14 | + "7" => "Funicular", | |
| 15 | + "11" => "Trolleybus", | |
| 16 | + "12" => "Monorail" | |
| 17 | + } | |
| 18 | + | |
| 19 | + defp gtfs_mode(route_type), do: Map.get(@gtfs_modes, route_type, "Unknown") | |
| 21 | 20 | |
| 22 | 21 | defp wrap(item) do |
| 23 | 22 | [item] |
@@ -84,240 +83,231 @@ defmodule RoomSanctum.Condenser.BasicMQTT do
| 84 | 83 | # IO.inspect({type, data}) |
| 85 | 84 | # end |
| 86 | 85 | |
| 87 | − case type do | |
| 88 | − :gtfs -> | |
| 89 | − data | |
| 90 | − |> Enum.map(fn f -> | |
| 91 | − %{ | |
| 92 | − time: f.arrival_time, | |
| 93 | − time_live: f.arrival_time_live_ts, | |
| 94 | − destination: f.trip.trip_headsign, | |
| 95 | − direction: f.trip.direction.direction, | |
| 96 | − route: f.trip.route_id, | |
| 97 | − presentation: route_presentation(f.trip.route, f.trip.route_id), | |
| 98 | − mode: f.trip.route.route_type |> gtfs_mode, | |
| 99 | − tz: f.tz, | |
| 100 | − bearing: Map.get(f, :bearing) | |
| 101 | − } | |
| 102 | − end) | |
| 103 | − |> Enum.reduce(%{}, fn %{ | |
| 104 | − time: time, | |
| 105 | − time_live: time_live, | |
| 106 | − destination: dest, | |
| 107 | − direction: dir, | |
| 108 | − route: route, | |
| 109 | − presentation: presentation, | |
| 110 | − mode: mode, | |
| 111 | − tz: tz, | |
| 112 | − bearing: bearing | |
| 113 | − }, | |
| 114 | − acc -> | |
| 115 | − update_in(acc, [{route, dest, dir}], fn | |
| 116 | − nil -> | |
| 117 | − %{ | |
| 118 | − route: route, | |
| 119 | − dest: dest, | |
| 120 | − dir: dir, | |
| 121 | − mode: mode, | |
| 122 | − times: [time], | |
| 123 | − times_live: [livetime(time_live, tz)], | |
| 124 | − bearing: bearing | |
| 125 | − } | |
| 126 | − |> Map.merge(presentation) | |
| 86 | + condense_type(type, data) | |
| 87 | + end | |
| 127 | 88 | |
| 128 | − refs -> | |
| 129 | − %{ | |
| 130 | − refs | |
| 131 | − | times: [time | refs.times], | |
| 132 | − times_live: [livetime(time_live, tz) | refs.times_live], | |
| 133 | − # A blended Plani groups a route across every stop it calls | |
| 134 | − # at inside the radius, so these times can come from two | |
| 135 | − # places at once. Agreeing keeps the bearing; disagreeing | |
| 136 | − # drops it, because "north east" would then be true of only | |
| 137 | − # some of the departures under it. | |
| 138 | − bearing: if(refs.bearing == bearing, do: bearing, else: nil) | |
| 139 | − } | |
| 140 | − end) | |
| 141 | − end) | |
| 142 | − |> Enum.map(fn {_k, v} -> | |
| 143 | − case Enum.any?(v.times_live, fn x -> x != nil end) do | |
| 144 | − true -> | |
| 145 | − v | |
| 146 | − |> Map.put(:times, v.times |> Enum.reverse()) | |
| 147 | − |> Map.put(:times_live, v.times_live |> Enum.reverse()) | |
| 148 | − | |
| 149 | − false -> | |
| 150 | − v |> Map.put(:times, v.times |> Enum.reverse()) |> Map.delete(:times_live) | |
| 151 | − end | |
| 152 | − end) | |
| 153 | − # Only a Plani has a bearing to give, and only for a stop far enough | |
| 154 | − # away to have one. Dropped rather than published as null, so a | |
| 155 | − # vision's payload is byte for byte what it always was. | |
| 156 | − |> Enum.map(fn route -> | |
| 157 | − if route.bearing, do: route, else: Map.delete(route, :bearing) | |
| 158 | − end) | |
| 159 | − | |
| 160 | − :gbfs -> | |
| 161 | − # A bike names its type by an id of the feed's own choosing -- Bay | |
| 162 | − # Wheels' e-bike is "2" -- so the types are resolved once for the list | |
| 163 | − # rather than every bike being labelled with a number. | |
| 164 | − vehicle_types = gbfs_vehicle_types(data) | |
| 165 | − | |
| 166 | − data | |
| 167 | − |> Enum.map(fn | |
| 168 | − # An area query answers with loose bikes rather than a dock, and a | |
| 169 | − # bike has none of a station's fields -- no name, no capacity, | |
| 170 | − # nothing to dock. Told apart by what came back rather than by the | |
| 171 | − # query, since the condenser is only ever handed the answer. | |
| 172 | − %{bike_id: bike_id} = b -> | |
| 173 | − %{ | |
| 174 | − kind: :free_bike, | |
| 175 | − name: vehicle_label(vehicle_types, b) || bike_id, | |
| 176 | − # What kind of thing it is, so a client can draw a car as a car. | |
| 177 | − # The name already says "Car", but a name is read and an icon is | |
| 178 | − # glanced at, and these cards are built to be glanced at. | |
| 179 | − form_factor: vehicle_form_factor(vehicle_types, b), | |
| 180 | − id: bike_id, | |
| 181 | − lat: b.lat, | |
| 182 | − lon: b.lon, | |
| 183 | − range_m: b.current_range_meters, | |
| 184 | − fuel_pct: b.current_fuel_percent, | |
| 185 | − reserved: b.is_reserved, | |
| 186 | − disabled: b.is_disabled | |
| 187 | − } | |
| 188 | − |> with_dir(b) | |
| 189 | − | |
| 190 | − f -> | |
| 191 | − %{ | |
| 192 | − name: f.name, | |
| 193 | − id: f.station_id, | |
| 194 | − avail: f.num_bikes_available, | |
| 195 | − avail_elec: f.num_ebikes_available, | |
| 196 | − # Same reasoning: a feed that does not break its count down by | |
| 197 | − # motor leaves these null, and subtracting one raises. | |
| 198 | − avail_std: (f.num_bikes_available || 0) - (f.num_ebikes_available || 0), | |
| 199 | − docks_avail: f.num_docks_available, | |
| 200 | − docks_disabled: f.num_docks_disabled, | |
| 201 | − capacity: f.capacity, | |
| 202 | − # A left join, so a dock that no e-bike is sitting in comes | |
| 203 | − # back nil rather than empty -- and mapping over that raised, | |
| 204 | − # which killed the condense for the *whole* board rather than | |
| 205 | − # this one dock. A Plani feels it hardest: it breaks a source | |
| 206 | − # out into an entry per dock, so a radius holding one ordinary | |
| 207 | − # unelectrified dock published nothing at all. | |
| 208 | − ebikes_info: | |
| 209 | − (f.ebikes_info || []) | |
| 210 | − |> Enum.map(fn eb -> | |
| 211 | − %{ | |
| 212 | − name: eb.displayed_number, | |
| 213 | − battery_pct: eb.battery_charge_percentage, | |
| 214 | − range_mi_cons: eb.range_estimate.conservative_range_miles, | |
| 215 | − range_me_est: eb.range_estimate.estimated_range_miles | |
| 216 | − } | |
| 217 | − end) | |
| 218 | − } | |
| 219 | − |> with_dir(f) | |
| 220 | − end) | |
| 221 | − | |
| 222 | − :tidal -> | |
| 223 | − data | |
| 224 | − |> Enum.group_by(fn x -> x.type end) | |
| 225 | − |> Enum.map(fn {extreme, data} -> | |
| 226 | − case data do | |
| 227 | − [first, second] -> | |
| 228 | − k1 = "first_#{extreme |> String.downcase()}" |> String.to_atom() | |
| 229 | − k2 = "second_#{extreme |> String.downcase()}" |> String.to_atom() | |
| 230 | − kv1 = "first_#{extreme |> String.downcase()}v" |> String.to_atom() | |
| 231 | − kv2 = "second_#{extreme |> String.downcase()}v" |> String.to_atom() | |
| 232 | − %{k1 => first.t |> time, k2 => second.t |> time, kv1 => first.v, kv2 => second.v} | |
| 233 | − | |
| 234 | − [solo] -> | |
| 235 | − k1 = "first_#{extreme |> String.downcase()}" |> String.to_atom() | |
| 236 | − kv1 = "first_#{extreme |> String.downcase()}v" |> String.to_atom() | |
| 237 | − %{k1 => solo.t |> time, kv1 => solo.v} | |
| 238 | − end | |
| 239 | − end) | |
| 240 | − |> Enum.reduce(&Map.merge/2) | |
| 241 | − |> wrap | |
| 242 | − | |
| 243 | − :weather -> | |
| 244 | − data | |
| 245 | − |> Enum.map(fn f -> | |
| 89 | + # Published as they come. | |
| 90 | + @passthrough [ | |
| 91 | + :cronos, | |
| 92 | + :gitlab, | |
| 93 | + :github, | |
| 94 | + :drought, | |
| 95 | + :pollen, | |
| 96 | + :icarus, | |
| 97 | + :mailbox, | |
| 98 | + :treasury, | |
| 99 | + :bourse, | |
| 100 | + :packages, | |
| 101 | + :const | |
| 102 | + ] | |
| 103 | + | |
| 104 | + defp condense_type(:gtfs, data) do | |
| 105 | + data | |
| 106 | + |> Enum.map(fn f -> | |
| 107 | + %{ | |
| 108 | + time: f.arrival_time, | |
| 109 | + time_live: f.arrival_time_live_ts, | |
| 110 | + destination: f.trip.trip_headsign, | |
| 111 | + direction: f.trip.direction.direction, | |
| 112 | + route: f.trip.route_id, | |
| 113 | + presentation: route_presentation(f.trip.route, f.trip.route_id), | |
| 114 | + mode: f.trip.route.route_type |> gtfs_mode, | |
| 115 | + tz: f.tz, | |
| 116 | + bearing: Map.get(f, :bearing) | |
| 117 | + } | |
| 118 | + end) | |
| 119 | + |> Enum.reduce(%{}, fn %{ | |
| 120 | + time: time, | |
| 121 | + time_live: time_live, | |
| 122 | + destination: dest, | |
| 123 | + direction: dir, | |
| 124 | + route: route, | |
| 125 | + presentation: presentation, | |
| 126 | + mode: mode, | |
| 127 | + tz: tz, | |
| 128 | + bearing: bearing | |
| 129 | + }, | |
| 130 | + acc -> | |
| 131 | + update_in(acc, [{route, dest, dir}], fn | |
| 132 | + nil -> | |
| 246 | 133 | %{ |
| 247 | − name: f.name, | |
| 248 | − weather: List.first(f.weather).main, | |
| 249 | − temp: f.main.temp, | |
| 250 | − feel: f.main.feels_like, | |
| 251 | − hum: f.main.humidity, | |
| 252 | − pressure: f.main.pressure, | |
| 253 | − wind: f.wind, | |
| 254 | − visibility: f |> Map.get(:visibility), | |
| 255 | − units: f.units | |
| 134 | + route: route, | |
| 135 | + dest: dest, | |
| 136 | + dir: dir, | |
| 137 | + mode: mode, | |
| 138 | + times: [time], | |
| 139 | + times_live: [livetime(time_live, tz)], | |
| 140 | + bearing: bearing | |
| 256 | 141 | } |
| 257 | − end) | |
| 258 | − | |
| 259 | − :aqi -> | |
| 260 | − data | |
| 261 | − |> Enum.map(fn f -> | |
| 262 | − pairs = HourlyObsData.compile_pairs(f) | |
| 263 | − pairs ||| %{name: f.reporting_areas |> List.first()} | |
| 264 | − end) | |
| 265 | − | |
| 266 | − :ephem -> | |
| 267 | − data | |
| 268 | − |> Enum.map(fn f -> | |
| 269 | − case Map.get(f, :period) do | |
| 270 | − nil -> {:name, f.name} | |
| 271 | − _val -> {f.period, f.result} | |
| 272 | − end | |
| 273 | − end) | |
| 274 | − |> Enum.into(%{}) | |
| 275 | − |> wrap | |
| 276 | − | |
| 277 | − :calendar -> | |
| 278 | − data | |
| 279 | − |> Enum.map(fn f -> | |
| 142 | + |> Map.merge(presentation) | |
| 143 | + | |
| 144 | + refs -> | |
| 280 | 145 | %{ |
| 281 | − date_start: f.date_start |> DateTime.to_date(), | |
| 282 | − description: f.blob["description"] | |
| 146 | + refs | |
| 147 | + | times: [time | refs.times], | |
| 148 | + times_live: [livetime(time_live, tz) | refs.times_live], | |
| 149 | + # A blended Plani groups a route across every stop it calls | |
| 150 | + # at inside the radius, so these times can come from two | |
| 151 | + # places at once. Agreeing keeps the bearing; disagreeing | |
| 152 | + # drops it, because "north east" would then be true of only | |
| 153 | + # some of the departures under it. | |
| 154 | + bearing: if(refs.bearing == bearing, do: bearing, else: nil) | |
| 283 | 155 | } |
| 284 | − end) | |
| 285 | − | |
| 286 | − :cronos -> | |
| 287 | − data | |
| 288 | − | |
| 289 | − :gitlab -> | |
| 290 | − data | |
| 291 | − | |
| 292 | − :github -> | |
| 293 | − data | |
| 294 | − | |
| 295 | − :drought -> | |
| 296 | − data | |
| 156 | + end) | |
| 157 | + end) | |
| 158 | + |> Enum.map(fn {_k, v} -> | |
| 159 | + case Enum.any?(v.times_live, fn x -> x != nil end) do | |
| 160 | + true -> | |
| 161 | + v | |
| 162 | + |> Map.put(:times, v.times |> Enum.reverse()) | |
| 163 | + |> Map.put(:times_live, v.times_live |> Enum.reverse()) | |
| 164 | + | |
| 165 | + false -> | |
| 166 | + v |> Map.put(:times, v.times |> Enum.reverse()) |> Map.delete(:times_live) | |
| 167 | + end | |
| 168 | + end) | |
| 169 | + # Only a Plani has a bearing to give, and only for a stop far enough | |
| 170 | + # away to have one. Dropped rather than published as null, so a | |
| 171 | + # vision's payload is byte for byte what it always was. | |
| 172 | + |> Enum.map(fn route -> | |
| 173 | + if route.bearing, do: route, else: Map.delete(route, :bearing) | |
| 174 | + end) | |
| 175 | + end | |
| 297 | 176 | |
| 298 | − :pollen -> | |
| 299 | − data | |
| 177 | + defp condense_type(:gbfs, data) do | |
| 178 | + # A bike names its type by an id of the feed's own choosing -- Bay | |
| 179 | + # Wheels' e-bike is "2" -- so the types are resolved once for the list | |
| 180 | + # rather than every bike being labelled with a number. | |
| 181 | + vehicle_types = gbfs_vehicle_types(data) | |
| 300 | 182 | |
| 301 | − :icarus -> | |
| 302 | − data | |
| 183 | + data | |
| 184 | + |> Enum.map(fn | |
| 185 | + # An area query answers with loose bikes rather than a dock, and a | |
| 186 | + # bike has none of a station's fields -- no name, no capacity, | |
| 187 | + # nothing to dock. Told apart by what came back rather than by the | |
| 188 | + # query, since the condenser is only ever handed the answer. | |
| 189 | + %{bike_id: bike_id} = b -> | |
| 190 | + %{ | |
| 191 | + kind: :free_bike, | |
| 192 | + name: vehicle_label(vehicle_types, b) || bike_id, | |
| 193 | + # What kind of thing it is, so a client can draw a car as a car. | |
| 194 | + # The name already says "Car", but a name is read and an icon is | |
| 195 | + # glanced at, and these cards are built to be glanced at. | |
| 196 | + form_factor: vehicle_form_factor(vehicle_types, b), | |
| 197 | + id: bike_id, | |
| 198 | + lat: b.lat, | |
| 199 | + lon: b.lon, | |
| 200 | + range_m: b.current_range_meters, | |
| 201 | + fuel_pct: b.current_fuel_percent, | |
| 202 | + reserved: b.is_reserved, | |
| 203 | + disabled: b.is_disabled | |
| 204 | + } | |
| 205 | + |> with_dir(b) | |
| 206 | + | |
| 207 | + f -> | |
| 208 | + %{ | |
| 209 | + name: f.name, | |
| 210 | + id: f.station_id, | |
| 211 | + avail: f.num_bikes_available, | |
| 212 | + avail_elec: f.num_ebikes_available, | |
| 213 | + # Same reasoning: a feed that does not break its count down by | |
| 214 | + # motor leaves these null, and subtracting one raises. | |
| 215 | + avail_std: (f.num_bikes_available || 0) - (f.num_ebikes_available || 0), | |
| 216 | + docks_avail: f.num_docks_available, | |
| 217 | + docks_disabled: f.num_docks_disabled, | |
| 218 | + capacity: f.capacity, | |
| 219 | + # A left join, so a dock that no e-bike is sitting in comes | |
| 220 | + # back nil rather than empty -- and mapping over that raised, | |
| 221 | + # which killed the condense for the *whole* board rather than | |
| 222 | + # this one dock. A Plani feels it hardest: it breaks a source | |
| 223 | + # out into an entry per dock, so a radius holding one ordinary | |
| 224 | + # unelectrified dock published nothing at all. | |
| 225 | + ebikes_info: | |
| 226 | + (f.ebikes_info || []) | |
| 227 | + |> Enum.map(fn eb -> | |
| 228 | + %{ | |
| 229 | + name: eb.displayed_number, | |
| 230 | + battery_pct: eb.battery_charge_percentage, | |
| 231 | + range_mi_cons: eb.range_estimate.conservative_range_miles, | |
| 232 | + range_me_est: eb.range_estimate.estimated_range_miles | |
| 233 | + } | |
| 234 | + end) | |
| 235 | + } | |
| 236 | + |> with_dir(f) | |
| 237 | + end) | |
| 238 | + end | |
| 303 | 239 | |
| 304 | − :mailbox -> | |
| 305 | − data | |
| 240 | + defp condense_type(:tidal, data) do | |
| 241 | + data | |
| 242 | + |> Enum.group_by(fn x -> x.type end) | |
| 243 | + |> Enum.map(fn {extreme, data} -> | |
| 244 | + case data do | |
| 245 | + [first, second] -> | |
| 246 | + k1 = "first_#{extreme |> String.downcase()}" |> String.to_atom() | |
| 247 | + k2 = "second_#{extreme |> String.downcase()}" |> String.to_atom() | |
| 248 | + kv1 = "first_#{extreme |> String.downcase()}v" |> String.to_atom() | |
| 249 | + kv2 = "second_#{extreme |> String.downcase()}v" |> String.to_atom() | |
| 250 | + %{k1 => first.t |> time, k2 => second.t |> time, kv1 => first.v, kv2 => second.v} | |
| 251 | + | |
| 252 | + [solo] -> | |
| 253 | + k1 = "first_#{extreme |> String.downcase()}" |> String.to_atom() | |
| 254 | + kv1 = "first_#{extreme |> String.downcase()}v" |> String.to_atom() | |
| 255 | + %{k1 => solo.t |> time, kv1 => solo.v} | |
| 256 | + end | |
| 257 | + end) | |
| 258 | + |> Enum.reduce(&Map.merge/2) | |
| 259 | + |> wrap | |
| 260 | + end | |
| 306 | 261 | |
| 307 | − :treasury -> | |
| 308 | − data | |
| 262 | + defp condense_type(:weather, data) do | |
| 263 | + data | |
| 264 | + |> Enum.map(fn f -> | |
| 265 | + %{ | |
| 266 | + name: f.name, | |
| 267 | + weather: List.first(f.weather).main, | |
| 268 | + temp: f.main.temp, | |
| 269 | + feel: f.main.feels_like, | |
| 270 | + hum: f.main.humidity, | |
| 271 | + pressure: f.main.pressure, | |
| 272 | + wind: f.wind, | |
| 273 | + visibility: f |> Map.get(:visibility), | |
| 274 | + units: f.units | |
| 275 | + } | |
| 276 | + end) | |
| 277 | + end | |
| 309 | 278 | |
| 310 | − :bourse -> | |
| 311 | − data | |
| 279 | + defp condense_type(:aqi, data) do | |
| 280 | + data | |
| 281 | + |> Enum.map(fn f -> | |
| 282 | + pairs = HourlyObsData.compile_pairs(f) | |
| 283 | + pairs ||| %{name: f.reporting_areas |> List.first()} | |
| 284 | + end) | |
| 285 | + end | |
| 312 | 286 | |
| 313 | − :packages -> | |
| 314 | − data | |
| 287 | + defp condense_type(:ephem, data) do | |
| 288 | + data | |
| 289 | + |> Enum.map(fn f -> | |
| 290 | + case Map.get(f, :period) do | |
| 291 | + nil -> {:name, f.name} | |
| 292 | + _val -> {f.period, f.result} | |
| 293 | + end | |
| 294 | + end) | |
| 295 | + |> Enum.into(%{}) | |
| 296 | + |> wrap | |
| 297 | + end | |
| 315 | 298 | |
| 316 | − :const -> | |
| 317 | − data | |
| 318 | − end | |
| 299 | + defp condense_type(:calendar, data) do | |
| 300 | + data | |
| 301 | + |> Enum.map(fn f -> | |
| 302 | + %{ | |
| 303 | + date_start: f.date_start |> DateTime.to_date(), | |
| 304 | + description: f.blob["description"] | |
| 305 | + } | |
| 306 | + end) | |
| 319 | 307 | end |
| 320 | 308 | |
| 309 | + defp condense_type(type, data) when type in @passthrough, do: data | |
| 310 | + | |
| 321 | 311 | @doc """ |
| 322 | 312 | Condenses data and wraps it with query information |
| 323 | 313 | """ |
modified
apps/room_sanctum/lib/room_sanctum/condensers/plus_mqtt.ex
+14
−15
@@ -146,21 +146,20 @@ defmodule RoomSanctum.Condenser.PlusMQTT do
| 146 | 146 | |
| 147 | 147 | # Same table as Basic's, kept here rather than reaching into |
| 148 | 148 | # a private function of another condenser. |
| 149 | − defp gtfs_mode(route_type) do | |
| 150 | − case route_type do | |
| 151 | − "0" -> "LightRail" | |
| 152 | − "1" -> "Subway" | |
| 153 | − "2" -> "Rail" | |
| 154 | − "3" -> "Bus" | |
| 155 | − "4" -> "Ferry" | |
| 156 | − "5" -> "CableCar" | |
| 157 | − "6" -> "Gondola" | |
| 158 | − "7" -> "Funicular" | |
| 159 | − "11" -> "Trolleybus" | |
| 160 | − "12" -> "Monorail" | |
| 161 | − _ -> "Unknown" | |
| 162 | − end | |
| 163 | − end | |
| 149 | + @gtfs_modes %{ | |
| 150 | + "0" => "LightRail", | |
| 151 | + "1" => "Subway", | |
| 152 | + "2" => "Rail", | |
| 153 | + "3" => "Bus", | |
| 154 | + "4" => "Ferry", | |
| 155 | + "5" => "CableCar", | |
| 156 | + "6" => "Gondola", | |
| 157 | + "7" => "Funicular", | |
| 158 | + "11" => "Trolleybus", | |
| 159 | + "12" => "Monorail" | |
| 160 | + } | |
| 161 | + | |
| 162 | + defp gtfs_mode(route_type), do: Map.get(@gtfs_modes, route_type, "Unknown") | |
| 164 | 163 | |
| 165 | 164 | defp livetime(nil, _tz), do: nil |
| 166 | 165 |
modified
apps/room_sanctum/lib/room_sanctum/storage.ex
+56
−44
@@ -610,20 +610,22 @@ defmodule RoomSanctum.Storage do
| 610 | 610 | # spec's list is left alone rather than guessed at. |
| 611 | 611 | defp mode_label(nil), do: nil |
| 612 | 612 | |
| 613 | + @mode_labels %{ | |
| 614 | + "0" => "Tram", | |
| 615 | + "1" => "Subway", | |
| 616 | + "2" => "Rail", | |
| 617 | + "3" => "Bus", | |
| 618 | + "4" => "Ferry", | |
| 619 | + "5" => "Cable tram", | |
| 620 | + "6" => "Aerial lift", | |
| 621 | + "7" => "Funicular", | |
| 622 | + "11" => "Trolleybus", | |
| 623 | + "12" => "Monorail" | |
| 624 | + } | |
| 625 | + | |
| 613 | 626 | defp mode_label(route_type) do |
| 614 | − case to_string(route_type) do | |
| 615 | − "0" -> "Tram" | |
| 616 | − "1" -> "Subway" | |
| 617 | − "2" -> "Rail" | |
| 618 | − "3" -> "Bus" | |
| 619 | − "4" -> "Ferry" | |
| 620 | − "5" -> "Cable tram" | |
| 621 | − "6" -> "Aerial lift" | |
| 622 | − "7" -> "Funicular" | |
| 623 | − "11" -> "Trolleybus" | |
| 624 | − "12" -> "Monorail" | |
| 625 | − other -> other | |
| 626 | − end | |
| 627 | + route_type = to_string(route_type) | |
| 628 | + Map.get(@mode_labels, route_type, route_type) | |
| 627 | 629 | end |
| 628 | 630 | |
| 629 | 631 | @doc """ |
@@ -1458,38 +1460,48 @@ defmodule RoomSanctum.Storage do
| 1458 | 1460 | ) |
| 1459 | 1461 | end |
| 1460 | 1462 | |
| 1461 | − from([st, trip: t] in queryable, | |
| 1462 | − where: | |
| 1463 | − (exists( | |
| 1464 | − from(c in scheduled, where: c.service_id == parent_as(:trip).service_id, select: 1) | |
| 1465 | − ) or | |
| 1466 | − exists( | |
| 1467 | − from(cd in exception.(1), | |
| 1468 | − where: cd.service_id == parent_as(:trip).service_id, | |
| 1469 | − select: 1 | |
| 1470 | − ) | |
| 1471 | − ) or | |
| 1472 | − (not exists( | |
| 1473 | − from(c in "gtfs_calendars", | |
| 1474 | − where: c.source_id == ^source_id and c.service_id == parent_as(:trip).service_id, | |
| 1475 | − select: 1 | |
| 1476 | − ) | |
| 1477 | − ) and | |
| 1478 | − not exists( | |
| 1479 | − from(cd in "gtfs_calendar_dates", | |
| 1480 | − where: | |
| 1481 | − cd.source_id == ^source_id and cd.service_id == parent_as(:trip).service_id, | |
| 1482 | − select: 1 | |
| 1483 | − ) | |
| 1484 | − )) or | |
| 1485 | − (not exists(from(c in scheduled, select: 1)) and | |
| 1486 | − not exists(from(cd in exception.(1), select: 1)))) and | |
| 1487 | − not exists( | |
| 1488 | − from(cd in exception.(2), | |
| 1489 | − where: cd.service_id == parent_as(:trip).service_id, | |
| 1490 | − select: 1 | |
| 1491 | − ) | |
| 1463 | + listed = dynamic([], ^service_in(scheduled) or ^service_in(exception.(1))) | |
| 1464 | + unmentioned = service_unmentioned(source_id) | |
| 1465 | + stale = nothing_running(scheduled, exception.(1)) | |
| 1466 | + removed = service_in(exception.(2)) | |
| 1467 | + | |
| 1468 | + where(queryable, ^dynamic([], (^listed or ^unmentioned or ^stale) and not (^removed))) | |
| 1469 | + end | |
| 1470 | + | |
| 1471 | + # The trip's service is one of the rows of this calendar query. | |
| 1472 | + defp service_in(calendar_query) do | |
| 1473 | + dynamic( | |
| 1474 | + [], | |
| 1475 | + exists( | |
| 1476 | + from(c in calendar_query, where: c.service_id == parent_as(:trip).service_id, select: 1) | |
| 1477 | + ) | |
| 1478 | + ) | |
| 1479 | + end | |
| 1480 | + | |
| 1481 | + # Neither file says anything about the trip's service. | |
| 1482 | + defp service_unmentioned(source_id) do | |
| 1483 | + dynamic( | |
| 1484 | + [], | |
| 1485 | + not exists( | |
| 1486 | + from(c in "gtfs_calendars", | |
| 1487 | + where: c.source_id == ^source_id and c.service_id == parent_as(:trip).service_id, | |
| 1488 | + select: 1 | |
| 1489 | + ) | |
| 1490 | + ) and | |
| 1491 | + not exists( | |
| 1492 | + from(cd in "gtfs_calendar_dates", | |
| 1493 | + where: cd.source_id == ^source_id and cd.service_id == parent_as(:trip).service_id, | |
| 1494 | + select: 1 | |
| 1492 | 1495 | ) |
| 1496 | + ) | |
| 1497 | + ) | |
| 1498 | + end | |
| 1499 | + | |
| 1500 | + # Nothing at all runs today: the source's service data has gone stale. | |
| 1501 | + defp nothing_running(scheduled, added) do | |
| 1502 | + dynamic( | |
| 1503 | + [], | |
| 1504 | + not exists(from(c in scheduled, select: 1)) and not exists(from(cd in added, select: 1)) | |
| 1493 | 1505 | ) |
| 1494 | 1506 | end |
| 1495 | 1507 |
modified
apps/room_sanctum/lib/room_sanctum/storage/gbfs/v1/vehicle_types.ex
+20
−19
@@ -20,28 +20,29 @@ defmodule RoomSanctum.Storage.GBFS.V1.VehicleTypes do
| 20 | 20 | about a type is its form factor and how it is propelled, which is exactly the |
| 21 | 21 | distinction a rider cares about. |
| 22 | 22 | """ |
| 23 | − def label(%{form_factor: form_factor, propulsion_type: propulsion}) do | |
| 24 | − case {form_factor, propulsion} do | |
| 25 | − {nil, _} -> nil | |
| 26 | − # The combinations a rider has a word for. GBFS spells out the machine | |
| 27 | − # ("bicycle", "electric_assist"); people say e-bike. | |
| 28 | − {"bicycle", "human"} -> "Bike" | |
| 29 | − {"bicycle", _electric} -> "E-bike" | |
| 30 | − {"cargo_bicycle", "human"} -> "Cargo bike" | |
| 31 | − {"cargo_bicycle", _electric} -> "Electric cargo bike" | |
| 32 | − {"scooter" <> _, _} -> "Scooter" | |
| 33 | − {"moped", _} -> "Moped" | |
| 34 | − {"car", _} -> "Car" | |
| 35 | − # Anything the spec adds later reads as well as its own words allow, | |
| 36 | − # rather than not at all. | |
| 37 | − {factor, nil} -> humanise(factor) | |
| 38 | − {factor, "human"} -> humanise(factor) | |
| 39 | − {factor, propulsion} -> "#{humanise(propulsion)} #{String.downcase(humanise(factor))}" | |
| 40 | − end | |
| 41 | − end | |
| 23 | + def label(%{form_factor: form_factor, propulsion_type: propulsion}), | |
| 24 | + do: name_for(form_factor, propulsion) | |
| 42 | 25 | |
| 43 | 26 | def label(_type), do: nil |
| 44 | 27 | |
| 28 | + defp name_for(nil, _propulsion), do: nil | |
| 29 | + # The combinations a rider has a word for. GBFS spells out the machine | |
| 30 | + # ("bicycle", "electric_assist"); people say e-bike. | |
| 31 | + defp name_for("bicycle", "human"), do: "Bike" | |
| 32 | + defp name_for("bicycle", _electric), do: "E-bike" | |
| 33 | + defp name_for("cargo_bicycle", "human"), do: "Cargo bike" | |
| 34 | + defp name_for("cargo_bicycle", _electric), do: "Electric cargo bike" | |
| 35 | + defp name_for("scooter" <> _, _propulsion), do: "Scooter" | |
| 36 | + defp name_for("moped", _propulsion), do: "Moped" | |
| 37 | + defp name_for("car", _propulsion), do: "Car" | |
| 38 | + # Anything the spec adds later reads as well as its own words allow, | |
| 39 | + # rather than not at all. | |
| 40 | + defp name_for(factor, nil), do: humanise(factor) | |
| 41 | + defp name_for(factor, "human"), do: humanise(factor) | |
| 42 | + | |
| 43 | + defp name_for(factor, propulsion), | |
| 44 | + do: "#{humanise(propulsion)} #{String.downcase(humanise(factor))}" | |
| 45 | + | |
| 45 | 46 | # "electric_assist" -> "Electric assist", "bicycle" -> "Bicycle". |
| 46 | 47 | defp humanise(value) do |
| 47 | 48 | value |
modified
apps/room_sanctum/lib/room_sanctum/workers/keryx_item_cache.ex
+48
−75
@@ -98,88 +98,61 @@ defmodule RoomSanctum.Worker.KeryxItemCache do
| 98 | 98 | end |
| 99 | 99 | end |
| 100 | 100 | |
| 101 | − # Extract item ID (duplicated from keryx_worker for independence) | |
| 102 | − defp extract_item_id(item, type) when is_map(item) do | |
| 103 | − id_value = | |
| 104 | − case type do | |
| 105 | − :gtfs -> | |
| 106 | − Map.get(item, :trip_id) || Map.get(item, "trip_id") | |
| 107 | − | |
| 108 | − :gbfs -> | |
| 109 | − # Use the name field for GBFS instead of station_id | |
| 110 | − Map.get(item, :name) || Map.get(item, "name") || Map.get(item, :station_id) || | |
| 111 | − Map.get(item, "station_id") | |
| 112 | − | |
| 113 | − :weather -> | |
| 114 | − Map.get(item, :time) || Map.get(item, "time") || Map.get(item, :dt) || | |
| 115 | − Map.get(item, "dt") | |
| 116 | − | |
| 117 | − :tidal -> | |
| 118 | − Map.get(item, :t) || Map.get(item, "t") || Map.get(item, :time) || Map.get(item, "time") | |
| 119 | − | |
| 120 | − :aqi -> | |
| 121 | − Map.get(item, :parameter) || Map.get(item, "parameter") | |
| 122 | − | |
| 123 | − :calendar -> | |
| 124 | − Map.get(item, :uid) || Map.get(item, "uid") || Map.get(item, :id) || Map.get(item, "id") | |
| 125 | − | |
| 126 | − :ephem -> | |
| 127 | − Map.get(item, :name) || Map.get(item, "name") | |
| 128 | − | |
| 129 | − :cronos -> | |
| 130 | − Map.get(item, :id) || Map.get(item, "id") || Map.get(item, :name) || | |
| 131 | − Map.get(item, "name") | |
| 132 | − | |
| 133 | − :gitlab -> | |
| 134 | − Map.get(item, :id) || Map.get(item, "id") | |
| 135 | − | |
| 136 | − :github -> | |
| 137 | − Map.get(item, :id) || Map.get(item, "id") | |
| 138 | − | |
| 139 | − :drought -> | |
| 140 | − Map.get(item, "fips") || Map.get(item, "state") || Map.get(item, "mapDate") || | |
| 141 | − Map.get(item, :mapDate) | |
| 142 | − | |
| 143 | − :pollen -> | |
| 144 | − case Map.get(item, "date") || Map.get(item, :date) do | |
| 145 | − %{"year" => y, "month" => m, "day" => d} -> "#{y}-#{m}-#{d}" | |
| 146 | − %{year: y, month: m, day: d} -> "#{y}-#{m}-#{d}" | |
| 147 | − other -> inspect(other) | |
| 148 | − end | |
| 101 | + # Where each type keeps the field an item's id is read from, in order of | |
| 102 | + # preference. A type not listed falls back to @default_id_keys. | |
| 103 | + @id_keys %{ | |
| 104 | + gtfs: [:trip_id, "trip_id"], | |
| 105 | + # Use the name field for GBFS instead of station_id | |
| 106 | + gbfs: [:name, "name", :station_id, "station_id"], | |
| 107 | + weather: [:time, "time", :dt, "dt"], | |
| 108 | + tidal: [:t, "t", :time, "time"], | |
| 109 | + aqi: [:parameter, "parameter"], | |
| 110 | + calendar: [:uid, "uid", :id, "id"], | |
| 111 | + ephem: [:name, "name"], | |
| 112 | + cronos: [:id, "id", :name, "name"], | |
| 113 | + gitlab: [:id, "id"], | |
| 114 | + github: [:id, "id"], | |
| 115 | + drought: ["fips", "state", "mapDate", :mapDate], | |
| 116 | + bourse: [:symbol, "symbol"], | |
| 117 | + treasury: [:pair, "pair"], | |
| 118 | + mailbox: [:uid, "uid"], | |
| 119 | + packages: [:tracking_number, "tracking_number", :id, "id"] | |
| 120 | + } | |
| 121 | + | |
| 122 | + @default_id_keys [:id, "id", :uid, "uid"] | |
| 149 | 123 | |
| 150 | − :bourse -> | |
| 151 | − Map.get(item, :symbol) || Map.get(item, "symbol") | |
| 152 | − | |
| 153 | − :treasury -> | |
| 154 | − Map.get(item, :pair) || Map.get(item, "pair") | |
| 155 | − | |
| 156 | − :mailbox -> | |
| 157 | − Map.get(item, :uid) || Map.get(item, "uid") | |
| 158 | − | |
| 159 | − :icarus -> | |
| 160 | − # Area mode keys on the airframe; a flight watch keys on the instance, | |
| 161 | − # so a notification does not re-fire when the hex is latched or the | |
| 162 | − # aircraft briefly drops out of coverage. | |
| 163 | − case Map.get(item, "kind") do | |
| 164 | − "flight" -> | |
| 165 | − "#{Map.get(item, "callsign")}/#{Map.get(item, "sched_arrival")}" | |
| 124 | + # Extract item ID (duplicated from keryx_worker for independence) | |
| 125 | + defp extract_item_id(item, type) when is_map(item), do: stubbify(item_id_value(item, type)) | |
| 166 | 126 | |
| 167 | − _ -> | |
| 168 | − Map.get(item, "hex") || Map.get(item, :hex) | |
| 169 | − end | |
| 127 | + defp extract_item_id(_item, _type), do: generate_stub() | |
| 170 | 128 | |
| 171 | − :packages -> | |
| 172 | − Map.get(item, :tracking_number) || Map.get(item, "tracking_number") || | |
| 173 | − Map.get(item, :id) || Map.get(item, "id") | |
| 129 | + defp item_id_value(item, :pollen) do | |
| 130 | + case Map.get(item, "date") || Map.get(item, :date) do | |
| 131 | + %{"year" => y, "month" => m, "day" => d} -> "#{y}-#{m}-#{d}" | |
| 132 | + %{year: y, month: m, day: d} -> "#{y}-#{m}-#{d}" | |
| 133 | + other -> inspect(other) | |
| 134 | + end | |
| 135 | + end | |
| 174 | 136 | |
| 175 | − _ -> | |
| 176 | − Map.get(item, :id) || Map.get(item, "id") || Map.get(item, :uid) || Map.get(item, "uid") | |
| 177 | − end | |
| 137 | + # Area mode keys on the airframe; a flight watch keys on the instance, | |
| 138 | + # so a notification does not re-fire when the hex is latched or the | |
| 139 | + # aircraft briefly drops out of coverage. | |
| 140 | + defp item_id_value(item, :icarus) do | |
| 141 | + case Map.get(item, "kind") do | |
| 142 | + "flight" -> "#{Map.get(item, "callsign")}/#{Map.get(item, "sched_arrival")}" | |
| 143 | + _ -> Map.get(item, "hex") || Map.get(item, :hex) | |
| 144 | + end | |
| 145 | + end | |
| 178 | 146 | |
| 179 | − stubbify(id_value) | |
| 147 | + defp item_id_value(item, type) do | |
| 148 | + first_value(item, Map.get(@id_keys, type, @default_id_keys)) | |
| 180 | 149 | end |
| 181 | 150 | |
| 182 | − defp extract_item_id(_item, _type), do: generate_stub() | |
| 151 | + # The first of the keys that holds a value wins -- an `a || b || ...` chain | |
| 152 | + # over them. | |
| 153 | + defp first_value(item, keys) do | |
| 154 | + Enum.reduce(keys, nil, fn key, found -> found || Map.get(item, key) end) | |
| 155 | + end | |
| 183 | 156 | |
| 184 | 157 | defp stubbify(nil), do: generate_stub() |
| 185 | 158 |
modified
apps/room_sanctum/lib/room_sanctum/workers/keryx_worker.ex
+82
−131
@@ -201,110 +201,79 @@ defmodule RoomSanctum.Worker.Keryx do
| 201 | 201 | end |
| 202 | 202 | |
| 203 | 203 | # Execute a single query |
| 204 | − defp execute_query(q) do | |
| 205 | − case q.source.type do | |
| 206 | − :gtfs -> | |
| 207 | − RoomGtfs.Worker.query_stop(q.source.id, q.query) | |
| 204 | + # Workers that may not be running: one that is not has nothing to read. | |
| 205 | + @readers %{ | |
| 206 | + drought: RoomDrought.Worker, | |
| 207 | + pollen: RoomPollen.Worker, | |
| 208 | + icarus: RoomIcarus.Worker, | |
| 209 | + mailbox: RoomHermes.Mail.ImapWorker, | |
| 210 | + treasury: RoomTreasury.Worker, | |
| 211 | + bourse: RoomBourse.Worker | |
| 212 | + } | |
| 208 | 213 | |
| 209 | − :gbfs -> | |
| 210 | − RoomGbfs.Worker.query_stop(q.source.id, q.query) | |
| 214 | + defp execute_query(%{source: %{type: :gtfs}} = q), | |
| 215 | + do: RoomGtfs.Worker.query_stop(q.source.id, q.query) | |
| 211 | 216 | |
| 212 | − :tidal -> | |
| 213 | − RoomTidal.Worker.query_tides(q.source.id, q.query) | |
| 217 | + defp execute_query(%{source: %{type: :gbfs}} = q), | |
| 218 | + do: RoomGbfs.Worker.query_stop(q.source.id, q.query) | |
| 214 | 219 | |
| 215 | − :weather -> | |
| 216 | − RoomWeather.Worker.query_weather( | |
| 217 | − q.source.id, | |
| 218 | − q.query | |
| 219 | − ) | |
| 220 | + defp execute_query(%{source: %{type: :tidal}} = q), | |
| 221 | + do: RoomTidal.Worker.query_tides(q.source.id, q.query) | |
| 220 | 222 | |
| 221 | − :aqi -> | |
| 222 | − RoomAirQuality.Worker.query_place( | |
| 223 | − q.source.id, | |
| 224 | − q.query | |
| 225 | − ) | |
| 223 | + defp execute_query(%{source: %{type: :weather}} = q), | |
| 224 | + do: RoomWeather.Worker.query_weather(q.source.id, q.query) | |
| 226 | 225 | |
| 227 | − :ephem -> | |
| 228 | − RoomEphem.Worker.query_ephem( | |
| 229 | − q.source.id, | |
| 230 | − q.query | |
| 231 | − ) | |
| 226 | + defp execute_query(%{source: %{type: :aqi}} = q), | |
| 227 | + do: RoomAirQuality.Worker.query_place(q.source.id, q.query) | |
| 232 | 228 | |
| 233 | − :calendar -> | |
| 234 | − RoomCalendar.Worker.query_calendar( | |
| 235 | − q.source.id, | |
| 236 | − q.query | |
| 237 | − ) | |
| 229 | + defp execute_query(%{source: %{type: :ephem}} = q), | |
| 230 | + do: RoomEphem.Worker.query_ephem(q.source.id, q.query) | |
| 238 | 231 | |
| 239 | − :cronos -> | |
| 240 | − RoomCronos.Worker.query_cronos(q.id, q.query) | |
| 232 | + defp execute_query(%{source: %{type: :calendar}} = q), | |
| 233 | + do: RoomCalendar.Worker.query_calendar(q.source.id, q.query) | |
| 241 | 234 | |
| 242 | − :gitlab -> | |
| 243 | − case RoomGitlab.Worker.pid(q.source.id) do | |
| 244 | − nil -> | |
| 245 | − [] | |
| 235 | + defp execute_query(%{source: %{type: :cronos}} = q), | |
| 236 | + do: RoomCronos.Worker.query_cronos(q.id, q.query) | |
| 246 | 237 | |
| 247 | − _val -> | |
| 248 | − (Process.alive?(RoomGitlab.Worker.pid(q.source.id)) && | |
| 249 | − RoomGitlab.Worker.read_jobs(q.source.id, q.query)) || [] | |
| 250 | − end | |
| 251 | − | |
| 252 | − :github -> | |
| 253 | − case RoomGithub.Worker.pid(q.source.id) do | |
| 254 | − nil -> | |
| 255 | − [] | |
| 256 | − | |
| 257 | − _val -> | |
| 258 | − level = Map.get(q.query, :level) || Map.get(q.query, "level") || "runs" | |
| 259 | − | |
| 260 | − if Process.alive?(RoomGithub.Worker.pid(q.source.id)) do | |
| 261 | − case level do | |
| 262 | − "jobs" -> RoomGithub.Worker.read_jobs(q.source.id, q.query) | |
| 263 | − _ -> RoomGithub.Worker.read_runs(q.source.id, q.query) | |
| 264 | − end | |
| 265 | − else | |
| 266 | − [] | |
| 267 | − end | |
| 268 | − end | |
| 238 | + defp execute_query(%{source: %{type: :gitlab}} = q) do | |
| 239 | + case RoomGitlab.Worker.pid(q.source.id) do | |
| 240 | + nil -> | |
| 241 | + [] | |
| 269 | 242 | |
| 270 | − :packages -> | |
| 271 | − RoomPackages.Worker.read(q.source.id, q.query) | |
| 243 | + _val -> | |
| 244 | + (Process.alive?(RoomGitlab.Worker.pid(q.source.id)) && | |
| 245 | + RoomGitlab.Worker.read_jobs(q.source.id, q.query)) || [] | |
| 246 | + end | |
| 247 | + end | |
| 272 | 248 | |
| 273 | − :drought -> | |
| 274 | − case RoomDrought.Worker.pid(q.source.id) do | |
| 275 | − nil -> [] | |
| 276 | − _pid -> RoomDrought.Worker.read(q.source.id, q.query) || [] | |
| 277 | − end | |
| 249 | + defp execute_query(%{source: %{type: :github}} = q) do | |
| 250 | + case RoomGithub.Worker.pid(q.source.id) do | |
| 251 | + nil -> | |
| 252 | + [] | |
| 278 | 253 | |
| 279 | − :pollen -> | |
| 280 | − case RoomPollen.Worker.pid(q.source.id) do | |
| 281 | − nil -> [] | |
| 282 | − _pid -> RoomPollen.Worker.read(q.source.id, q.query) || [] | |
| 283 | − end | |
| 254 | + _val -> | |
| 255 | + level = Map.get(q.query, :level) || Map.get(q.query, "level") || "runs" | |
| 284 | 256 | |
| 285 | − :icarus -> | |
| 286 | − case RoomIcarus.Worker.pid(q.source.id) do | |
| 287 | − nil -> [] | |
| 288 | − _pid -> RoomIcarus.Worker.read(q.source.id, q.query) || [] | |
| 257 | + if Process.alive?(RoomGithub.Worker.pid(q.source.id)) do | |
| 258 | + case level do | |
| 259 | + "jobs" -> RoomGithub.Worker.read_jobs(q.source.id, q.query) | |
| 260 | + _ -> RoomGithub.Worker.read_runs(q.source.id, q.query) | |
| 261 | + end | |
| 262 | + else | |
| 263 | + [] | |
| 289 | 264 | end |
| 265 | + end | |
| 266 | + end | |
| 290 | 267 | |
| 291 | − :mailbox -> | |
| 292 | − case RoomHermes.Mail.ImapWorker.pid(q.source.id) do | |
| 293 | − nil -> [] | |
| 294 | − _pid -> RoomHermes.Mail.ImapWorker.read(q.source.id, q.query) || [] | |
| 295 | − end | |
| 268 | + defp execute_query(%{source: %{type: :packages}} = q), | |
| 269 | + do: RoomPackages.Worker.read(q.source.id, q.query) | |
| 296 | 270 | |
| 297 | − :treasury -> | |
| 298 | − case RoomTreasury.Worker.pid(q.source.id) do | |
| 299 | − nil -> [] | |
| 300 | − _pid -> RoomTreasury.Worker.read(q.source.id, q.query) || [] | |
| 301 | − end | |
| 271 | + defp execute_query(%{source: %{type: type}} = q) when is_map_key(@readers, type) do | |
| 272 | + worker = Map.fetch!(@readers, type) | |
| 302 | 273 | |
| 303 | − :bourse -> | |
| 304 | − case RoomBourse.Worker.pid(q.source.id) do | |
| 305 | − nil -> [] | |
| 306 | − _pid -> RoomBourse.Worker.read(q.source.id, q.query) || [] | |
| 307 | − end | |
| 274 | + case worker.pid(q.source.id) do | |
| 275 | + nil -> [] | |
| 276 | + _pid -> worker.read(q.source.id, q.query) || [] | |
| 308 | 277 | end |
| 309 | 278 | end |
| 310 | 279 |
@@ -496,56 +465,38 @@ defmodule RoomSanctum.Worker.Keryx do
| 496 | 465 | end |
| 497 | 466 | end |
| 498 | 467 | |
| 468 | + # Where each type keeps the field an item's id is read from, in order of | |
| 469 | + # preference. A type not listed falls back to the common id fields, | |
| 470 | + # @default_id_keys. | |
| 471 | + @id_keys %{ | |
| 472 | + gtfs: [:trip_id, "trip_id"], | |
| 473 | + # Use the name field for GBFS instead of station_id | |
| 474 | + gbfs: [:name, "name", :station_id, "station_id"], | |
| 475 | + weather: [:time, "time", :dt, "dt"], | |
| 476 | + tidal: [:t, "t", :time, "time"], | |
| 477 | + aqi: [:parameter, "parameter"], | |
| 478 | + calendar: [:uid, "uid", :id, "id"], | |
| 479 | + ephem: [:name, "name"], | |
| 480 | + cronos: [:id, "id", :name, "name"], | |
| 481 | + gitlab: [:id, "id"], | |
| 482 | + packages: [:tracking_number, "tracking_number", :id, "id"] | |
| 483 | + } | |
| 484 | + | |
| 485 | + @default_id_keys [:id, "id", :uid, "uid"] | |
| 486 | + | |
| 499 | 487 | # Extract and stubbify an identifier from an item based on its type |
| 500 | 488 | defp extract_item_id(item, type) when is_map(item) do |
| 501 | − # Try to find a suitable ID field based on the data type | |
| 502 | − id_value = | |
| 503 | − case type do | |
| 504 | − :gtfs -> | |
| 505 | − Map.get(item, :trip_id) || Map.get(item, "trip_id") | |
| 506 | − | |
| 507 | − :gbfs -> | |
| 508 | − # Use the name field for GBFS instead of station_id | |
| 509 | − Map.get(item, :name) || Map.get(item, "name") || Map.get(item, :station_id) || | |
| 510 | − Map.get(item, "station_id") | |
| 511 | − | |
| 512 | − :weather -> | |
| 513 | − Map.get(item, :time) || Map.get(item, "time") || Map.get(item, :dt) || | |
| 514 | − Map.get(item, "dt") | |
| 515 | − | |
| 516 | − :tidal -> | |
| 517 | − Map.get(item, :t) || Map.get(item, "t") || Map.get(item, :time) || Map.get(item, "time") | |
| 518 | − | |
| 519 | − :aqi -> | |
| 520 | − Map.get(item, :parameter) || Map.get(item, "parameter") | |
| 521 | − | |
| 522 | − :calendar -> | |
| 523 | − Map.get(item, :uid) || Map.get(item, "uid") || Map.get(item, :id) || Map.get(item, "id") | |
| 524 | − | |
| 525 | − :ephem -> | |
| 526 | − Map.get(item, :name) || Map.get(item, "name") | |
| 527 | − | |
| 528 | − :cronos -> | |
| 529 | − Map.get(item, :id) || Map.get(item, "id") || Map.get(item, :name) || | |
| 530 | − Map.get(item, "name") | |
| 531 | − | |
| 532 | − :gitlab -> | |
| 533 | − Map.get(item, :id) || Map.get(item, "id") | |
| 534 | − | |
| 535 | − :packages -> | |
| 536 | − Map.get(item, :tracking_number) || Map.get(item, "tracking_number") || | |
| 537 | − Map.get(item, :id) || Map.get(item, "id") | |
| 538 | − | |
| 539 | − _ -> | |
| 540 | − # Fallback: try common ID fields | |
| 541 | − Map.get(item, :id) || Map.get(item, "id") || Map.get(item, :uid) || Map.get(item, "uid") | |
| 542 | − end | |
| 543 | − | |
| 544 | − stubbify(id_value) | |
| 489 | + stubbify(first_value(item, Map.get(@id_keys, type, @default_id_keys))) | |
| 545 | 490 | end |
| 546 | 491 | |
| 547 | 492 | defp extract_item_id(_item, _type), do: generate_stub() |
| 548 | 493 | |
| 494 | + # The first of the keys that holds a value wins -- an `a || b || ...` chain | |
| 495 | + # over them. | |
| 496 | + defp first_value(item, keys) do | |
| 497 | + Enum.reduce(keys, nil, fn key, found -> found || Map.get(item, key) end) | |
| 498 | + end | |
| 499 | + | |
| 549 | 500 | # Convert a value into a URL-safe stub |
| 550 | 501 | defp stubbify(nil), do: generate_stub() |
| 551 | 502 |
modified
apps/room_sanctum/lib/room_sanctum/workers/plani_worker.ex
+109
−104
@@ -296,110 +296,7 @@ defmodule RoomSanctum.Worker.Plani do
| 296 | 296 | defp ask(source_id, %Geo.Point{} = anchor, plani) do |
| 297 | 297 | source = Configuration.get_source!(:bare, source_id) |
| 298 | 298 | |
| 299 | − case source.type do | |
| 300 | − :gtfs -> | |
| 301 | − stops = Storage.nearby_stops(source_id, anchor, plani.limit, plani.radius) | |
| 302 | − | |
| 303 | − by_stop = | |
| 304 | − Enum.map(stops, fn stop -> | |
| 305 | − arrivals = | |
| 306 | − source_id | |
| 307 | − |> arrivals_at(stop.stop_id, source.config) | |
| 308 | − |> Enum.sort_by(& &1.arrival_time) | |
| 309 | − | |
| 310 | − # Which way the stop lies, carried on each arrival: the condenser | |
| 311 | − # groups arrivals into routes and only it knows which end up | |
| 312 | − # together, so the bearing has to travel with them. | |
| 313 | − bearing = compass(anchor, Map.get(stop, :stop_lat), Map.get(stop, :stop_lon)) | |
| 314 | − | |
| 315 | − {stop, Enum.map(arrivals, &Map.put(&1, :bearing, bearing))} | |
| 316 | − end) | |
| 317 | − |> nearest_per_route(plani) | |
| 318 | − | |
| 319 | − entries = | |
| 320 | − if plani.break_out do | |
| 321 | − # One entry per stop, named after it, the way a vision's queries | |
| 322 | − # are. A stop with nothing due is left out rather than published | |
| 323 | − # empty -- the same call the clients already make. | |
| 324 | − for {stop, arrivals} <- by_stop, arrivals != [] do | |
| 325 | − key = "#{source_id}:#{stop.stop_id}" | |
| 326 | − | |
| 327 | − descriptor = | |
| 328 | − described(source, key, stop.stop_name, %{ | |
| 329 | − source_id: source_id, | |
| 330 | − stop: stop.stop_id | |
| 331 | − }) | |
| 332 | − | |
| 333 | − {{key, :gtfs}, arrivals, descriptor} | |
| 334 | − end | |
| 335 | − else | |
| 336 | − blended = | |
| 337 | − by_stop | |
| 338 | − |> Enum.flat_map(fn {_stop, arrivals} -> arrivals end) | |
| 339 | − |> Enum.sort_by(& &1.arrival_time) | |
| 340 | − |> Enum.take(16) | |
| 341 | − | |
| 342 | − [{{source_id, :gtfs}, blended, described(source)}] | |
| 343 | − end | |
| 344 | − | |
| 345 | − {entries, places(stops, source, :stop)} | |
| 346 | − | |
| 347 | − :gbfs -> | |
| 348 | − # Both, because a source does not say which kind it is. A docked | |
| 349 | − # system answers with docks and a dockless one with loose bikes, and | |
| 350 | − # asking only for the latter left a city full of docks looking empty. | |
| 351 | − # Both come back closest-first, so the cap keeps the nearest rather | |
| 352 | − # than whichever the feed happened to list first. Capped separately: | |
| 353 | − # they are different things, and one starving the other would make a | |
| 354 | − # docked system with loose bikes in it look like it had no docks. | |
| 355 | − bikes = | |
| 356 | − Storage.free_bikes_near(source_id, anchor, plani.radius) | |
| 357 | − |> cap(plani.bike_limit) | |
| 358 | − |> Enum.map(&toward(&1, anchor)) | |
| 359 | − | |
| 360 | − docks = | |
| 361 | − Storage.stations_near(source_id, anchor, plani.radius) | |
| 362 | − |> cap(plani.bike_limit) | |
| 363 | − |> Enum.map(&toward(&1, anchor)) | |
| 364 | − | |
| 365 | − entries = | |
| 366 | − if plani.break_out do | |
| 367 | − # A dock is a place, so it gets its own entry. Loose bikes are not | |
| 368 | − # -- they are a count of what is lying about nearby -- so they stay | |
| 369 | − # together under the source. | |
| 370 | − per_dock = | |
| 371 | − for dock <- docks do | |
| 372 | − key = "#{source_id}:#{dock.station_id}" | |
| 373 | − {{key, :gbfs}, [dock], described(source, key, dock.name, %{source_id: source_id})} | |
| 374 | − end | |
| 375 | − | |
| 376 | − if bikes == [], | |
| 377 | − do: per_dock, | |
| 378 | − else: [{{source_id, :gbfs}, bikes, described(source)} | per_dock] | |
| 379 | − else | |
| 380 | − [{{source_id, :gbfs}, bikes ++ docks, described(source)}] | |
| 381 | − end | |
| 382 | − | |
| 383 | − {entries, places(bikes, source, :bike) ++ places(docks, source, :dock)} | |
| 384 | − | |
| 385 | − :aqi -> | |
| 386 | − readings = Storage.nearby_aqi_stations(source_id, anchor, plani.limit) | |
| 387 | − {[{{source_id, :aqi}, readings, described(source)}], places(readings, source, :monitor)} | |
| 388 | − | |
| 389 | − type when type in @asked_at_a_point -> | |
| 390 | − # Answered *at* the anchor rather than near it. These name a foci in a | |
| 391 | − # vision -- the weather at home, sunrise at home -- and the only thing | |
| 392 | − # a Plani changes is where "at" is. The query is the anchor itself, | |
| 393 | − # which `Configuration.place_for!/1` prefers over a foci id. | |
| 394 | − results = ask_at(type, source_id, %{place: anchor}) | |
| 395 | − {[{{source_id, type}, results, described(source)}], []} | |
| 396 | − | |
| 397 | − other -> | |
| 398 | − # Sources with nothing located in them. Left out rather than guessed | |
| 399 | − # at; the page says what it could not draw. | |
| 400 | − Logger.debug("plani: no spatial answer for a #{other} source") | |
| 401 | − nil | |
| 402 | − end | |
| 299 | + ask_source(source.type, source_id, source, anchor, plani) | |
| 403 | 300 | rescue |
| 404 | 301 | # `catch` takes throws and exits and lets a raise straight through, so one |
| 405 | 302 | # source with no config, or a stop id that has gone, took the whole worker |
@@ -414,6 +311,114 @@ defmodule RoomSanctum.Worker.Plani do
| 414 | 311 | |
| 415 | 312 | defp ask(_source_id, _anchor, _plani), do: nil |
| 416 | 313 | |
| 314 | + defp ask_source(:gtfs, source_id, source, anchor, plani) do | |
| 315 | + stops = Storage.nearby_stops(source_id, anchor, plani.limit, plani.radius) | |
| 316 | + | |
| 317 | + by_stop = | |
| 318 | + Enum.map(stops, fn stop -> | |
| 319 | + arrivals = | |
| 320 | + source_id | |
| 321 | + |> arrivals_at(stop.stop_id, source.config) | |
| 322 | + |> Enum.sort_by(& &1.arrival_time) | |
| 323 | + | |
| 324 | + # Which way the stop lies, carried on each arrival: the condenser | |
| 325 | + # groups arrivals into routes and only it knows which end up | |
| 326 | + # together, so the bearing has to travel with them. | |
| 327 | + bearing = compass(anchor, Map.get(stop, :stop_lat), Map.get(stop, :stop_lon)) | |
| 328 | + | |
| 329 | + {stop, Enum.map(arrivals, &Map.put(&1, :bearing, bearing))} | |
| 330 | + end) | |
| 331 | + |> nearest_per_route(plani) | |
| 332 | + | |
| 333 | + entries = | |
| 334 | + if plani.break_out do | |
| 335 | + # One entry per stop, named after it, the way a vision's queries | |
| 336 | + # are. A stop with nothing due is left out rather than published | |
| 337 | + # empty -- the same call the clients already make. | |
| 338 | + for {stop, arrivals} <- by_stop, arrivals != [] do | |
| 339 | + key = "#{source_id}:#{stop.stop_id}" | |
| 340 | + | |
| 341 | + descriptor = | |
| 342 | + described(source, key, stop.stop_name, %{ | |
| 343 | + source_id: source_id, | |
| 344 | + stop: stop.stop_id | |
| 345 | + }) | |
| 346 | + | |
| 347 | + {{key, :gtfs}, arrivals, descriptor} | |
| 348 | + end | |
| 349 | + else | |
| 350 | + blended = | |
| 351 | + by_stop | |
| 352 | + |> Enum.flat_map(fn {_stop, arrivals} -> arrivals end) | |
| 353 | + |> Enum.sort_by(& &1.arrival_time) | |
| 354 | + |> Enum.take(16) | |
| 355 | + | |
| 356 | + [{{source_id, :gtfs}, blended, described(source)}] | |
| 357 | + end | |
| 358 | + | |
| 359 | + {entries, places(stops, source, :stop)} | |
| 360 | + end | |
| 361 | + | |
| 362 | + defp ask_source(:gbfs, source_id, source, anchor, plani) do | |
| 363 | + # Both, because a source does not say which kind it is. A docked | |
| 364 | + # system answers with docks and a dockless one with loose bikes, and | |
| 365 | + # asking only for the latter left a city full of docks looking empty. | |
| 366 | + # Both come back closest-first, so the cap keeps the nearest rather | |
| 367 | + # than whichever the feed happened to list first. Capped separately: | |
| 368 | + # they are different things, and one starving the other would make a | |
| 369 | + # docked system with loose bikes in it look like it had no docks. | |
| 370 | + bikes = | |
| 371 | + Storage.free_bikes_near(source_id, anchor, plani.radius) | |
| 372 | + |> cap(plani.bike_limit) | |
| 373 | + |> Enum.map(&toward(&1, anchor)) | |
| 374 | + | |
| 375 | + docks = | |
| 376 | + Storage.stations_near(source_id, anchor, plani.radius) | |
| 377 | + |> cap(plani.bike_limit) | |
| 378 | + |> Enum.map(&toward(&1, anchor)) | |
| 379 | + | |
| 380 | + entries = | |
| 381 | + if plani.break_out do | |
| 382 | + # A dock is a place, so it gets its own entry. Loose bikes are not | |
| 383 | + # -- they are a count of what is lying about nearby -- so they stay | |
| 384 | + # together under the source. | |
| 385 | + per_dock = | |
| 386 | + for dock <- docks do | |
| 387 | + key = "#{source_id}:#{dock.station_id}" | |
| 388 | + {{key, :gbfs}, [dock], described(source, key, dock.name, %{source_id: source_id})} | |
| 389 | + end | |
| 390 | + | |
| 391 | + if bikes == [], | |
| 392 | + do: per_dock, | |
| 393 | + else: [{{source_id, :gbfs}, bikes, described(source)} | per_dock] | |
| 394 | + else | |
| 395 | + [{{source_id, :gbfs}, bikes ++ docks, described(source)}] | |
| 396 | + end | |
| 397 | + | |
| 398 | + {entries, places(bikes, source, :bike) ++ places(docks, source, :dock)} | |
| 399 | + end | |
| 400 | + | |
| 401 | + defp ask_source(:aqi, source_id, source, anchor, plani) do | |
| 402 | + readings = Storage.nearby_aqi_stations(source_id, anchor, plani.limit) | |
| 403 | + {[{{source_id, :aqi}, readings, described(source)}], places(readings, source, :monitor)} | |
| 404 | + end | |
| 405 | + | |
| 406 | + defp ask_source(type, source_id, source, anchor, _plani) when type in @asked_at_a_point do | |
| 407 | + # Answered *at* the anchor rather than near it. These name a foci in a | |
| 408 | + # vision -- the weather at home, sunrise at home -- and the only thing | |
| 409 | + # a Plani changes is where "at" is. The query is the anchor itself, | |
| 410 | + # which `Configuration.place_for!/1` prefers over a foci id. | |
| 411 | + results = ask_at(type, source_id, %{place: anchor}) | |
| 412 | + {[{{source_id, type}, results, described(source)}], []} | |
| 413 | + end | |
| 414 | + | |
| 415 | + defp ask_source(other, _source_id, _source, _anchor, _plani) do | |
| 416 | + # Sources with nothing located in them. Left out rather than guessed | |
| 417 | + # at; the page says what it could not draw. | |
| 418 | + Logger.debug("plani: no spatial answer for a #{other} source") | |
| 419 | + nil | |
| 420 | + end | |
| 421 | + | |
| 417 | 422 | # What the condensers expect alongside the data: a vision hands them a query, |
| 418 | 423 | # so a Plani hands them something query-shaped. The empty `query` matters -- |
| 419 | 424 | # the GTFS condenser reads a stop out of it to look up stop-specific alerts, |
modified
apps/room_sanctum/lib/room_sanctum/workers/vision_worker.ex
+58
−77
@@ -308,98 +308,79 @@ defmodule RoomSanctum.Worker.Vision do
| 308 | 308 | :skip |
| 309 | 309 | end |
| 310 | 310 | |
| 311 | − defp query_one(q) do | |
| 312 | − case q.source.type do | |
| 313 | − :gtfs -> | |
| 314 | − RoomGtfs.Worker.query_stop(q.source.id, q.query) | |
| 311 | + # Workers that may not be running: one that is not has nothing to read. | |
| 312 | + @readers %{ | |
| 313 | + drought: RoomDrought.Worker, | |
| 314 | + pollen: RoomPollen.Worker, | |
| 315 | + icarus: RoomIcarus.Worker, | |
| 316 | + mailbox: RoomHermes.Mail.ImapWorker, | |
| 317 | + treasury: RoomTreasury.Worker, | |
| 318 | + bourse: RoomBourse.Worker | |
| 319 | + } | |
| 315 | 320 | |
| 316 | − :gbfs -> | |
| 317 | − RoomGbfs.Worker.query_stop(q.source.id, q.query) | |
| 321 | + defp query_one(%{source: %{type: :gtfs}} = q), | |
| 322 | + do: RoomGtfs.Worker.query_stop(q.source.id, q.query) | |
| 318 | 323 | |
| 319 | − :tidal -> | |
| 320 | − RoomTidal.Worker.query_tides(q.source.id, q.query) | |
| 324 | + defp query_one(%{source: %{type: :gbfs}} = q), | |
| 325 | + do: RoomGbfs.Worker.query_stop(q.source.id, q.query) | |
| 321 | 326 | |
| 322 | − :weather -> | |
| 323 | − RoomWeather.Worker.query_weather(q.source.id, q.query) | |
| 327 | + defp query_one(%{source: %{type: :tidal}} = q), | |
| 328 | + do: RoomTidal.Worker.query_tides(q.source.id, q.query) | |
| 324 | 329 | |
| 325 | − :aqi -> | |
| 326 | − RoomAirQuality.Worker.query_place(q.source.id, q.query) | |
| 330 | + defp query_one(%{source: %{type: :weather}} = q), | |
| 331 | + do: RoomWeather.Worker.query_weather(q.source.id, q.query) | |
| 327 | 332 | |
| 328 | − :ephem -> | |
| 329 | − RoomEphem.Worker.query_ephem(q.source.id, q.query) | |
| 333 | + defp query_one(%{source: %{type: :aqi}} = q), | |
| 334 | + do: RoomAirQuality.Worker.query_place(q.source.id, q.query) | |
| 330 | 335 | |
| 331 | − :calendar -> | |
| 332 | − RoomCalendar.Worker.query_calendar(q.source.id, q.query) | |
| 336 | + defp query_one(%{source: %{type: :ephem}} = q), | |
| 337 | + do: RoomEphem.Worker.query_ephem(q.source.id, q.query) | |
| 333 | 338 | |
| 334 | − :cronos -> | |
| 335 | − RoomCronos.Worker.query_cronos(q.id, q.query) | |
| 339 | + defp query_one(%{source: %{type: :calendar}} = q), | |
| 340 | + do: RoomCalendar.Worker.query_calendar(q.source.id, q.query) | |
| 336 | 341 | |
| 337 | − :gitlab -> | |
| 338 | − case RoomGitlab.Worker.pid(q.source.id) do | |
| 339 | − nil -> | |
| 340 | − [] | |
| 342 | + defp query_one(%{source: %{type: :cronos}} = q), | |
| 343 | + do: RoomCronos.Worker.query_cronos(q.id, q.query) | |
| 341 | 344 | |
| 342 | − _val -> | |
| 343 | − (Process.alive?(RoomGitlab.Worker.pid(q.source.id)) && | |
| 344 | − RoomGitlab.Worker.read_jobs(q.source.id, q.query)) || [] | |
| 345 | − end | |
| 346 | − | |
| 347 | − :github -> | |
| 348 | − case RoomGithub.Worker.pid(q.source.id) do | |
| 349 | − nil -> | |
| 350 | − [] | |
| 351 | − | |
| 352 | − _val -> | |
| 353 | − level = Map.get(q.query, :level) || Map.get(q.query, "level") || "runs" | |
| 354 | − | |
| 355 | − if Process.alive?(RoomGithub.Worker.pid(q.source.id)) do | |
| 356 | − case level do | |
| 357 | − "jobs" -> RoomGithub.Worker.read_jobs(q.source.id, q.query) | |
| 358 | − _ -> RoomGithub.Worker.read_runs(q.source.id, q.query) | |
| 359 | − end | |
| 360 | − else | |
| 361 | − [] | |
| 362 | − end | |
| 363 | − end | |
| 364 | − | |
| 365 | − :packages -> | |
| 366 | − RoomPackages.Worker.read(q.source.id, q.query) | |
| 345 | + defp query_one(%{source: %{type: :gitlab}} = q) do | |
| 346 | + case RoomGitlab.Worker.pid(q.source.id) do | |
| 347 | + nil -> | |
| 348 | + [] | |
| 367 | 349 | |
| 368 | − :drought -> | |
| 369 | − case RoomDrought.Worker.pid(q.source.id) do | |
| 370 | − nil -> [] | |
| 371 | − _pid -> RoomDrought.Worker.read(q.source.id, q.query) || [] | |
| 372 | − end | |
| 373 | − | |
| 374 | − :pollen -> | |
| 375 | − case RoomPollen.Worker.pid(q.source.id) do | |
| 376 | − nil -> [] | |
| 377 | − _pid -> RoomPollen.Worker.read(q.source.id, q.query) || [] | |
| 378 | − end | |
| 350 | + _val -> | |
| 351 | + (Process.alive?(RoomGitlab.Worker.pid(q.source.id)) && | |
| 352 | + RoomGitlab.Worker.read_jobs(q.source.id, q.query)) || [] | |
| 353 | + end | |
| 354 | + end | |
| 379 | 355 | |
| 380 | − :icarus -> | |
| 381 | − case RoomIcarus.Worker.pid(q.source.id) do | |
| 382 | − nil -> [] | |
| 383 | − _pid -> RoomIcarus.Worker.read(q.source.id, q.query) || [] | |
| 356 | + defp query_one(%{source: %{type: :github}} = q) do | |
| 357 | + case RoomGithub.Worker.pid(q.source.id) do | |
| 358 | + nil -> | |
| 359 | + [] | |
| 360 | + | |
| 361 | + _val -> | |
| 362 | + level = Map.get(q.query, :level) || Map.get(q.query, "level") || "runs" | |
| 363 | + | |
| 364 | + if Process.alive?(RoomGithub.Worker.pid(q.source.id)) do | |
| 365 | + case level do | |
| 366 | + "jobs" -> RoomGithub.Worker.read_jobs(q.source.id, q.query) | |
| 367 | + _ -> RoomGithub.Worker.read_runs(q.source.id, q.query) | |
| 368 | + end | |
| 369 | + else | |
| 370 | + [] | |
| 384 | 371 | end |
| 372 | + end | |
| 373 | + end | |
| 385 | 374 | |
| 386 | − :mailbox -> | |
| 387 | − case RoomHermes.Mail.ImapWorker.pid(q.source.id) do | |
| 388 | − nil -> [] | |
| 389 | − _pid -> RoomHermes.Mail.ImapWorker.read(q.source.id, q.query) || [] | |
| 390 | − end | |
| 375 | + defp query_one(%{source: %{type: :packages}} = q), | |
| 376 | + do: RoomPackages.Worker.read(q.source.id, q.query) | |
| 391 | 377 | |
| 392 | − :treasury -> | |
| 393 | − case RoomTreasury.Worker.pid(q.source.id) do | |
| 394 | − nil -> [] | |
| 395 | − _pid -> RoomTreasury.Worker.read(q.source.id, q.query) || [] | |
| 396 | − end | |
| 378 | + defp query_one(%{source: %{type: type}} = q) when is_map_key(@readers, type) do | |
| 379 | + worker = Map.fetch!(@readers, type) | |
| 397 | 380 | |
| 398 | − :bourse -> | |
| 399 | − case RoomBourse.Worker.pid(q.source.id) do | |
| 400 | − nil -> [] | |
| 401 | − _pid -> RoomBourse.Worker.read(q.source.id, q.query) || [] | |
| 402 | − end | |
| 381 | + case worker.pid(q.source.id) do | |
| 382 | + nil -> [] | |
| 383 | + _pid -> worker.read(q.source.id, q.query) || [] | |
| 403 | 384 | end |
| 404 | 385 | end |
| 405 | 386 | end |
modified
apps/room_sanctum/lib/room_sanctum_web/icon_helpers.ex
+51
−128
@@ -1,130 +1,53 @@
| 1 | 1 | defmodule RoomSanctumWeb.IconHelpers do |
| 2 | − def icon(source_type) do | |
| 3 | − case source_type do | |
| 4 | − :calendar -> | |
| 5 | − "fa-calendar-alt" | |
| 6 | − | |
| 7 | − :rideshare -> | |
| 8 | − "fa-taxi" | |
| 9 | − | |
| 10 | − :hass -> | |
| 11 | − "fa-home" | |
| 12 | − | |
| 13 | − :gtfs -> | |
| 14 | − "fa-bus-alt" | |
| 15 | − | |
| 16 | − :gbfs -> | |
| 17 | − "fa-bicycle" | |
| 18 | − | |
| 19 | − :tidal -> | |
| 20 | − "fa-water" | |
| 21 | − | |
| 22 | − :ephem -> | |
| 23 | − "fa-moon" | |
| 24 | − | |
| 25 | − :weather -> | |
| 26 | − "fa-cloud-sun" | |
| 27 | − | |
| 28 | − :aqi -> | |
| 29 | − "fa-lungs" | |
| 30 | − | |
| 31 | − :cronos -> | |
| 32 | − "fa-clock" | |
| 33 | − | |
| 34 | − :gitlab -> | |
| 35 | − "fa-code-branch" | |
| 36 | − | |
| 37 | − :github -> | |
| 38 | − "fa-github" | |
| 39 | − | |
| 40 | − :drought -> | |
| 41 | − "fa-sun-plant-wilt" | |
| 42 | − | |
| 43 | − :pollen -> | |
| 44 | − "fa-seedling" | |
| 45 | − | |
| 46 | − :icarus -> | |
| 47 | − "fa-plane-up" | |
| 48 | − | |
| 49 | − :mailbox -> | |
| 50 | − "fa-inbox" | |
| 51 | − | |
| 52 | − :treasury -> | |
| 53 | − "fa-money-bill-transfer" | |
| 54 | − | |
| 55 | − :bourse -> | |
| 56 | − "fa-chart-line" | |
| 57 | − | |
| 58 | − :packages -> | |
| 59 | − "fa-envelopes-bulk" | |
| 60 | − | |
| 61 | − # | |
| 62 | − :const -> | |
| 63 | − "fa-triangle-exclamation" | |
| 64 | − end | |
| 65 | − end | |
| 66 | − | |
| 67 | − def icon_code(source_type) do | |
| 68 | − case source_type do | |
| 69 | − :calendar -> | |
| 70 | − "f073" | |
| 71 | − | |
| 72 | − :rideshare -> | |
| 73 | − "f1ba" | |
| 74 | − | |
| 75 | − :hass -> | |
| 76 | − "f015" | |
| 77 | − | |
| 78 | − :gtfs -> | |
| 79 | − "f55e" | |
| 80 | − | |
| 81 | − :gbfs -> | |
| 82 | − "f206" | |
| 83 | − | |
| 84 | − :tidal -> | |
| 85 | − "f773" | |
| 86 | − | |
| 87 | − :ephem -> | |
| 88 | − "f186" | |
| 89 | − | |
| 90 | − :weather -> | |
| 91 | − "f6c4" | |
| 92 | − | |
| 93 | − :aqi -> | |
| 94 | − "f604" | |
| 95 | − | |
| 96 | − :cronos -> | |
| 97 | − "f017" | |
| 98 | − | |
| 99 | − :gitlab -> | |
| 100 | − "f126" | |
| 101 | − | |
| 102 | − :github -> | |
| 103 | − "f09b" | |
| 104 | − | |
| 105 | − :drought -> | |
| 106 | − "e5c8" | |
| 107 | − | |
| 108 | − :pollen -> | |
| 109 | − "f4d8" | |
| 110 | − | |
| 111 | − :icarus -> | |
| 112 | − "e22d" | |
| 113 | − | |
| 114 | − :mailbox -> | |
| 115 | − "f01c" | |
| 116 | − | |
| 117 | − :treasury -> | |
| 118 | − "e528" | |
| 119 | − | |
| 120 | − :bourse -> | |
| 121 | − "f201" | |
| 122 | − | |
| 123 | − :const -> | |
| 124 | − "f071" | |
| 125 | − | |
| 126 | − :packages -> | |
| 127 | − "f674" | |
| 128 | − end | |
| 129 | − end | |
| 2 | + # Font Awesome class and codepoint for each source type. An unknown type | |
| 3 | + # raises, as the case these replace did. | |
| 4 | + @icons %{ | |
| 5 | + calendar: "fa-calendar-alt", | |
| 6 | + rideshare: "fa-taxi", | |
| 7 | + hass: "fa-home", | |
| 8 | + gtfs: "fa-bus-alt", | |
| 9 | + gbfs: "fa-bicycle", | |
| 10 | + tidal: "fa-water", | |
| 11 | + ephem: "fa-moon", | |
| 12 | + weather: "fa-cloud-sun", | |
| 13 | + aqi: "fa-lungs", | |
| 14 | + cronos: "fa-clock", | |
| 15 | + gitlab: "fa-code-branch", | |
| 16 | + github: "fa-github", | |
| 17 | + drought: "fa-sun-plant-wilt", | |
| 18 | + pollen: "fa-seedling", | |
| 19 | + icarus: "fa-plane-up", | |
| 20 | + mailbox: "fa-inbox", | |
| 21 | + treasury: "fa-money-bill-transfer", | |
| 22 | + bourse: "fa-chart-line", | |
| 23 | + packages: "fa-envelopes-bulk", | |
| 24 | + const: "fa-triangle-exclamation" | |
| 25 | + } | |
| 26 | + | |
| 27 | + @icon_codes %{ | |
| 28 | + calendar: "f073", | |
| 29 | + rideshare: "f1ba", | |
| 30 | + hass: "f015", | |
| 31 | + gtfs: "f55e", | |
| 32 | + gbfs: "f206", | |
| 33 | + tidal: "f773", | |
| 34 | + ephem: "f186", | |
| 35 | + weather: "f6c4", | |
| 36 | + aqi: "f604", | |
| 37 | + cronos: "f017", | |
| 38 | + gitlab: "f126", | |
| 39 | + github: "f09b", | |
| 40 | + drought: "e5c8", | |
| 41 | + pollen: "f4d8", | |
| 42 | + icarus: "e22d", | |
| 43 | + mailbox: "f01c", | |
| 44 | + treasury: "e528", | |
| 45 | + bourse: "f201", | |
| 46 | + const: "f071", | |
| 47 | + packages: "f674" | |
| 48 | + } | |
| 49 | + | |
| 50 | + def icon(source_type), do: Map.fetch!(@icons, source_type) | |
| 51 | + | |
| 52 | + def icon_code(source_type), do: Map.fetch!(@icon_codes, source_type) | |
| 130 | 53 | end |
modified
apps/room_zeus/lib/supervision_worker.ex
+22
−22
@@ -5,29 +5,29 @@ defmodule RoomZeus.DynSupervisor do
| 5 | 5 | DynamicSupervisor.start_link(__MODULE__, init_arg, name: __MODULE__) |
| 6 | 6 | end |
| 7 | 7 | |
| 8 | − def start_child(type, instance_id) do | |
| 9 | − spec = | |
| 10 | − case type do | |
| 11 | − :gtfs -> {RoomGtfs.Worker, [name: instance_id]} | |
| 12 | − :gbfs -> {RoomGbfs.Worker, [name: instance_id]} | |
| 13 | − :aqi -> {RoomAirQuality.Worker, [name: instance_id]} | |
| 14 | − :tidal -> {RoomTidal.Worker, [name: instance_id]} | |
| 15 | − :weather -> {RoomWeather.Worker, [name: instance_id]} | |
| 16 | − :calendar -> {RoomCalendar.Worker, [name: instance_id]} | |
| 17 | − :gitlab -> {RoomGitlab.Worker, [name: instance_id]} | |
| 18 | − :github -> {RoomGithub.Worker, [name: instance_id]} | |
| 19 | − :packages -> {RoomPackages.Worker, [name: instance_id]} | |
| 20 | − :drought -> {RoomDrought.Worker, [name: instance_id]} | |
| 21 | − :pollen -> {RoomPollen.Worker, [name: instance_id]} | |
| 22 | − :icarus -> {RoomIcarus.Worker, [name: instance_id]} | |
| 23 | − :mailbox -> {RoomHermes.Mail.ImapWorker, [name: instance_id]} | |
| 24 | − :treasury -> {RoomTreasury.Worker, [name: instance_id]} | |
| 25 | − :bourse -> {RoomBourse.Worker, [name: instance_id]} | |
| 26 | − _ -> nil | |
| 27 | − end | |
| 8 | + # The worker behind each source type; a type not listed starts nothing. | |
| 9 | + @workers %{ | |
| 10 | + gtfs: RoomGtfs.Worker, | |
| 11 | + gbfs: RoomGbfs.Worker, | |
| 12 | + aqi: RoomAirQuality.Worker, | |
| 13 | + tidal: RoomTidal.Worker, | |
| 14 | + weather: RoomWeather.Worker, | |
| 15 | + calendar: RoomCalendar.Worker, | |
| 16 | + gitlab: RoomGitlab.Worker, | |
| 17 | + github: RoomGithub.Worker, | |
| 18 | + packages: RoomPackages.Worker, | |
| 19 | + drought: RoomDrought.Worker, | |
| 20 | + pollen: RoomPollen.Worker, | |
| 21 | + icarus: RoomIcarus.Worker, | |
| 22 | + mailbox: RoomHermes.Mail.ImapWorker, | |
| 23 | + treasury: RoomTreasury.Worker, | |
| 24 | + bourse: RoomBourse.Worker | |
| 25 | + } | |
| 28 | 26 | |
| 29 | − if spec != nil do | |
| 30 | − DynamicSupervisor.start_child(__MODULE__, spec) | |
| 27 | + def start_child(type, instance_id) do | |
| 28 | + case Map.fetch(@workers, type) do | |
| 29 | + {:ok, worker} -> DynamicSupervisor.start_child(__MODULE__, {worker, [name: instance_id]}) | |
| 30 | + :error -> nil | |
| 31 | 31 | end |
| 32 | 32 | end |
| 33 | 33 |
Parents: 6e7ed93