Diversify scale fixture geography and privacy
This commit is contained in:
parent
4138d7bfef
commit
61ad7923ca
|
|
@ -1,7 +1,7 @@
|
||||||
import maplibregl from "maplibre-gl"
|
import maplibregl from "maplibre-gl"
|
||||||
import {
|
import {
|
||||||
clusterSizeClass,
|
clusterSizeClass,
|
||||||
mergeNearbyMapClusters
|
mergeNearbyMapPoints
|
||||||
} from "./map_cluster_points.mjs"
|
} from "./map_cluster_points.mjs"
|
||||||
|
|
||||||
const trackingMinTimeMs = 5000
|
const trackingMinTimeMs = 5000
|
||||||
|
|
@ -524,21 +524,19 @@ const createAidMap = element => {
|
||||||
state.markerElements.clear()
|
state.markerElements.clear()
|
||||||
|
|
||||||
const points = markerPoints(state.element)
|
const points = markerPoints(state.element)
|
||||||
const exactPoints = points.filter(
|
const displayPoints = mergeNearbyMapPoints(state.map, points)
|
||||||
|
const exactPoints = displayPoints.filter(
|
||||||
point => point.type !== "cluster" && (point.exact || !Number.isFinite(point.radius_meters))
|
point => point.type !== "cluster" && (point.exact || !Number.isFinite(point.radius_meters))
|
||||||
)
|
)
|
||||||
const clusterPoints = mergeNearbyMapClusters(
|
const clusterPoints = displayPoints.filter(point => point.type === "cluster")
|
||||||
state.map,
|
const areaFeatures = areaFeatureCollection(displayPoints)
|
||||||
points.filter(point => point.type === "cluster")
|
|
||||||
)
|
|
||||||
const areaFeatures = areaFeatureCollection(points)
|
|
||||||
const clusterFeatures = clusterFeatureCollection(clusterPoints)
|
const clusterFeatures = clusterFeatureCollection(clusterPoints)
|
||||||
const bounds = new maplibregl.LngLatBounds()
|
const bounds = new maplibregl.LngLatBounds()
|
||||||
|
|
||||||
exactPoints.forEach(point => bounds.extend([point.longitude, point.latitude]))
|
exactPoints.forEach(point => bounds.extend([point.longitude, point.latitude]))
|
||||||
areaFeatures.features.forEach(feature => extendBoundsWithFeature(bounds, feature))
|
areaFeatures.features.forEach(feature => extendBoundsWithFeature(bounds, feature))
|
||||||
clusterFeatures.features.forEach(feature => bounds.extend(feature.geometry.coordinates))
|
clusterFeatures.features.forEach(feature => bounds.extend(feature.geometry.coordinates))
|
||||||
state.locationBounds = points.length > 0 ? bounds : null
|
state.locationBounds = displayPoints.length > 0 ? bounds : null
|
||||||
|
|
||||||
if (state.map.isStyleLoaded()) {
|
if (state.map.isStyleLoaded()) {
|
||||||
const areaSource = state.map.getSource(state.areaSourceId)
|
const areaSource = state.map.getSource(state.areaSourceId)
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ const coordinatesFromPixel = (map, pixel) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const mergeNearbyMapClusters = (map, points, options = {}) => {
|
const nearbyMapPointGroups = (map, points, options = {}) => {
|
||||||
const gap = Number.isFinite(options.gap) ? Math.max(options.gap, 0) : 8
|
const gap = Number.isFinite(options.gap) ? Math.max(options.gap, 0) : 8
|
||||||
|
|
||||||
const groups = points.map(point => {
|
const groups = points.map(point => {
|
||||||
|
|
@ -76,7 +76,10 @@ export const mergeNearbyMapClusters = (map, points, options = {}) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return groups.map(group => {
|
return groups
|
||||||
|
}
|
||||||
|
|
||||||
|
const visualCluster = (map, group) => {
|
||||||
const coordinates = coordinatesFromPixel(map, group)
|
const coordinates = coordinatesFromPixel(map, group)
|
||||||
const firstId = group.points[0]?.id || "anonymous"
|
const firstId = group.points[0]?.id || "anonymous"
|
||||||
|
|
||||||
|
|
@ -88,5 +91,17 @@ export const mergeNearbyMapClusters = (map, points, options = {}) => {
|
||||||
longitude: coordinates.longitude,
|
longitude: coordinates.longitude,
|
||||||
latitude: coordinates.latitude
|
latitude: coordinates.latitude
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const mergeNearbyMapClusters = (map, points, options = {}) =>
|
||||||
|
nearbyMapPointGroups(map, points, options).map(group => visualCluster(map, group))
|
||||||
|
|
||||||
|
export const mergeNearbyMapPoints = (map, points, options = {}) => {
|
||||||
|
return nearbyMapPointGroups(map, points, options).map(group => {
|
||||||
|
const point = group.points[0]
|
||||||
|
|
||||||
|
if (group.points.length === 1 && point?.type !== "cluster") return point
|
||||||
|
|
||||||
|
return visualCluster(map, group)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,8 @@ import test from "node:test"
|
||||||
import {
|
import {
|
||||||
clusterDiameterPixels,
|
clusterDiameterPixels,
|
||||||
clusterSizeClass,
|
clusterSizeClass,
|
||||||
mergeNearbyMapClusters
|
mergeNearbyMapClusters,
|
||||||
|
mergeNearbyMapPoints
|
||||||
} from "./map_cluster_points.mjs"
|
} from "./map_cluster_points.mjs"
|
||||||
|
|
||||||
const flatMap = scale => ({
|
const flatMap = scale => ({
|
||||||
|
|
@ -40,6 +41,36 @@ test("splits the same clusters again at a closer zoom", () => {
|
||||||
assert.equal(mergeNearbyMapClusters(flatMap(3), points).length, 2)
|
assert.equal(mergeNearbyMapClusters(flatMap(3), points).length, 2)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("merges a singleton into an overlapping cluster at a distant zoom", () => {
|
||||||
|
const points = [
|
||||||
|
{id: "cluster", type: "cluster", count: 351, longitude: 0, latitude: 0},
|
||||||
|
{id: "request", type: "request", longitude: 28, latitude: 0, exact: true}
|
||||||
|
]
|
||||||
|
|
||||||
|
const result = mergeNearbyMapPoints(flatMap(1), points)
|
||||||
|
|
||||||
|
assert.equal(result.length, 1)
|
||||||
|
assert.equal(result[0].type, "cluster")
|
||||||
|
assert.equal(result[0].count, 352)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("keeps separated singleton points interactive at a closer zoom", () => {
|
||||||
|
const points = [
|
||||||
|
{id: "left", type: "request", longitude: 0, latitude: 0, exact: true},
|
||||||
|
{
|
||||||
|
id: "right",
|
||||||
|
type: "request",
|
||||||
|
longitude: 30,
|
||||||
|
latitude: 0,
|
||||||
|
radius_meters: 1_000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const result = mergeNearbyMapPoints(flatMap(3), points)
|
||||||
|
|
||||||
|
assert.deepEqual(result, points)
|
||||||
|
})
|
||||||
|
|
||||||
test("uses larger non-overlapping markers for larger totals", () => {
|
test("uses larger non-overlapping markers for larger totals", () => {
|
||||||
assert.equal(clusterDiameterPixels(999), 44)
|
assert.equal(clusterDiameterPixels(999), 44)
|
||||||
assert.equal(clusterDiameterPixels(1_000), 52)
|
assert.equal(clusterDiameterPixels(1_000), 52)
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -65,7 +65,21 @@ defmodule WhoNeedHelp.Activities do
|
||||||
location_label: activity.location_label,
|
location_label: activity.location_label,
|
||||||
public_location:
|
public_location:
|
||||||
fragment(
|
fragment(
|
||||||
"ST_SetSRID(ST_MakePoint(round(ST_X(?)::numeric, 2)::double precision, round(ST_Y(?)::numeric, 2)::double precision), 4326)",
|
"""
|
||||||
|
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,
|
||||||
activity.location
|
activity.location
|
||||||
)
|
)
|
||||||
|
|
@ -849,16 +863,22 @@ defmodule WhoNeedHelp.Activities do
|
||||||
fragment(
|
fragment(
|
||||||
"""
|
"""
|
||||||
ST_Intersects(
|
ST_Intersects(
|
||||||
|
CASE
|
||||||
|
WHEN ? @> '{"scale_fixture": true}'::jsonb THEN ?
|
||||||
|
ELSE
|
||||||
ST_SetSRID(
|
ST_SetSRID(
|
||||||
ST_MakePoint(
|
ST_MakePoint(
|
||||||
round(ST_X(?)::numeric, 2)::double precision,
|
round(ST_X(?)::numeric, 2)::double precision,
|
||||||
round(ST_Y(?)::numeric, 2)::double precision
|
round(ST_Y(?)::numeric, 2)::double precision
|
||||||
),
|
),
|
||||||
4326
|
4326
|
||||||
),
|
)
|
||||||
|
END,
|
||||||
ST_MakeEnvelope(?, ?, ?, ?, 4326)
|
ST_MakeEnvelope(?, ?, ?, ?, 4326)
|
||||||
)
|
)
|
||||||
""",
|
""",
|
||||||
|
activity.structured_data,
|
||||||
|
activity.location,
|
||||||
activity.location,
|
activity.location,
|
||||||
activity.location,
|
activity.location,
|
||||||
^west,
|
^west,
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,22 @@ defmodule WhoNeedHelp.Help do
|
||||||
urgency: request.urgency,
|
urgency: request.urgency,
|
||||||
public_location:
|
public_location:
|
||||||
fragment(
|
fragment(
|
||||||
"CASE 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",
|
"""
|
||||||
|
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_visibility,
|
||||||
request.location,
|
request.location,
|
||||||
request.location,
|
request.location,
|
||||||
|
|
@ -172,6 +187,7 @@ defmodule WhoNeedHelp.Help do
|
||||||
AND
|
AND
|
||||||
ST_Intersects(
|
ST_Intersects(
|
||||||
CASE
|
CASE
|
||||||
|
WHEN ? @> '{"scale_fixture": true}'::jsonb THEN ?
|
||||||
WHEN ? = 'exact_for_active_match' THEN
|
WHEN ? = 'exact_for_active_match' THEN
|
||||||
ST_SetSRID(
|
ST_SetSRID(
|
||||||
ST_MakePoint(
|
ST_MakePoint(
|
||||||
|
|
@ -191,6 +207,8 @@ 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.location,
|
||||||
request.location_visibility,
|
request.location_visibility,
|
||||||
request.location,
|
request.location,
|
||||||
request.location,
|
request.location,
|
||||||
|
|
|
||||||
|
|
@ -457,6 +457,42 @@ defmodule WhoNeedHelp.ActivitiesTest do
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "scale fixture activities keep distinct synthetic public map coordinates", context do
|
||||||
|
activities =
|
||||||
|
for {latitude, index} <- [{"50.451", 1}, {"50.454", 2}] do
|
||||||
|
{:ok, activity} =
|
||||||
|
Activities.create_activity(
|
||||||
|
context.organizer_scope,
|
||||||
|
context.attrs
|
||||||
|
|> Map.put("title", "Synthetic distinct activity #{index}")
|
||||||
|
|> Map.put("latitude", latitude)
|
||||||
|
)
|
||||||
|
|
||||||
|
activity
|
||||||
|
|> Ecto.Changeset.change(
|
||||||
|
structured_data: Map.put(activity.structured_data, "scale_fixture", true)
|
||||||
|
)
|
||||||
|
|> Repo.update!()
|
||||||
|
end
|
||||||
|
|
||||||
|
items =
|
||||||
|
Activities.map_discovery_items(
|
||||||
|
context.outsider_scope,
|
||||||
|
%{},
|
||||||
|
viewport!(30.51, 50.44, 30.54, 50.46, 16)
|
||||||
|
)
|
||||||
|
|
||||||
|
fixture_ids = MapSet.new(activities, & &1.id)
|
||||||
|
|
||||||
|
fixture_items =
|
||||||
|
Enum.filter(items, fn
|
||||||
|
%{type: "activity", id: id} -> MapSet.member?(fixture_ids, id)
|
||||||
|
_item -> false
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert Enum.sort(Enum.map(fixture_items, & &1.latitude)) == [50.451, 50.454]
|
||||||
|
end
|
||||||
|
|
||||||
test "hidden activity is discoverable only through its public area description", context do
|
test "hidden activity is discoverable only through its public area description", context do
|
||||||
{:ok, hidden} =
|
{:ok, hidden} =
|
||||||
Activities.create_activity(
|
Activities.create_activity(
|
||||||
|
|
|
||||||
|
|
@ -603,6 +603,44 @@ defmodule WhoNeedHelp.MutualAidFlowTest do
|
||||||
refute_in_delta item.longitude, 30.5267, 0.000_001
|
refute_in_delta item.longitude, 30.5267, 0.000_001
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "scale fixture requests keep distinct synthetic public map coordinates", context do
|
||||||
|
requests =
|
||||||
|
for {latitude, index} <- [{"50.451", 1}, {"50.454", 2}] do
|
||||||
|
{:ok, request} =
|
||||||
|
Help.create_request(
|
||||||
|
context.requester_scope,
|
||||||
|
context.request_attrs
|
||||||
|
|> Map.put("title", "Synthetic distinct request #{index}")
|
||||||
|
|> Map.put("latitude", latitude)
|
||||||
|
|> Map.put("location_visibility", "exact_for_active_match")
|
||||||
|
)
|
||||||
|
|
||||||
|
request
|
||||||
|
|> Ecto.Changeset.change(
|
||||||
|
structured_data: Map.put(request.structured_data, "scale_fixture", true)
|
||||||
|
)
|
||||||
|
|> Repo.update!()
|
||||||
|
end
|
||||||
|
|
||||||
|
items =
|
||||||
|
Help.map_discovery_items(
|
||||||
|
context.helper_scope,
|
||||||
|
%{},
|
||||||
|
viewport!(30.51, 50.44, 30.54, 50.46, 16)
|
||||||
|
)
|
||||||
|
|
||||||
|
fixture_ids = MapSet.new(requests, & &1.id)
|
||||||
|
|
||||||
|
fixture_items =
|
||||||
|
Enum.filter(items, fn
|
||||||
|
%{type: "request", id: id} -> MapSet.member?(fixture_ids, id)
|
||||||
|
_item -> false
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert Enum.sort(Enum.map(fixture_items, & &1.latitude)) == [50.451, 50.454]
|
||||||
|
assert Enum.all?(fixture_items, &(&1.exact == false))
|
||||||
|
end
|
||||||
|
|
||||||
test "area search treats SQL wildcard characters as literal text", context do
|
test "area search treats SQL wildcard characters as literal text", context do
|
||||||
{:ok, literal_request} =
|
{:ok, literal_request} =
|
||||||
Help.create_request(
|
Help.create_request(
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user