Heartmaps
Bewoners lopen een route door hun buurt en laten op elke plek achter wat ze ervan vinden.
Een onderzoeker zet een route uit op een kaart, met punten op plekken die ertoe doen. Open de kaart op je telefoon, loop de route en reageer op de plek zelf. Een kompas en een looplijn wijzen de weg; een punt wordt actief als je er dichtbij bent. De onderzoeker ziet alle reacties terug op de kaart, precies waar ze zijn achtergelaten.
Heartmaps is een teamproject. Mijn deel: de route-editor, de reacties met locatie, de navigatie voor de deelnemer en het zelfstandig uitrollen van de applicatie. De backend is Django met GeoDjango en PostGIS, met GraphQL en REST; de frontend TypeScript met Mapbox GL, Three.js en deck.gl, gebouwd op gedeelde modules. Een punt wordt actief binnen vijftien meter. Deelnemers hebben geen account nodig. De applicatie draait als container op Scaleway, bouwt op een versie-tag. Voor elke release lopen Playwright-tests op telefoon- en schermformaat.
Colofon
- Getoond: Groene Westen, Amsterdam, demo december 2025
- Studio: Starling Studio
- Mijn aandeel: route-editor, reacties met locatie, navigatie, deelnemerskant, uitrol
Code
Hoe een reactie aan een plek hangt, ingekort: bij elke positiebepaling zoekt de telefoon het dichtstbijzijnde punt dat nog niet gepasseerd is, en opent het reactiescherm één keer per punt. De reactie wordt met het punt en de coördinaten van de deelnemer opgeslagen.
// In updateNavigation(lat, lng): het dichtstbijzijnde punt dat nog voor je ligt
let minDistance = Infinity
let nearestIndex = currentIndex
for (let i = currentIndex; i < route.points.length; i++) {
const point = route.points[i]
const dist = haversineDistanceMeters(lat, lng, point.latitude, point.longitude)
if (dist < minDistance) { minDistance = dist; nearestIndex = i }
if (i === currentIndex && dist < WAYPOINT_REACHED_THRESHOLD) {
nearestIndex = Math.min(i + 1, route.points.length - 1)
break
}
}
// Dichtbij genoeg en nog niet getoond voor dit punt: vraag om een reactie
const currentPoint = route.points[nearestIndex]
if (minDistance < REACTION_DISTANCE_THRESHOLD && currentPoint.id !== lastReactionPointId) {
lastReactionPointId = currentPoint.id
const existing = store.myReactions?.get(currentPoint.id)
if (existing) showReactionPanelForEdit(currentPoint, existing)
else showReactionPanel(currentPoint)
}
// Bij het opslaan: de reactie hangt aan het punt, met waar de deelnemer stond
const variables = { routePointId: pointId, participantId, smiley, text: text || null,
longitude: coords[0], latitude: coords[1] }