Warn before a boda ride
Check a trip before the rider sets off, and surface the spots along the safest route.
type LatLng = { lat: number; lng: number }
export async function checkTrip(base: string, from: LatLng, to: LatLng) {
const url = new URL("/api/route", base)
url.searchParams.set("from", `${from.lat},${from.lng}`)
url.searchParams.set("to", `${to.lat},${to.lng}`)
const res = await fetch(url)
if (!res.ok) throw new Error((await res.json()).error)
const { routes } = await res.json()
const safest = routes[0] // routes are sorted safest first
return {
minutes: Math.round(safest.duration / 60),
warnings: safest.dangers.map((d: any) => ({
title: d.spot.title,
severity: d.spot.severity,
kmIntoTrip: +(d.along / 1000).toFixed(1),
night: d.spot.time_of_day === "night",
})),
saferAlternativeExists: routes.length > 1 && routes[0].risk < routes[1].risk,
}
}Nearby alerts
The public map warns people within 300 m of a spot. The spot list is small, so fetch it once and check distances on the device. The user’s location never needs to leave their phone.
const R = 6371000
const rad = (d: number) => (d * Math.PI) / 180
function metres(a: LatLng, b: LatLng) {
const h =
Math.sin(rad(b.lat - a.lat) / 2) ** 2 +
Math.cos(rad(a.lat)) * Math.cos(rad(b.lat)) * Math.sin(rad(b.lng - a.lng) / 2) ** 2
return 2 * R * Math.asin(Math.sqrt(h))
}
const { spots } = await (await fetch(`${base}/api/spots`)).json()
const warned = new Set<number>()
navigator.geolocation.watchPosition(({ coords }) => {
const me = { lat: coords.latitude, lng: coords.longitude }
for (const spot of spots) {
if (spot.status === "disputed" || warned.has(spot.id)) continue
if (metres(me, spot) <= 300) {
warned.add(spot.id)
notify(`Danger spot ahead: ${spot.title}`)
}
}
})Keep a map fresh
Poll GET /api/spots every one or two minutes while the page is visible, and refresh when the
tab becomes visible again. New reports and moderator decisions then show up without a reload.
async function refresh() {
if (document.visibilityState !== "visible") return
const res = await fetch("/api/spots")
if (res.ok) render((await res.json()).spots)
}
setInterval(refresh, 120_000)
document.addEventListener("visibilitychange", refresh)Search as you type
Query mapped spots on every keystroke, and places (with nearby danger) once the user pauses.
let timers: ReturnType<typeof setTimeout>[] = []
function onInput(q: string) {
timers.forEach(clearTimeout)
if (q.trim().length < 2) return render({ spots: [], places: [] })
// Fast: database-only spot search.
timers.push(setTimeout(async () => render(await (await fetch(`/api/search?places=0&q=${encodeURIComponent(q)}`)).json()), 120))
// Slower: adds places, each with the mapped spots within 2 km.
timers.push(setTimeout(async () => render(await (await fetch(`/api/search?q=${encodeURIComponent(q)}`)).json()), 450))
}Ignore responses from older queries if a newer one has started, since they can arrive out of order.
Show which way the user voted
GET /api/me returns the caller’s votes as { [spotId]: 1 | -1 }. Use it to disable the button
they already pressed, since a repeat vote returns 409.
const { votes, requireApproval } = await (await fetch("/api/me")).json()
const myVote = votes[spot.id] // 1, -1 or undefinedShare a spot
Every spot has a deep link on the public map: https://deathspot.org/?spot=<id>. It opens the
map centred on the spot with its details showing. It works well in WhatsApp messages.