Read a foci the way this app writes one

b4ce49f · Gabriel Morell · 2026-08-11 00:38

10 files +67 -2
Message
{commit_body(@commit)}

Files changed

modified apps/room_sanctum/lib/room_sanctum_web/components/query_geospatial_map.ex
+17 −1
@@ -579,9 +579,15 @@ defmodule RoomSanctumWeb.Components.QueryGeospatialMap do
579 579 # The field is :place. This read :point, and since that was in a guard the
580 580 # KeyError was swallowed and the clause simply never matched -- so a foci
581 581 # has never resolved to anywhere, and no aqi query has ever been placed.
582 + #
583 + # A foci stores {lat, lon}, which is the reverse of the PostGIS convention
584 + # everything else here follows -- gbfs stations and free bikes are {lon, lat}.
585 + # That is how the foci picker writes it and how room_icarus reads it, so it
586 + # is the convention, not a bug to correct: reading it the standard way put
587 + # SFO in Antarctica, latitude clamped to -90.
582 588 defp get_foci_coordinates(foci_id) do
583 589 case RoomSanctum.Storage.get_foci_by_id(foci_id) do
584 − %{place: %Geo.Point{coordinates: {lng, lat}}} -> {lat, lng}
590 + %{place: %Geo.Point{coordinates: {lat, lon}}} -> on_earth(lat, lon)
585 591 _ -> false
586 592 end
587 593 rescue
@@ -589,6 +595,16 @@ defmodule RoomSanctumWeb.Components.QueryGeospatialMap do
589 595 Ecto.NoResultsError -> false
590 596 end
591 597
598 + # Leaflet clamps an out-of-range latitude to the pole, so a point stored the
599 + # other way round would be drawn confidently in the wrong hemisphere. Better
600 + # to leave it off the map.
601 + defp on_earth(lat, lon)
602 + when is_number(lat) and is_number(lon) and
603 + lat >= -90 and lat <= 90 and lon >= -180 and lon <= 180,
604 + do: {lat, lon}
605 +
606 + defp on_earth(_lat, _lon), do: false
607 +
592 608 # Get coordinates from GTFS vehicle positions
593 609 defp get_gtfs_vehicle_coordinates(source_id, query) do
594 610 try do
modified apps/room_sanctum/test/room_sanctum_web/components/query_placement_test.exs
+50 −1
@@ -24,7 +24,9 @@ defmodule RoomSanctumWeb.QueryPlacementTest do
24 24 Configuration.create_foci(%{
25 25 name: "Home",
26 26 user_id: user.id,
27 − place: %Geo.Point{coordinates: {-71.0589, 42.3601}, srid: 4326}
27 + # {lat, lon} -- how the foci picker writes it, verified against the
28 + # rows in the running database
29 + place: %Geo.Point{coordinates: {42.3601, -71.0589}, srid: 4326}
28 30 })
29 31
30 32 %{user: user, foci: foci}
@@ -108,6 +110,53 @@ defmodule RoomSanctumWeb.QueryPlacementTest do
108 110 refute html =~ ~s(lat="0")
109 111 end
110 112
113 + test "a foci is read the way the app writes it, not the PostGIS way", %{user: user} do
114 + # Read as {lon, lat} this is latitude -71, which Leaflet clamps to the
115 + # south pole -- SFO drawn in Antarctica.
116 + {:ok, sfo} =
117 + Configuration.create_foci(%{
118 + name: "SFO",
119 + user_id: user.id,
120 + place: %Geo.Point{coordinates: {37.6226, -122.3843}, srid: 4326}
121 + })
122 +
123 + src = source(user, :ephem, %{"__type__" => "ephem"})
124 +
125 + {:ok, query} =
126 + Configuration.create_query(%{
127 + name: "Above SFO", notes: "", source_id: src.id, user_id: user.id,
128 + query: %{"__type__" => "ephem", "foci_id" => sfo.id}
129 + })
130 +
131 + html = render_queries([Configuration.get_query!(query.id)])
132 +
133 + assert html =~ ~s(lat="37.6226")
134 + assert html =~ ~s(lng="-122.3843")
135 + refute html =~ ~s(lat="-122.3843")
136 + end
137 +
138 + test "a foci stored somewhere impossible is left off rather than drawn at the pole",
139 + %{user: user} do
140 + {:ok, broken} =
141 + Configuration.create_foci(%{
142 + name: "Broken",
143 + user_id: user.id,
144 + place: %Geo.Point{coordinates: {-122.3843, 37.6226}, srid: 4326}
145 + })
146 +
147 + src = source(user, :ephem, %{"__type__" => "ephem"})
148 +
149 + {:ok, query} =
150 + Configuration.create_query(%{
151 + name: "Nowhere", notes: "", source_id: src.id, user_id: user.id,
152 + query: %{"__type__" => "ephem", "foci_id" => broken.id}
153 + })
154 +
155 + html = render_queries([Configuration.get_query!(query.id)])
156 +
157 + refute html =~ ~s(name="Nowhere")
158 + end
159 +
111 160 test "placeable and unplaceable queries mix without either breaking", %{user: user, foci: foci} do
112 161 ephem = source(user, :ephem, %{"__type__" => "ephem"})
113 162 markets = source(user, :bourse, %{"__type__" => "bourse"})

Parents: fc3f047