Optimize viewport discovery and enable automatic updates
This commit is contained in:
parent
d3114e211b
commit
afc0040a65
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
selected_as(
|
||||||
fragment(
|
fragment(
|
||||||
"floor((ST_X(ST_Transform(?, 3857)) + ?) / ?)::bigint",
|
"floor(((? + 180.0) / 360.0) * ?)::bigint",
|
||||||
point.public_location,
|
point.public_longitude,
|
||||||
constant(^world_half),
|
constant(^cell_count)
|
||||||
constant(^cell_size)
|
),
|
||||||
|
:cell_x
|
||||||
),
|
),
|
||||||
cell_y:
|
cell_y:
|
||||||
|
selected_as(
|
||||||
fragment(
|
fragment(
|
||||||
"floor((ST_Y(ST_Transform(?, 3857)) + ?) / ?)::bigint",
|
"floor(((ln(tan(pi() / 4.0 + radians(least(85.05112878, greatest(-85.05112878, ?))) / 2.0)) / pi() + 1.0) / 2.0) * ?)::bigint",
|
||||||
point.public_location,
|
point.public_latitude,
|
||||||
constant(^world_half),
|
constant(^cell_count)
|
||||||
constant(^cell_size)
|
),
|
||||||
|
: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("\\", "\\\\")
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
selected_as(
|
||||||
fragment(
|
fragment(
|
||||||
"floor((ST_X(ST_Transform(?, 3857)) + ?) / ?)::bigint",
|
"floor(((? + 180.0) / 360.0) * ?)::bigint",
|
||||||
point.public_location,
|
point.public_longitude,
|
||||||
constant(^world_half),
|
constant(^cell_count)
|
||||||
constant(^cell_size)
|
),
|
||||||
|
:cell_x
|
||||||
),
|
),
|
||||||
cell_y:
|
cell_y:
|
||||||
|
selected_as(
|
||||||
fragment(
|
fragment(
|
||||||
"floor((ST_Y(ST_Transform(?, 3857)) + ?) / ?)::bigint",
|
"floor(((ln(tan(pi() / 4.0 + radians(least(85.05112878, greatest(-85.05112878, ?))) / 2.0)) / pi() + 1.0) / 2.0) * ?)::bigint",
|
||||||
point.public_location,
|
point.public_latitude,
|
||||||
constant(^world_half),
|
constant(^cell_count)
|
||||||
constant(^cell_size)
|
),
|
||||||
|
: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}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user