feat(maps): add location recenter control
This commit is contained in:
parent
bcbe417a79
commit
51de2d829b
|
|
@ -167,6 +167,22 @@ html {
|
||||||
text-underline-offset: 0.12em;
|
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 {
|
.request-discovery-toolbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
||||||
|
|
@ -147,6 +147,59 @@ const mapRetryButton = (element, retry) => {
|
||||||
return button
|
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) => {
|
const reportMapUnavailable = (element, reason, error = null) => {
|
||||||
element.dataset.mapUnavailable = "true"
|
element.dataset.mapUnavailable = "true"
|
||||||
element.dataset.mapUnavailableReason = reason
|
element.dataset.mapUnavailableReason = reason
|
||||||
|
|
@ -169,6 +222,8 @@ const createAidMap = element => {
|
||||||
markers: [],
|
markers: [],
|
||||||
markerElements: new Map(),
|
markerElements: new Map(),
|
||||||
initializedViewport: false,
|
initializedViewport: false,
|
||||||
|
locationBounds: null,
|
||||||
|
recenterControl: null,
|
||||||
areaSourceId: `${element.id || "aid-map"}-areas`,
|
areaSourceId: `${element.id || "aid-map"}-areas`,
|
||||||
areaFillId: `${element.id || "aid-map"}-area-fill`,
|
areaFillId: `${element.id || "aid-map"}-area-fill`,
|
||||||
areaLineId: `${element.id || "aid-map"}-area-line`,
|
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 = () => {
|
state.renderMarkers = () => {
|
||||||
if (!state.map) return
|
if (!state.map) return
|
||||||
|
|
||||||
|
|
@ -361,6 +428,12 @@ const createAidMap = element => {
|
||||||
)
|
)
|
||||||
const areaFeatures = areaFeatureCollection(points)
|
const areaFeatures = areaFeatureCollection(points)
|
||||||
const clusterFeatures = clusterFeatureCollection(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()) {
|
if (state.map.isStyleLoaded()) {
|
||||||
const areaSource = state.map.getSource(state.areaSourceId)
|
const areaSource = state.map.getSource(state.areaSourceId)
|
||||||
|
|
@ -456,13 +529,7 @@ const createAidMap = element => {
|
||||||
state.markers.push(marker)
|
state.markers.push(marker)
|
||||||
})
|
})
|
||||||
|
|
||||||
if (points.length > 0 && state.element.dataset.autoFit !== "false") {
|
if (state.element.dataset.autoFit !== "false") state.fitMarkers()
|
||||||
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})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
state.mount = () => {
|
state.mount = () => {
|
||||||
|
|
@ -501,6 +568,11 @@ const createAidMap = element => {
|
||||||
demoMap ? "bottom-right" : "top-right"
|
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) {
|
if (state.element.dataset.discoveryMap === "true" && navigator.geolocation) {
|
||||||
state.map.addControl(
|
state.map.addControl(
|
||||||
new maplibregl.GeolocateControl({
|
new maplibregl.GeolocateControl({
|
||||||
|
|
@ -572,6 +644,8 @@ const createAidMap = element => {
|
||||||
window.removeEventListener("wnh:request-highlight", state.onRequestHighlight)
|
window.removeEventListener("wnh:request-highlight", state.onRequestHighlight)
|
||||||
state.map?.remove()
|
state.map?.remove()
|
||||||
state.map = null
|
state.map = null
|
||||||
|
state.locationBounds = null
|
||||||
|
state.recenterControl = null
|
||||||
}
|
}
|
||||||
|
|
||||||
return state
|
return state
|
||||||
|
|
|
||||||
|
|
@ -284,6 +284,7 @@ defmodule WhoNeedHelpWeb.ActivityLive.Index do
|
||||||
gettext("The map is unavailable in this browser. Request details remain usable.")
|
gettext("The map is unavailable in this browser. Request details remain usable.")
|
||||||
}
|
}
|
||||||
data-map-retry-label={gettext("Try map again")}
|
data-map-retry-label={gettext("Try map again")}
|
||||||
|
data-map-recenter-label={gettext("Show location on map")}
|
||||||
class="aid-map"
|
class="aid-map"
|
||||||
/>
|
/>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
|
||||||
|
|
@ -540,6 +540,7 @@ defmodule WhoNeedHelpWeb.ActivityLive.Show do
|
||||||
gettext("The map is unavailable in this browser. Request details remain usable.")
|
gettext("The map is unavailable in this browser. Request details remain usable.")
|
||||||
}
|
}
|
||||||
data-map-retry-label={gettext("Try map again")}
|
data-map-retry-label={gettext("Try map again")}
|
||||||
|
data-map-recenter-label={gettext("Show location on map")}
|
||||||
class="aid-map"
|
class="aid-map"
|
||||||
/>
|
/>
|
||||||
<div class="rounded-2xl bg-base-200 p-5 text-sm">
|
<div class="rounded-2xl bg-base-200 p-5 text-sm">
|
||||||
|
|
|
||||||
|
|
@ -1282,6 +1282,7 @@ defmodule WhoNeedHelpWeb.RequestLive.Show do
|
||||||
gettext("The map is unavailable in this browser. Request details remain usable.")
|
gettext("The map is unavailable in this browser. Request details remain usable.")
|
||||||
}
|
}
|
||||||
data-map-retry-label={gettext("Try map again")}
|
data-map-retry-label={gettext("Try map again")}
|
||||||
|
data-map-recenter-label={gettext("Show location on map")}
|
||||||
class="aid-map"
|
class="aid-map"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
|
|
@ -5421,3 +5421,10 @@ msgstr ""
|
||||||
#, elixir-autogen, elixir-format
|
#, 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."
|
msgid "WebGL is unavailable in this browser, so the full map cannot start. Select an example below or retry the map."
|
||||||
msgstr ""
|
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 ""
|
||||||
|
|
|
||||||
|
|
@ -5421,3 +5421,10 @@ msgstr "Selected example:"
|
||||||
#, elixir-autogen, elixir-format
|
#, 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."
|
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."
|
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"
|
||||||
|
|
|
||||||
|
|
@ -5573,3 +5573,10 @@ msgstr "Выбранный пример:"
|
||||||
#, elixir-autogen, elixir-format
|
#, 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."
|
msgid "WebGL is unavailable in this browser, so the full map cannot start. Select an example below or retry the map."
|
||||||
msgstr "WebGL недоступен в этом браузере, поэтому полная карта не может запуститься. Выберите пример ниже или попробуйте запустить карту ещё раз."
|
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 "Показать место на карте"
|
||||||
|
|
|
||||||
|
|
@ -5565,3 +5565,10 @@ msgstr "Вибраний приклад:"
|
||||||
#, elixir-autogen, elixir-format
|
#, 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."
|
msgid "WebGL is unavailable in this browser, so the full map cannot start. Select an example below or retry the map."
|
||||||
msgstr "WebGL недоступний у цьому браузері, тому повна карта не може запуститися. Виберіть приклад нижче або спробуйте запустити карту ще раз."
|
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 "Показати місце на мапі"
|
||||||
|
|
|
||||||
|
|
@ -322,6 +322,7 @@ defmodule WhoNeedHelpWeb.MutualAidLiveTest do
|
||||||
|> Enum.find(&(&1.slug == "coffee-meetup"))
|
|> Enum.find(&(&1.slug == "coffee-meetup"))
|
||||||
|
|
||||||
{:ok, view, _html} = build_conn() |> log_in_user(viewer) |> live(~p"/activities")
|
{: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} =
|
{:ok, _activity} =
|
||||||
Activities.create_activity(Accounts.Scope.for_user(organizer), %{
|
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, activity} = Activities.create_activity(organizer_scope, attrs)
|
||||||
{:ok, organizer_view, _html} = live(organizer_conn, ~p"/activities/#{activity.id}")
|
{:ok, organizer_view, _html} = live(organizer_conn, ~p"/activities/#{activity.id}")
|
||||||
{:ok, participant_view, html} = live(participant_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 =~ "Request to join"
|
||||||
assert html =~ "Safety controls"
|
assert html =~ "Safety controls"
|
||||||
assert html =~ "Block organizer"
|
assert html =~ "Block organizer"
|
||||||
|
|
@ -783,6 +789,7 @@ defmodule WhoNeedHelpWeb.MutualAidLiveTest do
|
||||||
{:ok, request} = Help.create_request(requester_scope, request_attrs(category))
|
{:ok, request} = Help.create_request(requester_scope, request_attrs(category))
|
||||||
{:ok, requester_view, _html} = live(requester_conn, ~p"/requests/#{request.id}")
|
{:ok, requester_view, _html} = live(requester_conn, ~p"/requests/#{request.id}")
|
||||||
{:ok, helper_view, _html} = live(helper_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
|
helper_view
|
||||||
|> element("button[phx-click='accept']")
|
|> element("button[phx-click='accept']")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user