Topografie-oefentool voor groep 5-8
  • JavaScript 80.5%
  • HTML 13.4%
  • CSS 5.1%
  • Shell 0.5%
  • PHP 0.5%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Jelmer e290c40541
All checks were successful
Dev preview / preview (push) Successful in 22s
fix: make feedback type visible without labels
Verified against the migrated instance: a `write:issue` token cannot set
labels (403 on /issues/N/labels), and Forgejo silently drops the `labels`
field on create. Rather than widening the token — this endpoint is
publicly reachable, so it stays minimal-scope — the type now leads the
issue title as an emoji, so bug and suggestion are distinguishable in the
issue list. Documented how to enable real labels if ever wanted.

Version → v2.30.3.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-20 17:29:41 +02:00
.claude docs(#83): migrate release skill + docs to Codeberg/tea 2026-04-22 17:04:19 +02:00
.forgejo/workflows ci: cut over from Codeberg to own Forgejo (git.jelmer.org) 2026-07-20 16:52:11 +02:00
css feat(#133): digibord-modus — grotere typografie voor projectie 2026-06-25 12:52:40 +02:00
data fix(#126): meer punten + losse stukjes/dubbelingen weg (set 5.9 wegen) 2026-06-15 10:33:53 +02:00
fonts feat(#134): dyslexie-vriendelijk lettertype (OpenDyslexic-toggle) 2026-06-25 11:48:56 +02:00
mockups docs(#132): checksum-uitleg + controleer-code-scherm (route A) 2026-06-25 11:40:52 +02:00
scripts feat: point the app at our own Forgejo instead of Codeberg 2026-07-20 17:26:16 +02:00
src refactor+feat(#134): body-font via variabele + instellingen op startscherm 2026-06-25 11:53:58 +02:00
tests feat(#133): digibord-modus — grotere typografie voor projectie 2026-06-25 12:52:40 +02:00
.assetsignore refactor(#95): rename test.js → test.mjs as ESM entrypoint 2026-04-22 10:35:30 +02:00
.DS_Store Missend bestand 2026-04-07 21:27:41 +02:00
.gitignore fix: set 5.9 wegen — point-in-NL clip + dichtgechaind net 2026-06-14 22:26:43 +02:00
.htaccess feat: add Uberspace deployment via rsync + .htaccess 2026-03-27 21:32:03 +01:00
apple-touch-icon.png feat(#112,#113,#115): hamburger menu + PWA icon redesign 2026-04-22 08:52:14 +02:00
AUDIT.md docs(#86): add AUDIT.md — code audit report for scope 1 2026-04-21 14:50:54 +02:00
cities.js fix: set 5.9 alleen de 24 steden van PDF pagina 2 2026-06-14 22:07:24 +02:00
CLAUDE.md feat: point the app at our own Forgejo instead of Codeberg 2026-07-20 17:26:16 +02:00
debug-gewesten.html feat(#42): add Alpen fuzzy region to set 7.3 2026-04-14 22:44:07 +02:00
debug-landen.html fix(#38): smooth Wadden inlet transitions in Noordzee polygon 2026-04-19 15:11:17 +02:00
debug-set83.html feat(#51): add set 8.3 — Noord- en Midden-Amerika 2026-04-20 11:59:16 +02:00
debug-wateren.html chore: add debug page for visual water GeoJSON inspection 2026-04-07 19:29:17 +02:00
eilanden-midden-amerika.geojson fix(#57): all 6 Antilles as real polygons + resize Pacific/Atlantic for set 89 2026-04-20 19:32:56 +02:00
eilanden-zuidoost-azie.geojson feat(#55): add Geobas set 8.7 Zuidoost-Azië (v2.14.0) 2026-04-20 16:56:45 +02:00
feedback.config.example.ini docs(#127): example-config wijst naar hergebruikte ntfy-topic Topoquiz 2026-06-24 12:53:11 +02:00
feedback.php fix: make feedback type visible without labels 2026-07-20 17:29:41 +02:00
gebieden-oceanie.geojson feat(#56): set 8.8 Australia and Oceania 2026-04-20 17:44:15 +02:00
gewesten.geojson feat(#51): add set 8.3 — Noord- en Midden-Amerika 2026-04-20 11:59:16 +02:00
icon-192.png feat(#112,#113,#115): hamburger menu + PWA icon redesign 2026-04-22 08:52:14 +02:00
icon-512.png feat(#112,#113,#115): hamburger menu + PWA icon redesign 2026-04-22 08:52:14 +02:00
icon-maskable-512.png feat(#112,#113,#115): hamburger menu + PWA icon redesign 2026-04-22 08:52:14 +02:00
icon-maskable.svg feat(#112,#113,#115): hamburger menu + PWA icon redesign 2026-04-22 08:52:14 +02:00
icon.svg feat(#112,#113,#115): hamburger menu + PWA icon redesign 2026-04-22 08:52:14 +02:00
index.html fix: make feedback type visible without labels 2026-07-20 17:29:41 +02:00
landen-afrika.geojson feat(#50): add set 8.2 Afrika (v2.9.0) 2026-04-19 22:27:01 +02:00
landen-europa.geojson fix(#78): switch country polygons to Natural Earth 1:10m for real coastlines 2026-04-19 14:44:56 +02:00
landen-midden-amerika.geojson feat(#57): add Geobas set 8.9 Midden-Amerika en Caraïben (v2.16.0) 2026-04-20 18:25:44 +02:00
landen-midden-oosten.geojson feat(#52): add set 8.4 Midden-Oosten 2026-04-20 13:46:36 +02:00
landen-noord-midden-amerika.geojson feat(#51): add set 8.3 — Noord- en Midden-Amerika 2026-04-20 11:59:16 +02:00
landen-oceanie.geojson feat(#56): set 8.8 Australia and Oceania 2026-04-20 17:44:15 +02:00
landen-oost-azie.geojson feat(#54): add Geobas set 8.6 Oost-Azië (v2.13.0) 2026-04-20 16:20:52 +02:00
landen-zuid-azie.geojson feat(#53): add set 8.5 Zuid-Azië 2026-04-20 15:48:29 +02:00
landen-zuidamerika.geojson feat(#49): add set 8.1 Zuid-Amerika (v2.8.0) 2026-04-19 21:05:08 +02:00
landen-zuidoost-azie.geojson feat(#55): add Geobas set 8.7 Zuidoost-Azië (v2.14.0) 2026-04-20 16:56:45 +02:00
leerkrachten.html docs: merknaam methode niet meer expliciet noemen (IP-voorzichtigheid) 2026-06-24 22:46:10 +02:00
manifest.json feat(#112,#113,#115): hamburger menu + PWA icon redesign 2026-04-22 08:52:14 +02:00
MANUAL-TEST.md fix(#117,#118): XP-card gap + coverage + manual test script 2026-04-22 12:09:52 +02:00
MIGRATION.md docs(#83): Codeberg pre-cutover health-check — all systems go 2026-04-28 15:43:00 +02:00
package-lock.json Add c8 coverage enforcement and scenario-based e2e tests 2026-03-14 16:29:50 +01:00
package.json feat: point the app at our own Forgejo instead of Codeberg 2026-07-20 17:26:16 +02:00
playwright.config.js test: calibrate viewport + zoom threshold for fit-to-viewport map 2026-04-21 23:36:43 +02:00
privacy.html feat: point the app at our own Forgejo instead of Codeberg 2026-07-20 17:26:16 +02:00
provincie_2023.geojson feat(#85): set 6.8 Fryslân — 12 steden + Afsluitdijk + 4 wadden-eilanden + Gaasterland + 4 wateren 2026-05-06 20:02:37 +02:00
README.md feat: point the app at our own Forgejo instead of Codeberg 2026-07-20 17:26:16 +02:00
REFACTORING.md docs: refactoring-analyse na set 71 + 72 2026-04-12 16:08:19 +02:00
sitemap.xml add sitemap and force HTTPS 2026-03-16 21:21:22 +01:00
sw.js fix: make feedback type visible without labels 2026-07-20 17:29:41 +02:00
test.mjs feat: set 5.9 Wegen op de kaart + NL-snelwegen-overlay 2026-06-14 22:00:22 +02:00
wateren.geojson feat: set 6.9 Gelderland — steden + streken (Veluwe/Achterhoek/Betuwe) + rivieren 2026-06-14 21:32:05 +02:00
wegen-nl.geojson fix(#126): meer punten + losse stukjes/dubbelingen weg (set 5.9 wegen) 2026-06-15 10:33:53 +02:00

Topografie Quiz

Een topografie-oefentool voor de bovenbouw van de basisschool (groep 58). Kinderen leren plaatsen, provincies, landen, wateren en meer via meerkeuze, typen of klikken op de kaart — met spaced repetition. De levels volgen de gangbare topografie-hoofdstuknummering (5.4, 6.8, 7.1, …).

Live: topoquiz.com — geen buildstap vereist.

Lokaal draaien: npx serve . (vereist een lokale server vanwege GeoJSON-fetch).

Tests: npm run test — draait unit tests (node test.mjs) én Playwright E2E tests. Altijd uitvoeren voor je pusht.


Projectstructuur

Bestand / map Inhoud
index.html Markup + view-code (~2400 regels). Historisch monoliet; CSS + pure logica worden stap voor stap uitgesplitst.
css/*.css Visuele laag, gesplitst per scherm: tokens.css (design-tokens), start.css (home + level-select, Routekaart-refresh), quiz.css, end.css, feedback.css (modal-framework + feedback + settings-stub), map.css, app.css (globaal), animations.css.
src/game/*.js Pure-logic ESM-modules, geïmporteerd door index.html én test.mjs (#95). Nu: text.js (fuzzy matching), geo.js (afstandshelpers). Wordt uitgebreid — zie issue #95.
cities.js Alle plaatsdata, set-definities, daily/bonus-format-tabellen. Geladen als browser-global én in Node via createRequire in test.mjs.
test.mjs Unit-suite (ESM). node test.mjs — geen mirror-code meer: importeert rechtstreeks uit src/game/.
tests/*.spec.js Playwright E2E-suite.
sw.js Service worker — offline cache + PWA-install. Bump CACHE bij elke release die een asset wijzigt.

Een nieuw level toevoegen

1. Data toevoegen aan cities.js

Kies de juiste array op basis van het elementtype:

Array Elementtype Rendering
ALL_CITIES Steden, hoofdsteden Stip op kaart
ALL_PROVINCES Nederlandse provincies Polygoon (provincie_2023.geojson)
ALL_WATERS Wateren (rivieren, zeeën) Lijn/polygoon (wateren.geojson)
ALL_COUNTRIES Landen Polygoon (landen-europa.geojson, landen-zuidamerika.geojson, landen-afrika.geojson, landen-noord-midden-amerika.geojson, landen-midden-oosten.geojson, landen-zuid-azie.geojson, landen-oost-azie.geojson, landen-zuidoost-azie.geojson, landen-oceanie.geojson, landen-midden-amerika.geojson)
ALL_PROVINCES (eilanden) Indonesische eilanden Polygoon (eilanden-zuidoost-azie.geojson) — Kalimantan, Sumatra, Sulawesi, Java, Molukken
ALL_PROVINCES (Antillen) ABC + Sint Maarten Polygoon (eilanden-midden-amerika.geojson) — echte polygonen voor set 8.9 (Saba + Sint Eustatius als fuzzy ellips)
ALL_PROVINCES (gebieden) Tasmanië + Antarctica Polygoon (gebieden-oceanie.geojson) — echte polygonen voor set 8.8

Velden per item:

// Stad
{ name: "Plaatsnaam", lat: 52.12, lon: 4.56, pop: 75000, sets: [66] }
// Land of water (set-specifiek)
{ name: "Estland", lat: 58.67, lon: 25.54, sets: [70] }
Veld Verplicht Uitleg
name Officiële naam (wordt getoond en gecontroleerd)
lat / lon WGS84-coördinaten (centroid voor label)
pop alleen ALL_CITIES Bevolking; bepaalt de stipgrootte (logaritmisch, 412px)
sets Array van set-nummers; een item kan in meerdere sets zitten
capital true voor hoofdsteden (vierkante marker)
aliases Alternatieve spellingen die als goed worden geaccepteerd
kind ALL_PROVINCES: 'gewest' / 'eiland' / 'gebied' — voor UI-labels in niet-NL-sets (7.2 gewesten, 8.7 eilanden, 8.8/8.9 gebieden)
shape 'fuzzy' (ellips) of 'peak' (bergtop-driehoek) — zie Shape-overrides onder Wateren
rx, ry, rot bij shape:'fuzzy' Ellips-radii in graden + rotatie (°)
posBySet Per-set positie-override: { [setNr]: { lat, lon, rx?, ry?, rot? } } — zie Shape-overrides

Voor wateren: sets-veld aanwezig = set-specifiek (bijv. rivieren in set 7.3); sets afwezig = gedeeld (NL wateren, altijd geladen voor set 57).

2. De set registreren in SETS

Sinds issue #93 wordt elke set aangemaakt via een van drie factory-functies die een kind-discriminator stempelen. Consumenten in index.html switchen op set.kind — geen ad-hoc null-checks meer op phases/daily.

Enkelvoudige setsimpleSet({ … }) — één quizType voor de hele sessie:

67: simpleSet({ name: '6.7  Noord-Holland', quizType: 'place', fitOnStart: true, group: 6,
                clickCorrectKm: 10, clickCloseKm: 30 }),

Meerfasige setphasedSet({ … }) — fases sequentieel doorlopen:

73: phasedSet({ name: '7.3  Frankrijk, Spanje en Portugal', group: 7, mastery: 1,
                bounds: [[35, -12], [52, 10]],
                clickCorrectKm: 80, clickCloseKm: 240,
                phases: [
                  { id: 'cities',  label: 'Steden',   quizType: 'place'    },
                  { id: 'regions', label: 'Gebieden', quizType: 'province' },
                  { id: 'rivers',  label: 'Rivieren', quizType: 'water'    },
                ] }),

Daily/bonusdailyBonusSet({ … }) — heterogene pool, variant bepaalt daily vs bonus:

98: dailyBonusSet({ name: '📅 Uitdaging van vandaag', variant: 'daily', mastery: 1 }),
99: dailyBonusSet({ name: 'Bonus: door elkaar',       variant: 'bonus', mastery: 1 }),

simpleSet velden:

Veld Verplicht Uitleg
name Weergavenaam in het menu
quizType 'place' / 'province' / 'water' / 'country'
group Groepsnummer (58) — bepaalt in welke groep het level verschijnt
fitOnStart true = zoom in op de actieve items; false (default) = heel Nederland
clickCorrectKm / clickCloseKm Klik-drempels in km (default 20/60)

phasedSet velden:

Veld Verplicht Uitleg
name Weergavenaam in het menu
group Groepsnummer (58)
phases Array { id, label, quizType } — sequentieel doorlopen
bounds [[lat,lon],[lat,lon]] — viewport voor EU/wereld-sets
clickCorrectKm / clickCloseKm Klik-drempels in km (default 20/60)
mastery Default 1; overschrijft MASTERY_MC/MASTERY_TEXT

dailyBonusSet velden:

Veld Verplicht Uitleg
name Weergavenaam in het menu
variant 'daily' (datum-geseed) of 'bonus' (shuffle op eerste open)
mastery Default 1
fitOnStart Default false — daily/bonus laat de kaart per vraag zoomen

De factory stempelt automatisch kind: 'simple' \| 'phased' \| 'dailyBonus'. Consumenten branchen op kind (en bij dailyBonus op variant).

Set-nummering volgt de topografie-hoofdstuknummers: 54 → 5.4, 61 → 6.1, 98 → daily, 99 → bonus.


Game modes

Elke set (behalve provincies) biedt drie oefenmodi:

Modus Beschrijving
Meerkeuze (mc) Een stip op de kaart, naam kiezen uit vier opties
Typen (text) Een stip op de kaart, naam zelf intypen (fuzzy matching)
Klik op de kaart (map) Stadsnaam zichtbaar, klik op de kaart waar de stad ligt

Klik-op-de-kaart drempelwaarden

Afstand Resultaat
< 20 km Correct
2060 km ⚠️ Bijna (telt als fout)
> 60 km Fout

Speciale sets

Dagelijkse uitdaging & bonus (set 98 / 99) — per groep, mixed-type

Zowel de daily (set 98) als de bonus (set 99) zijn per-groep en bevatten een mix van item-types (steden, provincies/regio's, wateren, landen, gebergten) — afgestemd op wat een kind in die groep heeft geleerd. Altijd meerkeuze, mastery = 1×, knoppen onderaan de set-lijst na groepkeuze.

De mix per groep wordt gedreven door twee tabellen in cities.js:

const DAILY_FORMAT = {
  5: [ {type:'region', count:3}, {type:'place', count:5}, {type:'water', count:2} ],
  6: [ {type:'place',  count:10} ],
  7: [ {type:'country', count:2}, {type:'place', count:4}, {type:'region', count:3}, {type:'water', count:1} ],
  8: [ {type:'country', count:3}, {type:'place', count:4}, {type:'region', count:2}, {type:'water', count:1} ],
};
const BONUS_FORMAT = {
  5: [...],  // 20 items: 6 regio + 10 steden + 4 wateren
  6: [ {type:'place', count:25} ],
  7: [...],  // 35 items: 7 landen + 14 steden + 7 regio + 4 gebergten + 3 wateren
  8: [...],  // 40 items: 12 landen + 16 steden + 8 gebergten + 4 wateren
};
  • Types (_itemType): 'place' (uit ALL_CITIES), 'country' (ALL_COUNTRIES), 'water' (ALL_WATERS), 'region' (alle kinds uit ALL_PROVINCES — provincie, gewest, regio, gebied, eiland, berg).
  • Per-groep filter: elk item komt uit ALL_<X> gefilterd op item.sets.some(s => SETS[s].group === selectedGroup).
  • Daily seed: dateSeed(dateStr, group) = (dateNum * 31 + group) | 0 — zelfde datum + andere groep geeft een andere pool.
  • Bonus wordt bij de eerste klik opgebouwd en in sessionStorage bewaard ({name, _itemType}[]) zodat terugnavigeren dezelfde pool herstelt.
  • Distractors in meerkeuze worden per vraag gefilterd op hetzelfde _itemType (en voor region ook op kind) — geen stad als distractor bij een regio-vraag.
  • Dedupe: buildMixedPool houdt een usedNames-set bij zodat namen die in meerdere pools voorkomen (Panama stad + land, Luxemburg stad + land) niet dubbel in één sessie landen — dat voorkomt collisions in de streak/answer-tracking.
  • Composite answer-key: dailyAnswerKey(city) = "${_itemType}:${name}" zodat cross-type naam-duplicaten los worden bijgehouden in de daily-emoji-grid.
  • Map-framing: startQuiz slaat voor daily/bonus de rAF-bounds-fallback over; renderQuestion bepaalt per item het kader (polygon → fitBounds op de laag; placeflyTo op de stad met zoom 8). Zonder deze skip racete de NL_BOUNDS-fallback met de Armenia-fitBounds en klapte de kaart terug naar Nederland.

Toegankelijkheid: de daily- en bonus-knop zijn alleen zichtbaar nadat de leerling op het startscherm een groep heeft gekozen. Daarvoor hangen ze verborgen onder sessionStorage.selectedGroup.


Quiztypes

place (standaard)

Elke stad krijgt een stip op de kaart. Provinciehoofdsteden (capital: true) krijgen een vierkante marker in plaats van een cirkel.

province

Provincievlakken worden gekleurd vanuit het lokale bestand provincie_2023.geojson (gebundeld in de repo, bron: cartomap.github.io). De vlakken worden vergeleken op statnaam; zorg dat ALL_PROVINCES[].name exact overeenkomt. Dit type gebruikt ALL_PROVINCES als activeCities in plaats van ALL_CITIES.

country

Landspolygonen worden gekleurd vanuit de landen-*.geojson bestanden (vereenvoudigd Natural Earth-formaat, per werelddeel apart). Bestanden worden bij het opstarten samengevoegd tot één FeatureCollection. Elk feature heeft een name-property die overeenkomt met ALL_COUNTRIES[].name. Meerkeuze- en typemodus werken identiek aan province; klik-op-de-kaart gebruikt distanceToCountry() (0 km als klik binnen het polygoon valt).

water

Waterlijnen en -polygonen vanuit wateren.geojson. Ondersteunt LineString (rivieren, kanalen) en Polygon (zeeën, meren). Set-specifieke wateren hebben een sets-veld; wateren zonder sets worden alleen in set 57 geladen.

Meerfasige sets (phases)

Sets met een phases-array doorlopen de fases sequentieel: eerst alle items van fase 1 memoriseren, dan een tussenscherm, dan fase 2, enzovoort. Binnen elke fase is het quizType homogeen (geen menging van stippen en polygonen). De voortgangsbalk toont het fasesabel en itemteller per fase.


Spaced repetition

  • Meerkeuze / klik op kaart: een plaats is geleerd na 3× correct (MASTERY_MC = 3)
  • Typen: na 1× correct (MASTERY_TEXT = 1)
  • Speciale sets (daily/bonus): na 1× correct (instelbaar via mastery-veld in SETS)
  • Elke fout reset de streak naar 0
  • Plaatsen dichterbij de huidige vraag worden vaker als afleider gekozen (nearbyDistractors)

Fuzzy matching (typmodus)

Antwoorden worden genormaliseerd (lowercase, spaties/koppeltekens verwijderd) en vergeleken via Levenshtein-afstand:

Naamlengte Max. typefouten
≤ 4 tekens 0
58 tekens 1
≥ 9 tekens 2

Bij een typefouten-match krijgt de leerling de melding "Bijna!" maar telt het wel als goed.


Navigatie & voortgang

De app gebruikt de History API (pushState/popstate) zodat de browserterugknop werkt. Voortgang per level+modus wordt opgeslagen in sessionStorage en automatisch hersteld bij terugnavigeren. Voortgang wordt gewist zodra een quiz volledig is afgerond.

Tijdens een quiz toont de title-bar een hamburger (☰, #112): opnieuw beginnen, andere quiz kiezen, instellingen en feedback geven. Daarnaast is er een ⚙️-knop in de header van het startscherm (#134), zodat de weergave-instellingen ook vóór het spelen bereikbaar zijn (belangrijk voor de dyslexie-toggle).

Weergave-instellingen (Instellingen → Weergave)

Twee toegankelijkheids-toggles, beide bewaard in localStorage en toegepast bij paginalaad (applyDyslexie / applyDigibord in index.html), gestuurd via een body-class:

  • Dyslexie-vriendelijk lettertype (body.dyslexie, #134) — zet de UI om naar het self-hosted OpenDyslexic (OFL, fonts/opendyslexic-*.woff2). In dyslexie-modus wordt de display/mono-tracking geneutraliseerd en krijgen koppen font-weight: 700 voor leesbaarheid. Mono (cijfers/labels) blijft ongemoeid.
  • Digibord-modus (body.digibord, #133) — schaalt alle type-tokens (--fs-* in css/tokens.css) ~1.35× op voor projectie op het smartboard. Omdat de hele UI die tokens via var() gebruikt, groeit alle tekst mee. De kaart blijft op CSS-niveau ongemoeid, zodat klikken op de kaart exact blijft (geen CSS-zoom die Leaflet-coördinaten verschuift).

Visuele stijl — Routekaart-refresh

De v2.20v2.24 releases hebben de complete UI naar een consistente Routekaart-stijl (handoff v0.3) gebracht: ink-borders, paper-cards met chunky radii en drop-shadows, Luckiest Guy-koppen met sky-gradient headers, en zelfde pattern op feedback- en settings-modals (css/feedback.css). De design-tokens staan in css/tokens.css — één bron voor alle kleuren, radii en schaduwen.

Adaptieve highlight-zoom

Per vraag zoomt de kaart in op het actieve item via Leaflet's getBoundsZoom — kleine landen/provincies worden automatisch dichter bekeken dan grote. De polygonTypes-register geeft alleen een veilige bovencap (zoom 12) en padding; de exacte zoom wordt per item berekend vanuit de feature-bounds en de container-grootte (#116).


Wateren (set 5.7)

wateren.geojson

Alle waterlichamen staan in wateren.geojson als GeoJSON FeatureCollection. Elk feature heeft een name property die overeenkomt met de namen in cities.js (set 57).

Type Waterlichamen
LineString Rijn, Waal, Neder-Rijn, Lek, IJssel, Maas, Bergse Maas, Oude Maas, Nieuwe Waterweg, Noordzeekanaal, Amsterdam-Rijnkanaal
Polygon Noordzee, Waddenzee, Oosterschelde, Westerschelde, Eems
fuzzy (ellips) Alle nieuwe zeeën en oceanen (Middellandse Zee, Oostzee, Atlantische Oceaan, Grote Oceaan, Indische Oceaan, Caribische Zee, …)

Zeeën/oceanen: gebruik fuzzy, niet hard polygoon

Een harde zee-polygoon met echte kustlijn vergt per kust OSM-ways chainen, RDP, inlet-overbruggingen en visuele verificatie — de Noordzee (#37, #38) kostte meerdere iteraties en blijft dichtbij land imperfect. Voor alle nieuwe zeeën/oceanen gebruiken we daarom een shape: 'fuzzy' ellips, zo geplaatst en gemaatvoerd dat de ellips niet tegen land aanligt (ruime marge t.o.v. de kust). De Noordzee/Waddenzee/etc. blijven hard polygoon (al gedaan), maar we investeren daar niet meer in tenzij er een concrete blokkerende fout is. De Atlantische Oceaan was eerder een polygoon maar is in v2.16.0 omgezet naar fuzzy om per-set plaatsing mogelijk te maken (zie posBySet hieronder).

Shape-overrides: fuzzy, peak, en posBySet

Items in ALL_WATERS en ALL_PROVINCES kunnen een afwijkende visuele vorm krijgen via het shape-veld. ensureShapeFeatures() in index.html vervangt of injecteert de feature op rendertijd:

shape Rendering Extra velden Gebruikt voor
(geen) Polygon/LineString uit de GeoJSON Standaard (NL-provincies, landen, rivieren, hard polygoon-zeeën)
'fuzzy' Doorzichtige ellips rx, ry (graden), optioneel rot Zeeën/oceanen, gebieden zonder scherpe grens (Himalaya, Sahara), te kleine eilanden (Saba, Sint Eustatius)
'peak' Gekleurde driehoek (bergtop) Mount Everest (set 8.5) en toekomstige bergtoppen

posBySet-override — één entry, per-set plaatsing:

Voor items die in meerdere sets zitten maar per set een ander kijkvlak hebben (typisch oceanen). De base lat/lon/rx/ry/rot geldt als default; posBySet[setNr] overrulet die waarden alleen voor die ene set. buildEllipseFeature(entry, activeSet) leest de override op rendertijd.

{ name: 'Grote Oceaan', lat: 30, lon: 160, shape: 'fuzzy', rx: 30, ry: 25,
  sets: [86, 87, 88, 89],
  posBySet: {
    88: { lat:  -5, lon: 180, rx: 17, ry: 22 },   // Oceanië-perspectief
    89: { lat:  10, lon: -92, rx:  3, ry:  5 },   // Midden-Amerika-perspectief
  },
}

Gebruikt voor Atlantische Oceaan, Grote Oceaan en Indische Oceaan (set 78/88/89 gedeeld).

Klik-op-de-kaart modus (wateren)

De water-quiz ondersteunt ook map-modus. De leerling klikt op de kaart waar het water ligt; de afstand wordt berekend via distanceToWater():

  • LineString: kortste afstand tot een lijnstuk (punt-naar-segment)
  • Polygon: 0 km als het punt binnen het polygoon ligt; anders kortste afstand tot de rand

Drempelwaarden gelden dezelfde als steden (< 20 km correct, 2060 km bijna, > 60 km fout). Tijdens de vraag zijn alle waterlichamen verborgen totdat ze correct geraden zijn.

OSM-datapipeline

Riviercoördinaten komen uit OpenStreetMap via de Overpass API. De pipeline:

data/fetch-overpass.js       → haalt ruwe OSM way-geometrie op (rate-limit-aware)
data/fetch-overpass-retry.js → herhaalpogingen voor mislukte rivieren
data/fetch-rhein-south.js    → specifiek: Rijn Köln→Wesel (Rhein)
data/process-overpass.js     → chain + RDP-vereenvoudiging → processed.json

Stap 1 Fetch:

node data/fetch-overpass.js        # slaat op in data/overpass/*.json
node data/fetch-rhein-south.js     # Rhein-sectie (indien nodig)

Stap 2 Process:

node data/process-overpass.js      # output: data/overpass/processed.json

Instellingen per rivier in process-overpass.js:

Instelling Uitleg
file Ruwe Overpass JSON in data/overpass/
eps RDP-epsilon in graden (0.001 = detail, 0.003 = grof)

Stap 3 Handmatig bijwerken: Kopieer de gewenste coördinaten uit processed.json naar wateren.geojson. Gebruik debug-wateren.html + Playwright voor visuele controle (zie Debug workflow hieronder).

Een rivier verbeteren of toevoegen

  1. Zoek de OSM-naam op (bijv. "Rhein" in Duitsland, "Rijn" in Nederland)
  2. Schrijf een Overpass-query in fetch-overpass.js:
    { name: 'nieuwe-rivier', q: 'way["name"="Naam"]["waterway"="river"](lat1,lon1,lat2,lon2);out geom;' }
    
  3. Voer fetch + process uit
  4. Pas wateren.geojson aan en controleer visueel
  5. Voeg de rivier toe aan cities.js set 57 met lat/lon voor het label

Chaining: het process-script schakelt losse OSM-ways aaneen via een bidirectioneel greedy-algoritme (gap-drempel 0.05°). Bij een gap > 0.05° stopt de keten — voeg dan handmatig een brugpunt toe.

Debug workflow

python3 -m http.server 8080   # serveer vanuit project-root
# open http://localhost:8080/debug-wateren.html

Of via Playwright (headless screenshot):

node -e "
const { chromium } = require('playwright');
(async () => {
  const b = await chromium.launch();
  const p = await b.newPage();
  await p.setViewportSize({ width: 1200, height: 700 });
  await p.goto('http://localhost:8080/debug-wateren.html', { waitUntil: 'networkidle' });
  await p.waitForTimeout(2000);
  await p.evaluate(() => map.setView([52.3, 5.3], 7));
  await p.waitForTimeout(800);
  await p.screenshot({ path: 'debug/debug-wateren-overzicht.png' });
  await b.close();
})();
"

Dev tips

Voor snel handmatig testen kun je via URL-parameters direct de quiz in springen, zonder door het startscherm en mode-select te klikken:

URL Effect
?set=73 Springt naar mode-select van set 7.3
?set=73&mode=mc Start quiz set 7.3 in meerkeuze-modus, fase 0
?set=73&mode=map&phase=2 Start quiz in klik-op-kaart-modus, fase 2 (derde fase)

Modi: mc (meerkeuze), text (typen), map (klik op kaart). Ongeldige of ontbrekende parameters vallen stil terug op het gewone startscherm. Werkt op elke build (dev, preview én productie), maar bedoeld voor dev-testen.


Feedback

Feedback van gebruikers gaat via een ingebouwd formulier naar feedback.php, dat er een issue van maakt op git.jelmer.org en een ntfy-seintje stuurt.


Deploy & CI

  • feature/* branches → ontwikkeling van nieuwe features
  • Push naar staging triggert Forgejo Actions op git.jelmer.org (eigen runner): unit tests + Playwright E2E tests
  • Bij groene tests wordt staging automatisch gemerged naar main
  • main branch → automatisch gedeployed naar Uberspace via rsync → live op topoquiz.com
  • dev branch → automatisch gedeployed naar dev.topoquiz.com (preview op zelfde Uberspace-host)