feat(maps): add location recenter control

This commit is contained in:
SimpleTest 2026-07-25 02:31:56 +03:00
parent bcbe417a79
commit 51de2d829b
10 changed files with 135 additions and 7 deletions

View File

@ -167,6 +167,22 @@ html {
text-underline-offset: 0.12em;
}
.map-recenter-control {
display: grid !important;
place-items: center;
color: #1f2937;
}
.map-recenter-icon {
width: 1.125rem;
height: 1.125rem;
fill: none;
stroke: currentColor;
stroke-linecap: round;
stroke-linejoin: round;
stroke-width: 2;
}
.request-discovery-toolbar {
display: flex;
align-items: center;

View File

@ -147,6 +147,59 @@ const mapRetryButton = (element, retry) => {
return button
}
const mapRecenterControl = (element, recenter) => {
let container = null
let button = null
const handleClick = event => {
event.preventDefault()
recenter()
}
return {
onAdd() {
const label = element.dataset.mapRecenterLabel || "Show location on map"
const iconNamespace = "http://www.w3.org/2000/svg"
container = document.createElement("div")
container.className = "maplibregl-ctrl maplibregl-ctrl-group"
button = document.createElement("button")
button.type = "button"
button.className = "map-recenter-control"
button.dataset.mapRecenter = "true"
button.title = label
button.setAttribute("aria-label", label)
const icon = document.createElementNS(iconNamespace, "svg")
icon.classList.add("map-recenter-icon")
icon.setAttribute("viewBox", "0 0 24 24")
icon.setAttribute("aria-hidden", "true")
const circle = document.createElementNS(iconNamespace, "circle")
circle.setAttribute("cx", "12")
circle.setAttribute("cy", "12")
circle.setAttribute("r", "4.5")
const crosshair = document.createElementNS(iconNamespace, "path")
crosshair.setAttribute("d", "M12 2v4m0 12v4M2 12h4m12 0h4")
icon.append(circle, crosshair)
button.append(icon)
button.addEventListener("click", handleClick)
container.append(button)
return container
},
onRemove() {
button?.removeEventListener("click", handleClick)
container?.remove()
button = null
container = null
}
}
}
const reportMapUnavailable = (element, reason, error = null) => {
element.dataset.mapUnavailable = "true"
element.dataset.mapUnavailableReason = reason
@ -169,6 +222,8 @@ const createAidMap = element => {
markers: [],
markerElements: new Map(),
initializedViewport: false,
locationBounds: null,
recenterControl: null,
areaSourceId: `${element.id || "aid-map"}-areas`,
areaFillId: `${element.id || "aid-map"}-area-fill`,
areaLineId: `${element.id || "aid-map"}-area-line`,
@ -348,6 +403,18 @@ const createAidMap = element => {
)
}
state.fitMarkers = (duration = 0) => {
if (!state.active || !state.map || !state.locationBounds) return false
state.map.fitBounds(state.locationBounds, {
padding: 52,
maxZoom: 13,
duration
})
return true
}
state.renderMarkers = () => {
if (!state.map) return
@ -361,6 +428,12 @@ const createAidMap = element => {
)
const areaFeatures = areaFeatureCollection(points)
const clusterFeatures = clusterFeatureCollection(points)
const bounds = new maplibregl.LngLatBounds()
exactPoints.forEach(point => bounds.extend([point.longitude, point.latitude]))
areaFeatures.features.forEach(feature => extendBoundsWithFeature(bounds, feature))
clusterFeatures.features.forEach(feature => bounds.extend(feature.geometry.coordinates))
state.locationBounds = points.length > 0 ? bounds : null
if (state.map.isStyleLoaded()) {
const areaSource = state.map.getSource(state.areaSourceId)
@ -456,13 +529,7 @@ const createAidMap = element => {
state.markers.push(marker)
})
if (points.length > 0 && state.element.dataset.autoFit !== "false") {
const bounds = new maplibregl.LngLatBounds()
exactPoints.forEach(point => bounds.extend([point.longitude, point.latitude]))
areaFeatures.features.forEach(feature => extendBoundsWithFeature(bounds, feature))
clusterFeatures.features.forEach(feature => bounds.extend(feature.geometry.coordinates))
state.map.fitBounds(bounds, {padding: 52, maxZoom: 13, duration: 0})
}
if (state.element.dataset.autoFit !== "false") state.fitMarkers()
}
state.mount = () => {
@ -501,6 +568,11 @@ const createAidMap = element => {
demoMap ? "bottom-right" : "top-right"
)
if (state.element.dataset.mapRecenterLabel) {
state.recenterControl = mapRecenterControl(state.element, () => state.fitMarkers(450))
state.map.addControl(state.recenterControl, "top-right")
}
if (state.element.dataset.discoveryMap === "true" && navigator.geolocation) {
state.map.addControl(
new maplibregl.GeolocateControl({
@ -572,6 +644,8 @@ const createAidMap = element => {
window.removeEventListener("wnh:request-highlight", state.onRequestHighlight)
state.map?.remove()
state.map = null
state.locationBounds = null
state.recenterControl = null
}
return state

View File

@ -284,6 +284,7 @@ defmodule WhoNeedHelpWeb.ActivityLive.Index do
gettext("The map is unavailable in this browser. Request details remain usable.")
}
data-map-retry-label={gettext("Try map again")}
data-map-recenter-label={gettext("Show location on map")}
class="aid-map"
/>
</aside>

View File

@ -540,6 +540,7 @@ defmodule WhoNeedHelpWeb.ActivityLive.Show do
gettext("The map is unavailable in this browser. Request details remain usable.")
}
data-map-retry-label={gettext("Try map again")}
data-map-recenter-label={gettext("Show location on map")}
class="aid-map"
/>
<div class="rounded-2xl bg-base-200 p-5 text-sm">

View File

@ -1282,6 +1282,7 @@ defmodule WhoNeedHelpWeb.RequestLive.Show do
gettext("The map is unavailable in this browser. Request details remain usable.")
}
data-map-retry-label={gettext("Try map again")}
data-map-recenter-label={gettext("Show location on map")}
class="aid-map"
/>
<div

View File

@ -5421,3 +5421,10 @@ msgstr ""
#, elixir-autogen, elixir-format
msgid "WebGL is unavailable in this browser, so the full map cannot start. Select an example below or retry the map."
msgstr ""
#: lib/who_need_help_web/live/activity_live/index.ex
#: lib/who_need_help_web/live/activity_live/show.ex
#: lib/who_need_help_web/live/request_live/show.ex
#, elixir-autogen, elixir-format
msgid "Show location on map"
msgstr ""

View File

@ -5421,3 +5421,10 @@ msgstr "Selected example:"
#, elixir-autogen, elixir-format
msgid "WebGL is unavailable in this browser, so the full map cannot start. Select an example below or retry the map."
msgstr "WebGL is unavailable in this browser, so the full map cannot start. Select an example below or retry the map."
#: lib/who_need_help_web/live/activity_live/index.ex
#: lib/who_need_help_web/live/activity_live/show.ex
#: lib/who_need_help_web/live/request_live/show.ex
#, elixir-autogen, elixir-format
msgid "Show location on map"
msgstr "Show location on map"

View File

@ -5573,3 +5573,10 @@ msgstr "Выбранный пример:"
#, elixir-autogen, elixir-format
msgid "WebGL is unavailable in this browser, so the full map cannot start. Select an example below or retry the map."
msgstr "WebGL недоступен в этом браузере, поэтому полная карта не может запуститься. Выберите пример ниже или попробуйте запустить карту ещё раз."
#: lib/who_need_help_web/live/activity_live/index.ex
#: lib/who_need_help_web/live/activity_live/show.ex
#: lib/who_need_help_web/live/request_live/show.ex
#, elixir-autogen, elixir-format
msgid "Show location on map"
msgstr "Показать место на карте"

View File

@ -5565,3 +5565,10 @@ msgstr "Вибраний приклад:"
#, elixir-autogen, elixir-format
msgid "WebGL is unavailable in this browser, so the full map cannot start. Select an example below or retry the map."
msgstr "WebGL недоступний у цьому браузері, тому повна карта не може запуститися. Виберіть приклад нижче або спробуйте запустити карту ще раз."
#: lib/who_need_help_web/live/activity_live/index.ex
#: lib/who_need_help_web/live/activity_live/show.ex
#: lib/who_need_help_web/live/request_live/show.ex
#, elixir-autogen, elixir-format
msgid "Show location on map"
msgstr "Показати місце на мапі"

View File

@ -322,6 +322,7 @@ defmodule WhoNeedHelpWeb.MutualAidLiveTest do
|> Enum.find(&(&1.slug == "coffee-meetup"))
{:ok, view, _html} = build_conn() |> log_in_user(viewer) |> live(~p"/activities")
assert has_element?(view, "#activity-map[data-map-recenter-label='Show location on map']")
{:ok, _activity} =
Activities.create_activity(Accounts.Scope.for_user(organizer), %{
@ -661,6 +662,11 @@ defmodule WhoNeedHelpWeb.MutualAidLiveTest do
{:ok, activity} = Activities.create_activity(organizer_scope, attrs)
{:ok, organizer_view, _html} = live(organizer_conn, ~p"/activities/#{activity.id}")
{:ok, participant_view, html} = live(participant_conn, ~p"/activities/#{activity.id}")
assert has_element?(
participant_view,
"#activity-detail-map[data-map-recenter-label='Show location on map']"
)
assert html =~ "Request to join"
assert html =~ "Safety controls"
assert html =~ "Block organizer"
@ -783,6 +789,7 @@ defmodule WhoNeedHelpWeb.MutualAidLiveTest do
{:ok, request} = Help.create_request(requester_scope, request_attrs(category))
{:ok, requester_view, _html} = live(requester_conn, ~p"/requests/#{request.id}")
{:ok, helper_view, _html} = live(helper_conn, ~p"/requests/#{request.id}")
assert has_element?(requester_view, "#show-map[data-map-recenter-label='Show location on map']")
helper_view
|> element("button[phx-click='accept']")