Skip to content

Recipes

Practical patterns for route checks, nearby alerts and keeping a map fresh.

Updated View as Markdown

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 undefined

Share 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.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close