who_need_help/assets/js/hooks.js
SimpleTest ff856f637f
Some checks are pending
Quality / full-local-gates (push) Waiting to run
feat: expand homepage with interactive demo map
2026-07-21 20:13:55 +03:00

309 lines
8.4 KiB
JavaScript

import maplibregl from "maplibre-gl"
const trackingMinTimeMs = 5000
const defaultStyle = {
version: 8,
sources: {
osm: {
type: "raster",
tiles: [document.documentElement.dataset.mapTileUrl],
tileSize: 256,
attribution: "© OpenStreetMap contributors"
}
},
layers: [{id: "osm", type: "raster", source: "osm"}]
}
const markerPoints = element => {
try {
const points = JSON.parse(element.dataset.markers || "[]")
if (!Array.isArray(points)) return []
return points.filter(point =>
Number.isFinite(point?.longitude) && Number.isFinite(point?.latitude)
)
} catch (_error) {
return []
}
}
const createAidMap = element => {
const state = {
element,
map: null,
markers: []
}
state.markLoading = () => {
if (state.element.dataset.mapReady !== "true") {
state.element.dataset.mapReady = "false"
}
}
state.markReady = () => {
state.element.dataset.mapReady = "true"
}
state.showUnavailable = () => {
state.map = null
state.element.dataset.mapUnavailable = "true"
state.markReady()
const fallback = document.createElement("p")
fallback.className = "grid h-full place-items-center p-6 text-center text-sm"
fallback.dataset.mapFallback = "true"
fallback.textContent = state.element.dataset.mapUnavailableLabel
state.element.replaceChildren(fallback)
}
state.renderMarkers = () => {
if (!state.map) return
state.markers.forEach(marker => marker.remove())
state.markers = []
const points = markerPoints(state.element)
points.forEach(point => {
const popupContent = document.createElement("div")
const title = document.createElement("strong")
title.textContent = point.title || ""
popupContent.append(title, document.createElement("br"))
popupContent.append(document.createTextNode(point.location || ""))
const popup = new maplibregl.Popup({offset: 18}).setDOMContent(popupContent)
const marker = new maplibregl.Marker({color: point.exact ? "#d6573b" : "#278467"})
.setLngLat([point.longitude, point.latitude])
.setPopup(popup)
.addTo(state.map)
state.markers.push(marker)
})
if (points.length === 1) {
state.map.flyTo({center: [points[0].longitude, points[0].latitude], zoom: 12})
} else if (points.length > 1) {
const bounds = new maplibregl.LngLatBounds()
points.forEach(point => bounds.extend([point.longitude, point.latitude]))
state.map.fitBounds(bounds, {padding: 52, maxZoom: 13})
}
}
state.mount = () => {
state.markLoading()
const demoMap = state.element.dataset.demoMap === "true"
if (!supportsMapCanvas()) {
state.showUnavailable()
return
}
try {
state.map = new maplibregl.Map({
container: state.element,
style: defaultStyle,
center: [30.5234, 50.4501],
zoom: 5,
attributionControl: true,
dragRotate: !demoMap,
pitchWithRotate: !demoMap,
scrollZoom: !demoMap
})
} catch (_error) {
state.showUnavailable()
return
}
if (demoMap) state.map.touchZoomRotate.disableRotation()
state.map.addControl(
new maplibregl.NavigationControl({showCompass: !demoMap}),
demoMap ? "bottom-right" : "top-right"
)
state.map.on("dataloading", state.markLoading)
state.map.on("load", state.markReady)
state.map.on("idle", state.markReady)
state.renderMarkers()
}
state.destroy = () => {
state.map?.off("dataloading", state.markLoading)
state.map?.off("load", state.markReady)
state.map?.off("idle", state.markReady)
state.map?.remove()
}
return state
}
export const mountStaticAidMaps = root => {
root.querySelectorAll("[data-static-aid-map]").forEach(element => {
if (element.dataset.staticMapMounted === "true") return
const mount = () => {
element.dataset.staticMapMounted = "true"
element._aidMap = createAidMap(element)
element._aidMap.mount()
}
if (!("IntersectionObserver" in window)) {
mount()
return
}
const observer = new IntersectionObserver(entries => {
if (!entries.some(entry => entry.isIntersecting)) return
observer.disconnect()
mount()
}, {rootMargin: "240px"})
observer.observe(element)
})
}
export const Hooks = {
AidMap: {
mounted() {
this.aidMap = createAidMap(this.el)
this.aidMap.mount()
},
updated() {
this.aidMap.renderMarkers()
},
destroyed() {
this.aidMap.destroy()
}
},
LocationPicker: {
mounted() {
this.el.addEventListener("click", () => {
const latitudeInput = document.querySelector(this.el.dataset.latitudeTarget)
const longitudeInput = document.querySelector(this.el.dataset.longitudeTarget)
if (!latitudeInput || !longitudeInput || !navigator.geolocation) {
this.el.textContent = "Location is unavailable"
this.el.classList.add("btn-error")
return
}
navigator.geolocation.getCurrentPosition(
position => {
latitudeInput.value = position.coords.latitude
longitudeInput.value = position.coords.longitude
latitudeInput.dispatchEvent(new Event("input", {bubbles: true}))
longitudeInput.dispatchEvent(new Event("input", {bubbles: true}))
this.el.textContent = "Location added"
this.el.classList.add("btn-success")
},
() => {
this.el.textContent = "Location permission denied"
this.el.classList.add("btn-error")
},
{enableHighAccuracy: true, timeout: 10000}
)
})
}
},
LiveTracking: {
mounted() {
this.destroying = false
this.lastLocationSentAt = 0
this.pendingLocation = null
this.locationTimer = undefined
this.sendLocation = payload => {
this.lastLocationSentAt = Date.now()
this.pendingLocation = null
this.pushEvent("location-update", payload)
}
this.queueLocation = payload => {
const elapsed = Date.now() - this.lastLocationSentAt
if (this.lastLocationSentAt === 0 || elapsed >= trackingMinTimeMs) {
if (this.locationTimer !== undefined) {
window.clearTimeout(this.locationTimer)
this.locationTimer = undefined
}
this.sendLocation(payload)
return
}
this.pendingLocation = payload
if (this.locationTimer === undefined) {
this.locationTimer = window.setTimeout(() => {
this.locationTimer = undefined
if (!this.destroying && this.pendingLocation) {
this.sendLocation(this.pendingLocation)
}
}, trackingMinTimeMs - elapsed)
}
}
if (typeof window.WhoNeedHelpAndroid?.postMessage === "function") {
this.nativeError = () => {
this.pushEvent("location-error", {})
this.pushEvent("stop-tracking", {})
}
window.addEventListener("wnh:native-tracking-error", this.nativeError)
return
}
this.watchId = navigator.geolocation.watchPosition(
position => this.queueLocation({
latitude: position.coords.latitude,
longitude: position.coords.longitude,
accuracy_meters: position.coords.accuracy
}),
() => {
if (!this.destroying) this.pushEvent("location-error", {})
},
{enableHighAccuracy: true, maximumAge: 5000, timeout: 15000}
)
},
destroyed() {
this.destroying = true
this.pendingLocation = null
if (this.locationTimer !== undefined) {
window.clearTimeout(this.locationTimer)
this.locationTimer = undefined
}
if (this.nativeError) {
window.removeEventListener("wnh:native-tracking-error", this.nativeError)
}
if (this.watchId !== undefined) navigator.geolocation.clearWatch(this.watchId)
}
}
}
function supportsMapCanvas() {
const canvas = document.createElement("canvas")
const attributes = {
alpha: true,
depth: true,
stencil: true,
premultipliedAlpha: true
}
try {
const context =
canvas.getContext("webgl2", attributes) ||
canvas.getContext("webgl", attributes)
if (!context) return false
context.getExtension("WEBGL_lose_context")?.loseContext()
return true
} catch (_error) {
return false
}
}