Optimize viewport discovery and enable automatic updates

This commit is contained in:
SimpleTest 2026-08-01 23:39:39 +03:00
parent d3114e211b
commit afc0040a65
11 changed files with 269 additions and 243 deletions

View File

@ -337,7 +337,6 @@ const createAidMap = element => {
element, element,
map: null, map: null,
active: true, active: true,
pendingViewportAction: null,
resizeFrame: null, resizeFrame: null,
resizeObserver: null, resizeObserver: null,
renderedSize: null, renderedSize: null,
@ -948,11 +947,19 @@ const createAidMap = element => {
const viewport = mapViewport(state.map) const viewport = mapViewport(state.map)
if (!viewport) return if (!viewport) return
state.pendingViewportAction = { const clusterExpansion = state.clusterExpansionPending
viewport,
clusterExpansion: state.clusterExpansionPending
}
state.clusterExpansionPending = false state.clusterExpansionPending = false
// `moveend` already means that the camera transition has finished. Do
// not wait for MapLibre's `idle` event here: `idle` also waits for every
// requested map tile and fade animation, so a slow tile server can delay
// discovery refreshes for seconds even though the user stopped moving
// the map earlier.
if (clusterExpansion) {
state.onClusterExpand?.(viewport)
} else {
state.onViewportChange?.(viewport)
}
} }
state.onIdle = () => { state.onIdle = () => {
@ -964,15 +971,6 @@ const createAidMap = element => {
} }
state.syncVisibleAreas() state.syncVisibleAreas()
const action = state.pendingViewportAction
state.pendingViewportAction = null
if (action?.clusterExpansion) {
state.onClusterExpand?.(action.viewport)
} else if (action) {
state.onViewportChange?.(action.viewport)
}
} }
state.onRequestHighlight = event => state.highlightRequest(event.detail?.id) state.onRequestHighlight = event => state.highlightRequest(event.detail?.id)

View File

@ -62,8 +62,7 @@ defmodule WhoNeedHelp.Activities do
def map_discovery_items(%Scope{user: user}, filters, %DiscoveryViewport{} = viewport) do def map_discovery_items(%Scope{user: user}, filters, %DiscoveryViewport{} = viewport) do
level = DiscoveryCluster.level_for_zoom(viewport.zoom) level = DiscoveryCluster.level_for_zoom(viewport.zoom)
cell_size = DiscoveryCluster.cell_size_meters(level) cell_count = Integer.pow(2, level) * 1.0
world_half = DiscoveryCluster.world_half_meters()
points = points =
user user
@ -75,93 +74,44 @@ defmodule WhoNeedHelp.Activities do
) )
|> select([activity], %{ |> select([activity], %{
id: activity.id, id: activity.id,
title: activity.title, public_longitude: activity.public_longitude,
location_label: activity.location_label, public_latitude: activity.public_latitude
public_location:
fragment(
"""
CASE
WHEN ? @> '{"scale_fixture": true}'::jsonb THEN ?
ELSE
ST_SetSRID(
ST_MakePoint(
round(ST_X(?)::numeric, 2)::double precision,
round(ST_Y(?)::numeric, 2)::double precision
),
4326
)
END
""",
activity.structured_data,
activity.location,
activity.location,
activity.location
)
}) })
points points
|> subquery() |> subquery()
|> group_by([point], [ |> group_by([_point], [selected_as(:cell_x), selected_as(:cell_y)])
fragment(
"floor((ST_X(ST_Transform(?, 3857)) + ?) / ?)::bigint",
point.public_location,
constant(^world_half),
constant(^cell_size)
),
fragment(
"floor((ST_Y(ST_Transform(?, 3857)) + ?) / ?)::bigint",
point.public_location,
constant(^world_half),
constant(^cell_size)
)
])
|> select([point], %{ |> select([point], %{
count: count(point.id), count: count(point.id),
activity_id: fragment("CASE WHEN count(*) = 1 THEN min(?::text) END", point.id), activity_id: fragment("min(?::text)", point.id),
title: fragment("CASE WHEN count(*) = 1 THEN min(?) END", point.title), longitude: min(point.public_longitude),
location_label: fragment("CASE WHEN count(*) = 1 THEN min(?) END", point.location_label), latitude: min(point.public_latitude),
longitude: fragment("avg(ST_X(?))", point.public_location), west: min(point.public_longitude),
latitude: fragment("avg(ST_Y(?))", point.public_location), south: min(point.public_latitude),
west: fragment("min(ST_X(?))", point.public_location), east: max(point.public_longitude),
south: fragment("min(ST_Y(?))", point.public_location), north: max(point.public_latitude),
east: fragment("max(ST_X(?))", point.public_location),
north: fragment("max(ST_Y(?))", point.public_location),
west_m: fragment("min(ST_X(ST_Transform(?, 3857)))", point.public_location),
south_m: fragment("min(ST_Y(ST_Transform(?, 3857)))", point.public_location),
east_m: fragment("max(ST_X(ST_Transform(?, 3857)))", point.public_location),
north_m: fragment("max(ST_Y(ST_Transform(?, 3857)))", point.public_location),
cell_x: cell_x:
fragment( selected_as(
"floor((ST_X(ST_Transform(?, 3857)) + ?) / ?)::bigint", fragment(
point.public_location, "floor(((? + 180.0) / 360.0) * ?)::bigint",
constant(^world_half), point.public_longitude,
constant(^cell_size) constant(^cell_count)
),
:cell_x
), ),
cell_y: cell_y:
fragment( selected_as(
"floor((ST_Y(ST_Transform(?, 3857)) + ?) / ?)::bigint", fragment(
point.public_location, "floor(((ln(tan(pi() / 4.0 + radians(least(85.05112878, greatest(-85.05112878, ?))) / 2.0)) / pi() + 1.0) / 2.0) * ?)::bigint",
constant(^world_half), point.public_latitude,
constant(^cell_size) constant(^cell_count)
),
:cell_y
) )
}) })
|> order_by([point], |> order_by([_point], asc: selected_as(:cell_x), asc: selected_as(:cell_y))
asc:
fragment(
"floor((ST_X(ST_Transform(?, 3857)) + ?) / ?)::bigint",
point.public_location,
constant(^world_half),
constant(^cell_size)
),
asc:
fragment(
"floor((ST_Y(ST_Transform(?, 3857)) + ?) / ?)::bigint",
point.public_location,
constant(^world_half),
constant(^cell_size)
)
)
|> Repo.all() |> Repo.all()
|> hydrate_activity_map_singletons()
|> Enum.map(&map_discovery_item(&1, level)) |> Enum.map(&map_discovery_item(&1, level))
end end
@ -894,28 +844,15 @@ defmodule WhoNeedHelp.Activities do
^condition or ^condition or
fragment( fragment(
""" """
ST_Intersects( ? >= ? AND ? <= ? AND ? >= ? AND ? <= ?
CASE
WHEN ? @> '{"scale_fixture": true}'::jsonb THEN ?
ELSE
ST_SetSRID(
ST_MakePoint(
round(ST_X(?)::numeric, 2)::double precision,
round(ST_Y(?)::numeric, 2)::double precision
),
4326
)
END,
ST_MakeEnvelope(?, ?, ?, ?, 4326)
)
""", """,
activity.structured_data, activity.public_longitude,
activity.location,
activity.location,
activity.location,
^west, ^west,
^south, activity.public_longitude,
^east, ^east,
activity.public_latitude,
^south,
activity.public_latitude,
^north ^north
) )
) )
@ -1022,12 +959,12 @@ defmodule WhoNeedHelp.Activities do
hierarchy_level: level, hierarchy_level: level,
point_count: row.count, point_count: row.count,
expansion_zoom: expansion_zoom:
DiscoveryCluster.expansion_zoom( DiscoveryCluster.expansion_zoom_for_bounds(
level, level,
row.west_m, row.west,
row.south_m, row.south,
row.east_m, row.east,
row.north_m row.north
), ),
count: row.count, count: row.count,
latitude: latitude, latitude: latitude,
@ -1041,6 +978,36 @@ defmodule WhoNeedHelp.Activities do
} }
end end
defp hydrate_activity_map_singletons(rows) do
singleton_ids =
rows
|> Enum.filter(&(&1.count == 1))
|> Enum.map(& &1.activity_id)
details = activity_map_singleton_details(singleton_ids)
Enum.map(rows, fn
%{count: 1, activity_id: activity_id} = row ->
Map.merge(row, Map.fetch!(details, activity_id))
row ->
row
end)
end
defp activity_map_singleton_details([]), do: %{}
defp activity_map_singleton_details(singleton_ids) do
Activity
|> where([activity], activity.id in ^singleton_ids)
|> select([activity], {
activity.id,
%{title: activity.title, location_label: activity.location_label}
})
|> Repo.all()
|> Map.new(fn {id, detail} -> {to_string(id), detail} end)
end
defp escape_like(value) do defp escape_like(value) do
value value
|> String.replace("\\", "\\\\") |> String.replace("\\", "\\\\")

View File

@ -11,6 +11,8 @@ defmodule WhoNeedHelp.Activities.Activity do
field :structured_data, :map, default: %{} field :structured_data, :map, default: %{}
field :location_label, :string field :location_label, :string
field :location, Geo.PostGIS.Geometry field :location, Geo.PostGIS.Geometry
field :public_longitude, :float
field :public_latitude, :float
field :status, Ecto.Enum, values: [:open, :completed, :cancelled], default: :open field :status, Ecto.Enum, values: [:open, :completed, :cancelled], default: :open
field :location_visibility, Ecto.Enum, field :location_visibility, Ecto.Enum,

View File

@ -73,8 +73,7 @@ defmodule WhoNeedHelp.Help do
def map_discovery_items(%Scope{user: user}, filters, %DiscoveryViewport{} = viewport) do def map_discovery_items(%Scope{user: user}, filters, %DiscoveryViewport{} = viewport) do
level = DiscoveryCluster.level_for_zoom(viewport.zoom) level = DiscoveryCluster.level_for_zoom(viewport.zoom)
cell_size = DiscoveryCluster.cell_size_meters(level) cell_count = Integer.pow(2, level) * 1.0
world_half = DiscoveryCluster.world_half_meters()
points = points =
user user
@ -86,106 +85,46 @@ defmodule WhoNeedHelp.Help do
|> filter_request_map_cluster_viewport(viewport, level) |> filter_request_map_cluster_viewport(viewport, level)
|> select([request], %{ |> select([request], %{
id: request.id, id: request.id,
title: request.title,
location_label: request.location_label,
location_visibility: request.location_visibility,
radius_meters: request.location_radius_meters,
urgency: request.urgency, urgency: request.urgency,
public_location: public_longitude: request.public_longitude,
fragment( public_latitude: request.public_latitude
"""
CASE
WHEN ? @> '{"scale_fixture": true}'::jsonb THEN ?
WHEN ? = 'exact_for_active_match' THEN
ST_SetSRID(
ST_MakePoint(
round(ST_X(?)::numeric, 2)::double precision,
round(ST_Y(?)::numeric, 2)::double precision
),
4326
)
ELSE ?
END
""",
request.structured_data,
request.location,
request.location_visibility,
request.location,
request.location,
request.location
)
}) })
points points
|> subquery() |> subquery()
|> group_by([point], [ |> group_by([_point], [selected_as(:cell_x), selected_as(:cell_y)])
fragment(
"floor((ST_X(ST_Transform(?, 3857)) + ?) / ?)::bigint",
point.public_location,
constant(^world_half),
constant(^cell_size)
),
fragment(
"floor((ST_Y(ST_Transform(?, 3857)) + ?) / ?)::bigint",
point.public_location,
constant(^world_half),
constant(^cell_size)
)
])
|> select([point], %{ |> select([point], %{
count: count(point.id), count: count(point.id),
urgent_count: fragment("count(*) FILTER (WHERE ? = 'now')::bigint", point.urgency), urgent_count: fragment("count(*) FILTER (WHERE ? = 'now')::bigint", point.urgency),
request_id: fragment("CASE WHEN count(*) = 1 THEN min(?::text) END", point.id), request_id: fragment("min(?::text)", point.id),
title: fragment("CASE WHEN count(*) = 1 THEN min(?) END", point.title), longitude: min(point.public_longitude),
location_label: fragment("CASE WHEN count(*) = 1 THEN min(?) END", point.location_label), latitude: min(point.public_latitude),
location_visibility: west: min(point.public_longitude),
fragment( south: min(point.public_latitude),
"CASE WHEN count(*) = 1 THEN min(?::text) END", east: max(point.public_longitude),
point.location_visibility north: max(point.public_latitude),
),
radius_meters: fragment("CASE WHEN count(*) = 1 THEN max(?) END", point.radius_meters),
longitude: fragment("avg(ST_X(?))", point.public_location),
latitude: fragment("avg(ST_Y(?))", point.public_location),
west: fragment("min(ST_X(?))", point.public_location),
south: fragment("min(ST_Y(?))", point.public_location),
east: fragment("max(ST_X(?))", point.public_location),
north: fragment("max(ST_Y(?))", point.public_location),
west_m: fragment("min(ST_X(ST_Transform(?, 3857)))", point.public_location),
south_m: fragment("min(ST_Y(ST_Transform(?, 3857)))", point.public_location),
east_m: fragment("max(ST_X(ST_Transform(?, 3857)))", point.public_location),
north_m: fragment("max(ST_Y(ST_Transform(?, 3857)))", point.public_location),
cell_x: cell_x:
fragment( selected_as(
"floor((ST_X(ST_Transform(?, 3857)) + ?) / ?)::bigint", fragment(
point.public_location, "floor(((? + 180.0) / 360.0) * ?)::bigint",
constant(^world_half), point.public_longitude,
constant(^cell_size) constant(^cell_count)
),
:cell_x
), ),
cell_y: cell_y:
fragment( selected_as(
"floor((ST_Y(ST_Transform(?, 3857)) + ?) / ?)::bigint", fragment(
point.public_location, "floor(((ln(tan(pi() / 4.0 + radians(least(85.05112878, greatest(-85.05112878, ?))) / 2.0)) / pi() + 1.0) / 2.0) * ?)::bigint",
constant(^world_half), point.public_latitude,
constant(^cell_size) constant(^cell_count)
),
:cell_y
) )
}) })
|> order_by([point], |> order_by([_point], asc: selected_as(:cell_x), asc: selected_as(:cell_y))
asc:
fragment(
"floor((ST_X(ST_Transform(?, 3857)) + ?) / ?)::bigint",
point.public_location,
constant(^world_half),
constant(^cell_size)
),
asc:
fragment(
"floor((ST_Y(ST_Transform(?, 3857)) + ?) / ?)::bigint",
point.public_location,
constant(^world_half),
constant(^cell_size)
)
)
|> Repo.all() |> Repo.all()
|> hydrate_request_map_singletons()
|> Enum.map(&map_discovery_item(&1, level)) |> Enum.map(&map_discovery_item(&1, level))
end end
@ -227,21 +166,7 @@ defmodule WhoNeedHelp.Help do
( (
? && ST_MakeEnvelope(?, ?, ?, ?, 4326) ? && ST_MakeEnvelope(?, ?, ?, ?, 4326)
AND AND
ST_Intersects( ? >= ? AND ? <= ? AND ? >= ? AND ? <= ?
CASE
WHEN ? @> '{"scale_fixture": true}'::jsonb THEN ?
WHEN ? = 'exact_for_active_match' THEN
ST_SetSRID(
ST_MakePoint(
round(ST_X(?)::numeric, 2)::double precision,
round(ST_Y(?)::numeric, 2)::double precision
),
4326
)
ELSE ?
END,
ST_MakeEnvelope(?, ?, ?, ?, 4326)
)
) )
""", """,
request.location, request.location,
@ -249,15 +174,13 @@ defmodule WhoNeedHelp.Help do
^(south - 0.005), ^(south - 0.005),
^(east + 0.005), ^(east + 0.005),
^(north + 0.005), ^(north + 0.005),
request.structured_data, request.public_longitude,
request.location,
request.location_visibility,
request.location,
request.location,
request.location,
^west, ^west,
^south, request.public_longitude,
^east, ^east,
request.public_latitude,
^south,
request.public_latitude,
^north ^north
) )
) )
@ -1273,7 +1196,7 @@ defmodule WhoNeedHelp.Help do
location: row.location_label, location: row.location_label,
latitude: row.latitude, latitude: row.latitude,
longitude: row.longitude, longitude: row.longitude,
exact: row.location_visibility == "exact_public", exact: row.location_visibility in ["exact_public", :exact_public],
radius_meters: row.radius_meters radius_meters: row.radius_meters
} }
end end
@ -1290,12 +1213,12 @@ defmodule WhoNeedHelp.Help do
hierarchy_level: level, hierarchy_level: level,
point_count: row.count, point_count: row.count,
expansion_zoom: expansion_zoom:
DiscoveryCluster.expansion_zoom( DiscoveryCluster.expansion_zoom_for_bounds(
level, level,
row.west_m, row.west,
row.south_m, row.south,
row.east_m, row.east,
row.north_m row.north
), ),
count: row.count, count: row.count,
urgent_count: row.urgent_count, urgent_count: row.urgent_count,
@ -1310,6 +1233,41 @@ defmodule WhoNeedHelp.Help do
} }
end end
defp hydrate_request_map_singletons(rows) do
singleton_ids =
rows
|> Enum.filter(&(&1.count == 1))
|> Enum.map(& &1.request_id)
details = request_map_singleton_details(singleton_ids)
Enum.map(rows, fn
%{count: 1, request_id: request_id} = row ->
Map.merge(row, Map.fetch!(details, request_id))
row ->
row
end)
end
defp request_map_singleton_details([]), do: %{}
defp request_map_singleton_details(singleton_ids) do
HelpRequest
|> where([request], request.id in ^singleton_ids)
|> select([request], {
request.id,
%{
title: request.title,
location_label: request.location_label,
location_visibility: request.location_visibility,
radius_meters: request.location_radius_meters
}
})
|> Repo.all()
|> Map.new(fn {id, detail} -> {to_string(id), detail} end)
end
defp normalize_urgency(value) when value in [:now, "now"], do: {:ok, :now} defp normalize_urgency(value) when value in [:now, "now"], do: {:ok, :now}
defp normalize_urgency(value) when value in [:today, "today"], do: {:ok, :today} defp normalize_urgency(value) when value in [:today, "today"], do: {:ok, :today}
defp normalize_urgency(value) when value in [:scheduled, "scheduled"], do: {:ok, :scheduled} defp normalize_urgency(value) when value in [:scheduled, "scheduled"], do: {:ok, :scheduled}

View File

@ -124,6 +124,27 @@ defmodule WhoNeedHelp.Help.DiscoveryCluster do
|> clamp(0, @maximum_map_zoom) |> clamp(0, @maximum_map_zoom)
end end
@doc """
Returns the first expansion zoom for member bounds expressed as WGS84
longitude and latitude values.
Discovery aggregation already calculates geographic bounds. Projecting the
handful of returned clusters in Elixir avoids repeating `ST_Transform` for
every matching database row while preserving the same Web Mercator
hierarchy.
"""
def expansion_zoom_for_bounds(level, west, south, east, north)
when is_integer(level) and is_number(west) and is_number(south) and is_number(east) and
is_number(north) do
expansion_zoom(
level,
longitude_to_mercator_x(west),
latitude_to_mercator_y(south),
longitude_to_mercator_x(east),
latitude_to_mercator_y(north)
)
end
defp cell_index(coordinate, level) do defp cell_index(coordinate, level) do
coordinate coordinate
|> Kernel.+(@world_half_meters) |> Kernel.+(@world_half_meters)

View File

@ -12,6 +12,8 @@ defmodule WhoNeedHelp.Help.HelpRequest do
field :structured_data, :map, default: %{} field :structured_data, :map, default: %{}
field :location_label, :string field :location_label, :string
field :location, Geo.PostGIS.Geometry field :location, Geo.PostGIS.Geometry
field :public_longitude, :float
field :public_latitude, :float
field :location_radius_meters, :integer, default: 1_000 field :location_radius_meters, :integer, default: 1_000
field :status, Ecto.Enum, field :status, Ecto.Enum,

View File

@ -24,7 +24,7 @@ defmodule WhoNeedHelpWeb.ActivityLive.Index do
|> assign(:discovery_mode, "nearby") |> assign(:discovery_mode, "nearby")
|> assign(:view_mode, "split") |> assign(:view_mode, "split")
|> assign(:view_explicit, false) |> assign(:view_explicit, false)
|> assign(:map_update_mode, "manual") |> assign(:map_update_mode, "auto")
|> assign(:viewport, nil) |> assign(:viewport, nil)
|> assign(:pending_viewport, nil) |> assign(:pending_viewport, nil)
|> assign(:activities, []) |> assign(:activities, [])
@ -402,7 +402,7 @@ defmodule WhoNeedHelpWeb.ActivityLive.Index do
defp normalize_view_mode(_view_mode), do: "split" defp normalize_view_mode(_view_mode), do: "split"
defp normalize_map_update_mode(mode) when mode in @map_update_modes, do: mode defp normalize_map_update_mode(mode) when mode in @map_update_modes, do: mode
defp normalize_map_update_mode(_mode), do: "manual" defp normalize_map_update_mode(_mode), do: "auto"
defp activity_path(socket, overrides) do defp activity_path(socket, overrides) do
filters = Keyword.get(overrides, :filters, socket.assigns.filters) filters = Keyword.get(overrides, :filters, socket.assigns.filters)
@ -427,8 +427,8 @@ defmodule WhoNeedHelpWeb.ActivityLive.Index do
) )
) )
|> then(fn params -> |> then(fn params ->
if map_update_mode == "auto", if map_update_mode == "manual",
do: Map.put(params, "map_update", "auto"), do: Map.put(params, "map_update", "manual"),
else: params else: params
end) end)

View File

@ -31,7 +31,7 @@ defmodule WhoNeedHelpWeb.RequestLive.Index do
|> assign(:discovery_mode, "nearby") |> assign(:discovery_mode, "nearby")
|> assign(:view_mode, "split") |> assign(:view_mode, "split")
|> assign(:view_explicit, false) |> assign(:view_explicit, false)
|> assign(:map_update_mode, "manual") |> assign(:map_update_mode, "auto")
|> assign(:viewport, nil) |> assign(:viewport, nil)
|> assign(:pending_viewport, nil) |> assign(:pending_viewport, nil)
|> assign(:discovery_topics, []) |> assign(:discovery_topics, [])
@ -379,7 +379,7 @@ defmodule WhoNeedHelpWeb.RequestLive.Index do
defp normalize_view_mode(_view_mode), do: "split" defp normalize_view_mode(_view_mode), do: "split"
defp normalize_map_update_mode(mode) when mode in @map_update_modes, do: mode defp normalize_map_update_mode(mode) when mode in @map_update_modes, do: mode
defp normalize_map_update_mode(_mode), do: "manual" defp normalize_map_update_mode(_mode), do: "auto"
defp normalize_discovery_mode(discovery_mode) when discovery_mode in @discovery_modes, defp normalize_discovery_mode(discovery_mode) when discovery_mode in @discovery_modes,
do: discovery_mode do: discovery_mode
@ -413,8 +413,8 @@ defmodule WhoNeedHelpWeb.RequestLive.Index do
) )
) )
|> then(fn params -> |> then(fn params ->
if map_update_mode == "auto", if map_update_mode == "manual",
do: Map.put(params, "map_update", "auto"), do: Map.put(params, "map_update", "manual"),
else: params else: params
end) end)
end end

View File

@ -0,0 +1,57 @@
defmodule WhoNeedHelp.Repo.Migrations.AddPublicDiscoveryCoordinates do
use Ecto.Migration
def change do
alter table(:help_requests) do
add :public_longitude, :float,
generated: """
ALWAYS AS (
CASE
WHEN location IS NULL OR location_visibility = 'hidden' THEN NULL
WHEN structured_data @> '{"scale_fixture": true}'::jsonb THEN ST_X(location)
WHEN location_visibility = 'exact_for_active_match' THEN
round(ST_X(location)::numeric, 2)::double precision
ELSE ST_X(location)
END
) STORED
"""
add :public_latitude, :float,
generated: """
ALWAYS AS (
CASE
WHEN location IS NULL OR location_visibility = 'hidden' THEN NULL
WHEN structured_data @> '{"scale_fixture": true}'::jsonb THEN ST_Y(location)
WHEN location_visibility = 'exact_for_active_match' THEN
round(ST_Y(location)::numeric, 2)::double precision
ELSE ST_Y(location)
END
) STORED
"""
end
alter table(:activities) do
add :public_longitude, :float,
generated: """
ALWAYS AS (
CASE
WHEN location IS NULL OR location_visibility = 'hidden' THEN NULL
WHEN structured_data @> '{"scale_fixture": true}'::jsonb THEN ST_X(location)
ELSE round(ST_X(location)::numeric, 2)::double precision
END
) STORED
"""
add :public_latitude, :float,
generated: """
ALWAYS AS (
CASE
WHEN location IS NULL OR location_visibility = 'hidden' THEN NULL
WHEN structured_data @> '{"scale_fixture": true}'::jsonb THEN ST_Y(location)
ELSE round(ST_Y(location)::numeric, 2)::double precision
END
) STORED
"""
end
end
end

View File

@ -76,4 +76,22 @@ defmodule WhoNeedHelp.Help.DiscoveryClusterTest do
assert DiscoveryCluster.expansion_zoom(level, west, south, west, south) == 22 assert DiscoveryCluster.expansion_zoom(level, west, south, west, south) == 22
assert DiscoveryCluster.expansion_zoom(24, west, south, west, south) == 22 assert DiscoveryCluster.expansion_zoom(24, west, south, west, south) == 22
end end
test "geographic bounds preserve Web Mercator expansion levels" do
assert DiscoveryCluster.expansion_zoom_for_bounds(
7,
30.50,
50.40,
30.60,
50.50
) == 8
assert DiscoveryCluster.expansion_zoom_for_bounds(
7,
30.50,
50.40,
30.50,
50.40
) == 22
end
end end

View File

@ -385,7 +385,8 @@ defmodule WhoNeedHelpWeb.MutualAidLiveTest do
} do } do
category = Catalog.seed_defaults() category = Catalog.seed_defaults()
{:ok, request} = Help.create_request(scope, request_attrs(category)) {:ok, request} = Help.create_request(scope, request_attrs(category))
{:ok, view, _html} = live(conn, requests_in_kyiv_path()) manual_path = requests_in_kyiv_path() <> "&map_update=manual"
{:ok, view, _html} = live(conn, manual_path)
render_async(view) render_async(view)
@ -413,7 +414,7 @@ defmodule WhoNeedHelpWeb.MutualAidLiveTest do
assert_patch( assert_patch(
view, view,
~p"/requests?#{Map.merge(moved, %{"east" => "30.6", "north" => "50.5", "south" => "50.4", "view" => "split", "west" => "30.5"})}" ~p"/requests?#{Map.merge(moved, %{"east" => "30.6", "map_update" => "manual", "north" => "50.5", "south" => "50.4", "view" => "split", "west" => "30.5"})}"
) )
render_async(view) render_async(view)
@ -421,7 +422,7 @@ defmodule WhoNeedHelpWeb.MutualAidLiveTest do
test "request discovery can update automatically after map movement", %{conn: conn} do test "request discovery can update automatically after map movement", %{conn: conn} do
path = path =
~p"/requests?#{%{west: 30, south: 50, east: 31, north: 51, zoom: 10, width: 1200, height: 720, view: "split", map_update: "auto"}}" ~p"/requests?#{%{west: 30, south: 50, east: 31, north: 51, zoom: 10, width: 1200, height: 720, view: "split"}}"
{:ok, view, _html} = live(conn, path) {:ok, view, _html} = live(conn, path)
render_async(view) render_async(view)
@ -444,7 +445,7 @@ defmodule WhoNeedHelpWeb.MutualAidLiveTest do
assert_patch( assert_patch(
view, view,
~p"/requests?#{Map.merge(moved, %{"east" => "30.6", "map_update" => "auto", "north" => "50.5", "south" => "50.4", "view" => "split", "west" => "30.5"})}", ~p"/requests?#{Map.merge(moved, %{"east" => "30.6", "north" => "50.5", "south" => "50.4", "view" => "split", "west" => "30.5"})}",
1_000 1_000
) )
@ -454,6 +455,8 @@ defmodule WhoNeedHelpWeb.MutualAidLiveTest do
test "activity cluster expansion immediately searches the expanded viewport", %{conn: conn} do test "activity cluster expansion immediately searches the expanded viewport", %{conn: conn} do
{:ok, view, _html} = live(conn, activities_in_kyiv_path()) {:ok, view, _html} = live(conn, activities_in_kyiv_path())
assert has_element?(view, "#activity-map-auto-update[checked]")
expanded = %{ expanded = %{
"west" => "30.40", "west" => "30.40",
"south" => "50.35", "south" => "50.35",