Verbeter grafische kwaliteit van kleurplaten (v0.4.67-beta) #5

Open
bes-r wants to merge 208 commits from bes-r/coloring-quality-v0.4.67 into main AGit
31 changed files with 548 additions and 143 deletions
Showing only changes of commit 100dc62087 - Show all commits

View file

@ -1 +1 @@
0.3.69-beta 0.3.70-beta

View file

@ -1592,13 +1592,14 @@ body.dark .bd-stage{background:radial-gradient(circle at 50% 20%,var(--surface-2
/* Wereldoriëntatie: natuurherkenning en topografie */ /* Wereldoriëntatie: natuurherkenning en topografie */
.world-app{width:100%;min-height:100%;align-self:flex-start;display:flex;flex-direction:column;justify-content:flex-start;color:var(--ink)} .world-app{width:100%;min-height:100%;align-self:flex-start;display:flex;flex-direction:column;justify-content:flex-start;color:var(--ink)}
.world-top{position:sticky;top:-16px;z-index:6;width:100%;display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px;padding:8px 4px;background:var(--surface);border-bottom:1px solid var(--line)}.world-top>strong{font-size:15px}.world-top>span{padding:6px 10px;border-radius:999px;background:var(--surface-2);color:var(--muted);font-size:12px;font-weight:900;white-space:nowrap} .world-top{position:sticky;top:-16px;z-index:6;width:100%;display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:10px;padding:9px 4px;background:color-mix(in srgb,var(--surface) 94%,transparent);border-bottom:1px solid var(--line);backdrop-filter:blur(10px)}.world-top>strong{font-size:15px}.world-top>span{padding:6px 10px;border-radius:999px;background:var(--surface-2);color:var(--muted);font-size:12px;font-weight:900;white-space:nowrap}
.world-task-card{width:min(100%,650px);margin:0 auto;display:flex;flex-direction:column;align-items:center;gap:12px;padding:16px;border:1px solid var(--line);border-radius:20px;background:var(--surface);box-shadow:var(--shadow-1)}.world-task-card h3{margin:0;text-align:center;font-size:clamp(19px,4cqi,26px)}.world-intro{width:100%;margin:0;padding:9px 12px;border-radius:12px;background:var(--accent-soft);color:var(--accent-ink);font-weight:800;text-align:center} .world-task-card{width:min(100%,680px);margin:0 auto;display:flex;flex-direction:column;align-items:center;gap:13px;padding:18px;border:1px solid color-mix(in srgb,var(--line) 80%,var(--accent));border-radius:24px;background:linear-gradient(155deg,var(--surface),color-mix(in srgb,var(--surface-2) 68%,var(--surface)));box-shadow:0 18px 40px rgba(24,42,66,.12)}.world-task-card>h3{max-width:560px;margin:0;text-align:center;font-size:clamp(19px,4cqi,27px);line-height:1.2}.world-intro{width:100%;margin:0;padding:10px 14px;border-radius:14px;background:linear-gradient(120deg,var(--accent-soft),color-mix(in srgb,var(--accent-soft) 55%,var(--surface)));color:var(--accent-ink);font-weight:850;line-height:1.4;text-align:center}
.world-visual{width:min(100%,330px);display:flex;align-items:center;justify-content:center;gap:10px}.world-visual.mixed{width:min(100%,500px);display:grid;grid-template-columns:repeat(2,minmax(0,1fr))}.world-picture{display:block;width:100%;max-height:230px;object-fit:contain;border-radius:16px;border:1px solid var(--line);background:var(--surface-2)} .world-visual{width:min(100%,460px);display:flex;align-items:flex-start;justify-content:center;gap:12px}.world-visual.mixed{width:min(100%,620px);display:grid;grid-template-columns:repeat(2,minmax(0,1fr))}.world-photo{position:relative;min-width:0;margin:0;overflow:hidden;border:1px solid color-mix(in srgb,var(--line) 75%,var(--ink));border-radius:19px;background:#152432;box-shadow:0 10px 24px rgba(20,36,52,.18)}.world-picture{display:block;width:100%;height:auto;aspect-ratio:4/3;object-fit:cover}.world-photo-badge{position:absolute;left:9px;top:9px;padding:5px 9px;border:1px solid rgba(255,255,255,.35);border-radius:999px;background:rgba(10,27,42,.78);color:#fff;font-size:10px;font-weight:950;letter-spacing:.45px;text-transform:uppercase;box-shadow:0 2px 8px rgba(0,0,0,.18);backdrop-filter:blur(6px)}.world-photo-credit{position:absolute;right:7px;bottom:7px;max-width:calc(100% - 14px);overflow:hidden;padding:4px 7px;border-radius:8px;background:rgba(10,27,42,.8);color:#fff!important;font-size:9px;font-weight:750;line-height:1.2;text-decoration:none;text-overflow:ellipsis;white-space:nowrap;backdrop-filter:blur(6px)}.world-photo-credit:hover,.world-photo-credit:focus-visible{text-decoration:underline;outline:2px solid #fff;outline-offset:1px}
.world-answers{width:min(100%,520px);display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}.world-answers .tbtn{min-width:0;min-height:46px;padding:7px 10px}.world-answers .tbtn.correct{background:var(--green);color:#fff;border-color:transparent}.world-feedback{min-height:24px;margin:0;text-align:center;font-weight:900}.world-feedback.ok{color:var(--green)}.world-feedback.bad{color:var(--red)}.world-actions{display:flex;justify-content:center}.world-fact{max-width:520px;margin:0;padding:9px 12px;border-radius:12px;background:#eef7e9;color:#2f6f3e;font-weight:800;text-align:center} .world-identity{display:flex;flex-direction:column;align-items:center;gap:2px;text-align:center}.world-identity h3{margin:0;font-size:18px;line-height:1.2}.world-identity em{color:var(--muted);font-size:12px;font-family:Georgia,serif;font-weight:650}
.world-gallery{width:100%;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}.world-specimen{min-width:0;display:flex;flex-direction:column;align-items:center;gap:7px;padding:10px;border:1px solid var(--line);border-radius:16px;background:var(--surface)}.world-specimen .world-visual{width:100%}.world-specimen .world-picture{max-height:160px}.world-specimen h3{margin:0;font-size:17px;text-align:center}.world-specimen p{margin:0;color:var(--muted);font-size:12px;font-weight:700;line-height:1.35;text-align:center} .world-answers{width:min(100%,560px);display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:9px}.world-answers .tbtn{min-width:0;min-height:48px;padding:8px 11px;border:1px solid var(--line);background:var(--surface);box-shadow:0 3px 8px rgba(20,36,52,.06)}.world-answers .tbtn:hover{border-color:var(--accent);background:var(--accent-soft)}.world-answers .tbtn.correct{background:var(--green);color:#fff;border-color:transparent}.world-feedback{min-height:24px;margin:0;text-align:center;font-weight:900}.world-feedback.ok{color:var(--green)}.world-feedback.bad{color:var(--red)}.world-actions{display:flex;justify-content:center}.world-fact{width:min(100%,560px);margin:0;padding:11px 14px;border:1px solid color-mix(in srgb,var(--green) 28%,var(--line));border-radius:15px;background:#eef7e9;color:#2f6f3e;text-align:center}.world-fact .world-identity em{color:#527c5b}.world-fact p{margin:6px 0 0;font-weight:800;line-height:1.4}
.topo-card{width:min(100%,700px)}.topo-map-wrap{width:100%;display:flex;justify-content:center;overflow:auto}.topo-map{display:block;width:min(100%,430px);height:auto;max-height:500px;touch-action:manipulation}.topo-water{fill:#e4f4fb}.topo-province{stroke:var(--surface);stroke-width:3;vector-effect:non-scaling-stroke;transition:filter .15s,opacity .15s}.topo-province[role=button]{cursor:pointer}.topo-province[role=button]:hover,.topo-province[role=button]:focus{filter:brightness(.88);outline:none}.topo-province.picked{fill:var(--red)!important}.topo-province.target{fill:var(--green)!important}.topo-label,.topo-city-label{fill:#18324e;paint-order:stroke;stroke:#fff;stroke-width:3px;stroke-linejoin:round;font-size:10px;font-weight:950;text-anchor:middle;pointer-events:none}.topo-city-label{text-anchor:start;font-size:9px}.topo-marker{cursor:pointer}.topo-hit{fill:transparent}.topo-dot{fill:var(--navy);stroke:#fff;stroke-width:2}.topo-marker:hover .topo-dot,.topo-marker:focus .topo-dot{fill:var(--orange);outline:none}.topo-marker.picked .topo-dot{fill:var(--red)}.topo-marker.target .topo-dot{fill:var(--green);r:9} .world-gallery{width:100%;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}.world-specimen{min-width:0;display:flex;flex-direction:column;align-items:center;gap:9px;padding:11px;border:1px solid var(--line);border-radius:20px;background:linear-gradient(155deg,var(--surface),var(--surface-2));box-shadow:0 8px 22px rgba(20,36,52,.08)}.world-specimen .world-visual{width:100%}.world-specimen p{margin:0;padding:0 5px 3px;color:var(--muted);font-size:12px;font-weight:720;line-height:1.4;text-align:center}
body.dark .world-fact{background:#1d3b2a;color:#a9e5b7}body.dark .topo-water{fill:#173145}body.dark .topo-label,body.dark .topo-city-label{fill:#f7fbff;stroke:#18324e} .topo-card{width:min(100%,740px)}.topo-map-wrap{width:100%;display:flex;flex-direction:column;align-items:center;gap:7px;overflow:hidden;padding:10px;border:1px solid var(--line);border-radius:20px;background:linear-gradient(155deg,#edf8fd,#dff2fb)}.topo-map{display:block;width:min(100%,510px);height:auto;max-height:590px;touch-action:manipulation;filter:drop-shadow(0 9px 13px rgba(29,60,76,.13))}.topo-water{fill:#dff3fb}.topo-province{stroke:#fff;stroke-width:4.5;stroke-linejoin:round;vector-effect:non-scaling-stroke;transition:filter .15s,opacity .15s,fill .15s}.topo-province[role=button]{cursor:pointer}.topo-province[role=button]:hover,.topo-province[role=button]:focus{filter:brightness(.88) saturate(1.12);outline:none}.topo-province[role=button]:focus-visible{stroke:var(--navy);stroke-width:8}.topo-province.picked{fill:var(--red)!important}.topo-province.target{fill:var(--green)!important}.topo-label,.topo-city-label{fill:#18324e;paint-order:stroke;stroke:#fff;stroke-width:5px;stroke-linejoin:round;font-size:18px;font-weight:950;text-anchor:middle;pointer-events:none}.topo-city-label{text-anchor:start;font-size:16px}.topo-city-label.left{text-anchor:end}.topo-marker{cursor:pointer}.topo-hit{fill:transparent}.topo-dot{fill:var(--navy);stroke:#fff;stroke-width:4;transition:fill .15s,stroke-width .15s}.topo-marker:hover .topo-dot,.topo-marker:focus .topo-dot{fill:var(--orange);outline:none}.topo-marker:focus-visible .topo-dot{stroke:var(--navy);stroke-width:8}.topo-marker.picked .topo-dot{fill:var(--red)}.topo-marker.target .topo-dot{fill:var(--green);stroke-width:7}.topo-source{align-self:flex-end;color:#3b657b;font-size:9px;font-weight:750;text-decoration:none}.topo-source:hover,.topo-source:focus-visible{text-decoration:underline}
body.dark .world-fact{background:#1d3b2a;color:#a9e5b7}body.dark .world-fact .world-identity em{color:#a9e5b7}body.dark .topo-map-wrap{background:linear-gradient(155deg,#193446,#122c3e)}body.dark .topo-water{fill:#173145}body.dark .topo-label,body.dark .topo-city-label{fill:#f7fbff;stroke:#18324e}body.dark .topo-source{color:#a9cadb}
.clock-app{width:100%;min-height:100%;display:flex;flex-direction:column;justify-content:flex-start;color:var(--ink)} .clock-app{width:100%;min-height:100%;display:flex;flex-direction:column;justify-content:flex-start;color:var(--ink)}
.clock-top{position:sticky;top:-16px;z-index:6;width:100%;display:flex;align-items:center;flex-wrap:wrap;gap:8px;margin-bottom:8px;padding:8px 4px;background:var(--surface);border-bottom:1px solid var(--line)}.clock-level-control{display:flex;align-items:center;gap:6px}.clock-level-control>span{color:var(--muted);font-size:12px;font-weight:900}.clock-levels button{min-width:31px;padding:6px 9px;white-space:nowrap}.clock-mode{width:36px;padding:0;font-size:18px}.clock-score{margin-left:auto;padding:6px 10px;border-radius:999px;background:var(--surface-2);color:var(--muted);font-size:12px;font-weight:900} .clock-top{position:sticky;top:-16px;z-index:6;width:100%;display:flex;align-items:center;flex-wrap:wrap;gap:8px;margin-bottom:8px;padding:8px 4px;background:var(--surface);border-bottom:1px solid var(--line)}.clock-level-control{display:flex;align-items:center;gap:6px}.clock-level-control>span{color:var(--muted);font-size:12px;font-weight:900}.clock-levels button{min-width:31px;padding:6px 9px;white-space:nowrap}.clock-mode{width:36px;padding:0;font-size:18px}.clock-score{margin-left:auto;padding:6px 10px;border-radius:999px;background:var(--surface-2);color:var(--muted);font-size:12px;font-weight:900}
@ -1742,10 +1743,11 @@ body.dark .world-fact{background:#1d3b2a;color:#a9e5b7}body.dark .topo-water{fil
.money-task-card,.clock-task-card,.world-task-card{gap:9px;padding:10px} .money-task-card,.clock-task-card,.world-task-card{gap:9px;padding:10px}
.world-top{top:-10px;gap:5px} .world-top{top:-10px;gap:5px}
.world-gallery{grid-template-columns:1fr} .world-gallery{grid-template-columns:1fr}
.world-picture{max-height:190px} .world-visual.mixed{gap:6px}
.world-specimen .world-picture{max-height:150px} .world-photo-badge{left:6px;top:6px;padding:4px 7px;font-size:8px}
.world-photo-credit{right:5px;bottom:5px;max-width:calc(100% - 10px);font-size:8px}
.topo-map-wrap{padding:6px}
.topo-map{max-height:450px} .topo-map{max-height:450px}
.topo-label,.topo-city-label{font-size:8px}
.money-top,.clock-top{top:-10px;gap:5px} .money-top,.clock-top{top:-10px;gap:5px}
.money-level-control>span,.clock-level-control>span{display:none} .money-level-control>span,.clock-level-control>span{display:none}
.money-shop-task{grid-template-columns:minmax(0,.8fr) minmax(0,1.2fr);gap:7px} .money-shop-task{grid-template-columns:minmax(0,.8fr) minmax(0,1.2fr);gap:7px}

View file

@ -0,0 +1,56 @@
# Wereldoriëntatie beeldbronnen
De lokale WebP-bestanden zijn uitsluitend verkleinde en, waar nodig, gekaderde
afgeleiden van de hieronder genoemde foto's. De soortnamen zijn gecontroleerd
aan de hand van de bestandsbeschrijvingen op Wikimedia Commons.
## Bomen
| Bestand | Bronwerk | Maker | Licentie |
| --- | --- | --- | --- |
| `trees/oak.webp` | [1. English Oak (Quercus robur)](https://commons.wikimedia.org/wiki/File:1._English_Oak_(Quercus_robur)_(3607652442).jpg) | AnemoneProjectors | [CC BY-SA 2.0](https://creativecommons.org/licenses/by-sa/2.0/) |
| `trees/birch.webp` | [Betula pendula winter](https://commons.wikimedia.org/wiki/File:Betula_pendula_winter.jpg) | Maplan | [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) |
| `trees/horse-chestnut.webp` | [Paardenkastanje2](https://commons.wikimedia.org/wiki/File:Paardenkastanje2.jpg) | Toubib | [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) |
| `trees/pine.webp` | [Pinus sylvestris Glenmuick](https://commons.wikimedia.org/wiki/File:Pinus_sylvestris_Glenmuick.jpg) | Hello, I am Bruce | [CC BY-SA 2.0](https://creativecommons.org/licenses/by-sa/2.0/) |
| `trees/beech.webp` | [Fagales Fagus sylvatica](https://commons.wikimedia.org/wiki/File:Fagales_-_Fagus_sylvatica.jpg) | Emőke Dénes | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
| `trees/maple.webp` | [Acer-pseudoplatanus](https://commons.wikimedia.org/wiki/File:Acer-pseudoplatanus.jpg) | MPF | [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) |
| `trees/willow.webp` | [Salix alba an der Isar](https://commons.wikimedia.org/wiki/File:Salix_alba_an_der_Isar.jpg) | Burkhard Mücke | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
| `trees/alder.webp` | [Alnus glutinosa 011](https://commons.wikimedia.org/wiki/File:Alnus_glutinosa_011.jpg) | Willow | [CC BY 2.5](https://creativecommons.org/licenses/by/2.5/) |
## Bladeren en naalden
| Bestand | Bronwerk | Maker | Licentie |
| --- | --- | --- | --- |
| `leaves/oak.webp` | [Zomereik blad Quercus robur](https://commons.wikimedia.org/wiki/File:Zomereik_blad_Quercus_robur.jpg) | Rasbak | [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) |
| `leaves/birch.webp` | [Atlas roslin pl Brzoza brodawkowata 1393 6470](https://commons.wikimedia.org/wiki/File:Atlas_roslin_pl_Brzoza_brodawkowata_1393_6470.jpg) | Joanna Boisse | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
| `leaves/horse-chestnut.webp` | [Aesculus hippocastanum in botanical garden of UKW Bydgoszcz (2)](https://commons.wikimedia.org/wiki/File:Aesculus_hippocastanum_in_botanical_garden_of_UKW_Bydgoszcz_(2).jpg) | Tournasol7 | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) |
| `leaves/pine.webp` | [20151215Pinus sylvestris2](https://commons.wikimedia.org/wiki/File:20151215Pinus_sylvestris2.jpg) | AnRo0002 | [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/) |
| `leaves/beech.webp` | [20130530Hain Rotbuche](https://commons.wikimedia.org/wiki/File:20130530Hain_Rotbuche.jpg) | AnRo0002 | [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/) |
| `leaves/maple.webp` | [Acer pseudoplatanus 003](https://commons.wikimedia.org/wiki/File:Acer_pseudoplatanus_003.jpg) | Willow | [CC BY 2.5](https://creativecommons.org/licenses/by/2.5/) |
| `leaves/willow.webp` | [20160721Salix alba1](https://commons.wikimedia.org/wiki/File:20160721Salix_alba1.jpg) | AnRo0002 | [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/) |
| `leaves/alder.webp` | [20200531Alnus glutinosa2](https://commons.wikimedia.org/wiki/File:20200531Alnus_glutinosa2.jpg) | AnRo0002 | [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/) |
## Vogels
| Bestand | Bronwerk | Maker | Licentie |
| --- | --- | --- | --- |
| `birds/robin.webp` | [Erithacus rubecula, Roodborstje](https://commons.wikimedia.org/wiki/File:Erithacus_rubecula,_Roodborstje.jpg) | Rasbak | [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) |
| `birds/great-tit.webp` | [Parus major m](https://commons.wikimedia.org/wiki/File:Parus_major_m.jpg) | Sławek Staszczuk | [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) |
| `birds/blackbird.webp` | [Solsort](https://commons.wikimedia.org/wiki/File:Solsort.jpg) | Malene Thyssen | [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) |
| `birds/mallard.webp` | [Duisburg 10401IMG 0401](https://commons.wikimedia.org/wiki/File:Duisburg_10401IMG_0401.JPG) | Mbdortmund | [CC BY 3.0](https://creativecommons.org/licenses/by/3.0/) |
| `birds/blue-tit.webp` | [Cyanistes caeruleus 3 Luc Viatour](https://commons.wikimedia.org/wiki/File:Cyanistes_caeruleus_3_Luc_Viatour.jpg) | Luc Viatour | [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) |
| `birds/house-sparrow.webp` | [Huismus, man](https://commons.wikimedia.org/wiki/File:Huismus,_man.jpg) | JrPol | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
| `birds/magpie.webp` | [Сорока (Pica pica), Калининград](https://commons.wikimedia.org/wiki/File:%D0%A1%D0%BE%D1%80%D0%BE%D0%BA%D0%B0_(Pica_pica),_%D0%9A%D0%B0%D0%BB%D0%B8%D0%BD%D0%B8%D0%BD%D0%B3%D1%80%D0%B0%D0%B4.jpg) | Dion Art | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
| `birds/woodpecker.webp` | [Grote bonte specht](https://commons.wikimedia.org/wiki/File:Grote_bonte_specht.JPG) | Usitea | [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) |
## Topografie
`netherlands-provinces.svg` is een vereenvoudigde afgeleide zonder labels van
[Netherlands provinces map blank.svg](https://commons.wikimedia.org/wiki/File:Netherlands_provinces_map_blank.svg)
door Gust Justice, gelicentieerd onder
[CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/). De afgeleide
provinciecontouren worden onder dezelfde licentie verspreid.
De plaatsing van stadspunten is daarnaast gecontroleerd met
[Map provinces Netherlands-en.svg](https://commons.wikimedia.org/wiki/File:Map_provinces_Netherlands-en.svg)
door Alphathon ([CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/)).

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

View file

@ -2,7 +2,7 @@
"use strict"; "use strict";
/* version shown until /api/version resolves (or if the fetch fails, e.g. offline). /* version shown until /api/version resolves (or if the fetch fails, e.g. offline).
Kept in sync by hand with the VERSION file at the repo root on every release. */ Kept in sync by hand with the VERSION file at the repo root on every release. */
const VERSION = "0.3.69-beta"; const VERSION = "0.3.70-beta";
(function(){ (function(){
const tag = document.getElementById("verTag"); const tag = document.getElementById("verTag");
tag.textContent = "v"+VERSION; tag.textContent = "v"+VERSION;
@ -119,14 +119,15 @@ const I18N = {
wg_clock:"Oefenklok", wg_clock_d:"Leer kloktijden op vier niveaus met meerdere leerwijzen en spellen.", wg_clock:"Oefenklok", wg_clock_d:"Leer kloktijden op vier niveaus met meerdere leerwijzen en spellen.",
wg_placevalue:"Plaatswaardetabel", wg_placevalue_d:"Ontdek de waarde van cijfers tot honderdduizend.", wg_placevalue:"Plaatswaardetabel", wg_placevalue_d:"Ontdek de waarde van cijfers tot honderdduizend.",
wg_money:"Geldrekenen", wg_money_d:"Oefen bedragen maken, geld tellen en wisselgeld op drie niveaus.", wg_money:"Geldrekenen", wg_money_d:"Oefen bedragen maken, geld tellen en wisselgeld op drie niveaus.",
wg_trees:"Bomen & bladeren", wg_trees_d:"Leer acht Nederlandse bomen herkennen aan hun vorm en bladeren.", wg_trees:"Bomen & bladeren", wg_trees_d:"Herken acht Nederlandse boomsoorten op echte foto's van bomen en bladeren.",
wg_birds:"Vogels herkennen", wg_birds_d:"Leer acht veelvoorkomende Nederlandse vogels herkennen aan kleur en vorm.", wg_birds:"Vogels herkennen", wg_birds_d:"Herken acht veelvoorkomende Nederlandse vogels op echte natuurfoto's.",
wg_topography:"Topografie Nederland", wg_topography_d:"Oefen provincies, provinciehoofdsteden en belangrijke steden op de kaart.", wg_topography:"Topografie Nederland", wg_topography_d:"Oefen provincies, provinciehoofdsteden en belangrijke steden op een nauwkeurige kaart.",
worldActivity:"Werkvorm", worldLearn:"Leren", worldQuiz:"Quiz", worldLevel:"Niveau", worldTreeView:"Wat zie je?", worldTreeWhole:"Hele boom", worldTreeLeaf:"Blad", worldTreeMixed:"Boom en blad", worldActivity:"Werkvorm", worldLearn:"Leren", worldQuiz:"Quiz", worldLevel:"Niveau", worldTreeView:"Wat zie je?", worldTreeWhole:"Hele boom", worldTreeLeaf:"Blad", worldTreeMixed:"Boom en blad",
worldScore:"Score: {score} · reeks: {streak}", worldExploreHint:"Bekijk de kenmerken en vergelijk de soorten.", worldQuestionTree:"Welke boom zie je?", worldQuestionLeaf:"Bij welke boom hoort dit blad?", worldQuestionBird:"Welke vogel zie je?", worldScore:"Score: {score} · reeks: {streak}", worldExploreHint:"Bekijk de kenmerken en vergelijk de soorten.", worldQuestionTree:"Welke boom zie je?", worldQuestionLeaf:"Bij welke boom hoort dit blad?", worldQuestionBird:"Welke vogel zie je?",
worldCorrect:"Goed herkend! 🌟", worldTryAgain:"Kijk nog eens goed naar de kenmerken.", worldNext:"Volgende opdracht", worldCorrect:"Goed herkend! 🌟", worldTryAgain:"Kijk nog eens goed naar de kenmerken.", worldNext:"Volgende opdracht",
worldPhotoTree:"Boom", worldPhotoLeaf:"Blad of naalden", worldPhotoBird:"Vogel", worldPhotoCredit:"Foto: {author} · {license}",
topoMode:"Oefenen met", topoProvinces:"Provincies", topoCapitals:"Provinciehoofdsteden", topoCities:"Belangrijke steden", topoMapLabel:"Kaart van Nederland", topoMode:"Oefenen met", topoProvinces:"Provincies", topoCapitals:"Provinciehoofdsteden", topoCities:"Belangrijke steden", topoMapLabel:"Kaart van Nederland",
topoExploreHint:"Bekijk waar de namen op de kaart liggen.", topoFindProvince:"Tik op de provincie {name}.", topoFindCapital:"Waar ligt provinciehoofdstad {name}?", topoFindCity:"Waar ligt {name}?", topoExploreHint:"Bekijk de echte provinciegrenzen en vergelijk de ligging van de plaatsen.", topoFindProvince:"Tik op de provincie {name}.", topoFindCapital:"Waar ligt provinciehoofdstad {name}?", topoFindCity:"Waar ligt {name}?", topoMapSource:"Kaart: Gust Justice · CC BY-SA 4.0",
moneyLearningStyle:"Opdracht tonen", moneyStyleAmount:"Als bedrag", moneyStyleWords:"In woorden", moneyStyleBoth:"Bedrag en woorden", moneyLearningStyle:"Opdracht tonen", moneyStyleAmount:"Als bedrag", moneyStyleWords:"In woorden", moneyStyleBoth:"Bedrag en woorden",
moneyGame:"Spel", moneyGameMake:"Bedrag maken", moneyGameCount:"Geld tellen", moneyGameChange:"Wisselgeld", moneyGame:"Spel", moneyGameMake:"Bedrag maken", moneyGameCount:"Geld tellen", moneyGameChange:"Wisselgeld",
moneyCategory:"Koopcategorie", moneyCategoryAll:"Alle producten", moneyCategoryFood:"Eten en drinken", moneyCategorySchool:"Schoolspullen", moneyCategoryPlay:"Spelen en sport", moneyCategory:"Koopcategorie", moneyCategoryAll:"Alle producten", moneyCategoryFood:"Eten en drinken", moneyCategorySchool:"Schoolspullen", moneyCategoryPlay:"Spelen en sport",
@ -386,14 +387,15 @@ const I18N = {
wg_clock:"Practice clock", wg_clock_d:"Learn time at four levels with multiple learning styles and games.", wg_clock:"Practice clock", wg_clock_d:"Learn time at four levels with multiple learning styles and games.",
wg_placevalue:"Place value", wg_placevalue_d:"Explore digit values up to hundred thousands.", wg_placevalue:"Place value", wg_placevalue_d:"Explore digit values up to hundred thousands.",
wg_money:"Money", wg_money_d:"Practise making amounts, counting money and change at three levels.", wg_money:"Money", wg_money_d:"Practise making amounts, counting money and change at three levels.",
wg_trees:"Trees & leaves", wg_trees_d:"Learn to recognise eight common trees by their shape and leaves.", wg_trees:"Trees & leaves", wg_trees_d:"Recognise eight Dutch tree species from real photographs of trees and leaves.",
wg_birds:"Bird identification", wg_birds_d:"Learn to recognise eight common Dutch birds by colour and shape.", wg_birds:"Bird identification", wg_birds_d:"Recognise eight common Dutch birds from real nature photographs.",
wg_topography:"Map of the Netherlands", wg_topography_d:"Practise provinces, provincial capitals and important cities on the map.", wg_topography:"Map of the Netherlands", wg_topography_d:"Practise provinces, provincial capitals and important cities on an accurate map.",
worldActivity:"Activity", worldLearn:"Learn", worldQuiz:"Quiz", worldLevel:"Level", worldTreeView:"What do you see?", worldTreeWhole:"Whole tree", worldTreeLeaf:"Leaf", worldTreeMixed:"Tree and leaf", worldActivity:"Activity", worldLearn:"Learn", worldQuiz:"Quiz", worldLevel:"Level", worldTreeView:"What do you see?", worldTreeWhole:"Whole tree", worldTreeLeaf:"Leaf", worldTreeMixed:"Tree and leaf",
worldScore:"Score: {score} · streak: {streak}", worldExploreHint:"Study the features and compare the species.", worldQuestionTree:"Which tree do you see?", worldQuestionLeaf:"Which tree does this leaf belong to?", worldQuestionBird:"Which bird do you see?", worldScore:"Score: {score} · streak: {streak}", worldExploreHint:"Study the features and compare the species.", worldQuestionTree:"Which tree do you see?", worldQuestionLeaf:"Which tree does this leaf belong to?", worldQuestionBird:"Which bird do you see?",
worldCorrect:"Well recognised! 🌟", worldTryAgain:"Take another close look at the features.", worldNext:"Next task", worldCorrect:"Well recognised! 🌟", worldTryAgain:"Take another close look at the features.", worldNext:"Next task",
worldPhotoTree:"Tree", worldPhotoLeaf:"Leaf or needles", worldPhotoBird:"Bird", worldPhotoCredit:"Photo: {author} · {license}",
topoMode:"Practise", topoProvinces:"Provinces", topoCapitals:"Provincial capitals", topoCities:"Important cities", topoMapLabel:"Map of the Netherlands", topoMode:"Practise", topoProvinces:"Provinces", topoCapitals:"Provincial capitals", topoCities:"Important cities", topoMapLabel:"Map of the Netherlands",
topoExploreHint:"Study where the names belong on the map.", topoFindProvince:"Tap the province {name}.", topoFindCapital:"Where is the provincial capital {name}?", topoFindCity:"Where is {name}?", topoExploreHint:"Study the real provincial boundaries and compare the locations of the places.", topoFindProvince:"Tap the province {name}.", topoFindCapital:"Where is the provincial capital {name}?", topoFindCity:"Where is {name}?", topoMapSource:"Map: Gust Justice · CC BY-SA 4.0",
moneyLearningStyle:"Show task as", moneyStyleAmount:"As an amount", moneyStyleWords:"In words", moneyStyleBoth:"Amount and words", moneyLearningStyle:"Show task as", moneyStyleAmount:"As an amount", moneyStyleWords:"In words", moneyStyleBoth:"Amount and words",
moneyGame:"Game", moneyGameMake:"Make an amount", moneyGameCount:"Count money", moneyGameChange:"Change", moneyGame:"Game", moneyGameMake:"Make an amount", moneyGameCount:"Count money", moneyGameChange:"Change",
moneyCategory:"Shopping category", moneyCategoryAll:"All products", moneyCategoryFood:"Food and drinks", moneyCategorySchool:"School supplies", moneyCategoryPlay:"Play and sports", moneyCategory:"Shopping category", moneyCategoryAll:"All products", moneyCategoryFood:"Food and drinks", moneyCategorySchool:"School supplies", moneyCategoryPlay:"Play and sports",

View file

@ -1,143 +1,442 @@
/* Wereldoriëntatie: natuurherkenning en Nederlandse topografie. */ /* Wereldoriëntatie: echte soortfoto's en geografisch correcte topografie. */
"use strict";
const WORLD_MEDIA_ROOT="img/world/";
const worldMedia=(file,author,license,source)=>Object.freeze({file,author,license,source});
const WORLD_MEDIA=Object.freeze({
oak:{
tree:worldMedia("trees/oak.webp","AnemoneProjectors","CC BY-SA 2.0","https://commons.wikimedia.org/wiki/File:1._English_Oak_(Quercus_robur)_(3607652442).jpg"),
leaf:worldMedia("leaves/oak.webp","Rasbak","CC BY-SA 3.0","https://commons.wikimedia.org/wiki/File:Zomereik_blad_Quercus_robur.jpg"),
},
birch:{
tree:worldMedia("trees/birch.webp","Maplan","CC BY-SA 3.0","https://commons.wikimedia.org/wiki/File:Betula_pendula_winter.jpg"),
leaf:worldMedia("leaves/birch.webp","Joanna Boisse","CC BY-SA 4.0","https://commons.wikimedia.org/wiki/File:Atlas_roslin_pl_Brzoza_brodawkowata_1393_6470.jpg"),
},
horse_chestnut:{
tree:worldMedia("trees/horse-chestnut.webp","Toubib","CC BY-SA 3.0","https://commons.wikimedia.org/wiki/File:Paardenkastanje2.jpg"),
leaf:worldMedia("leaves/horse-chestnut.webp","Tournasol7","CC BY 4.0","https://commons.wikimedia.org/wiki/File:Aesculus_hippocastanum_in_botanical_garden_of_UKW_Bydgoszcz_(2).jpg"),
},
pine:{
tree:worldMedia("trees/pine.webp","Hello, I am Bruce","CC BY-SA 2.0","https://commons.wikimedia.org/wiki/File:Pinus_sylvestris_Glenmuick.jpg"),
leaf:worldMedia("leaves/pine.webp","AnRo0002","CC0 1.0","https://commons.wikimedia.org/wiki/File:20151215Pinus_sylvestris2.jpg"),
},
beech:{
tree:worldMedia("trees/beech.webp","Emőke Dénes","CC BY-SA 4.0","https://commons.wikimedia.org/wiki/File:Fagales_-_Fagus_sylvatica.jpg"),
leaf:worldMedia("leaves/beech.webp","AnRo0002","CC0 1.0","https://commons.wikimedia.org/wiki/File:20130530Hain_Rotbuche.jpg"),
},
maple:{
tree:worldMedia("trees/maple.webp","MPF","CC BY-SA 3.0","https://commons.wikimedia.org/wiki/File:Acer-pseudoplatanus.jpg"),
leaf:worldMedia("leaves/maple.webp","Willow","CC BY 2.5","https://commons.wikimedia.org/wiki/File:Acer_pseudoplatanus_003.jpg"),
},
willow:{
tree:worldMedia("trees/willow.webp","Burkhard Mücke","CC BY-SA 4.0","https://commons.wikimedia.org/wiki/File:Salix_alba_an_der_Isar.jpg"),
leaf:worldMedia("leaves/willow.webp","AnRo0002","CC0 1.0","https://commons.wikimedia.org/wiki/File:20160721Salix_alba1.jpg"),
},
alder:{
tree:worldMedia("trees/alder.webp","Willow","CC BY 2.5","https://commons.wikimedia.org/wiki/File:Alnus_glutinosa_011.jpg"),
leaf:worldMedia("leaves/alder.webp","AnRo0002","CC0 1.0","https://commons.wikimedia.org/wiki/File:20200531Alnus_glutinosa2.jpg"),
},
robin:{bird:worldMedia("birds/robin.webp","Rasbak","CC BY-SA 3.0","https://commons.wikimedia.org/wiki/File:Erithacus_rubecula,_Roodborstje.jpg")},
great_tit:{bird:worldMedia("birds/great-tit.webp","Sławek Staszczuk","CC BY-SA 3.0","https://commons.wikimedia.org/wiki/File:Parus_major_m.jpg")},
blackbird:{bird:worldMedia("birds/blackbird.webp","Malene Thyssen","CC BY-SA 3.0","https://commons.wikimedia.org/wiki/File:Solsort.jpg")},
mallard:{bird:worldMedia("birds/mallard.webp","Mbdortmund","CC BY 3.0","https://commons.wikimedia.org/wiki/File:Duisburg_10401IMG_0401.JPG")},
blue_tit:{bird:worldMedia("birds/blue-tit.webp","Luc Viatour","CC BY-SA 3.0","https://commons.wikimedia.org/wiki/File:Cyanistes_caeruleus_3_Luc_Viatour.jpg")},
house_sparrow:{bird:worldMedia("birds/house-sparrow.webp","JrPol","CC BY-SA 4.0","https://commons.wikimedia.org/wiki/File:Huismus,_man.jpg")},
magpie:{bird:worldMedia("birds/magpie.webp","Dion Art","CC BY-SA 4.0","https://commons.wikimedia.org/wiki/File:%D0%A1%D0%BE%D1%80%D0%BE%D0%BA%D0%B0_(Pica_pica),_%D0%9A%D0%B0%D0%BB%D0%B8%D0%BD%D0%B8%D0%BD%D0%B3%D1%80%D0%B0%D0%B4.jpg")},
woodpecker:{bird:worldMedia("birds/woodpecker.webp","Usitea","CC BY-SA 3.0","https://commons.wikimedia.org/wiki/File:Grote_bonte_specht.JPG")},
});
const WORLD_TREES=Object.freeze([ const WORLD_TREES=Object.freeze([
{id:'oak',nl:'Eik',en:'Oak',tier:1,hintNl:'Herkenbaar aan de ronde bladlobben en eikels.',hintEn:'Recognisable by rounded leaf lobes and acorns.'}, {id:"oak",nl:"Zomereik",en:"Pedunculate oak",latin:"Quercus robur",tier:1,hintNl:"Brede kroon, diep gelobde bladeren en eikels aan een steeltje.",hintEn:"Broad crown, deeply lobed leaves and acorns on a stalk."},
{id:'birch',nl:'Berk',en:'Birch',tier:1,hintNl:'Witte bast en driehoekige blaadjes met een gezaagde rand.',hintEn:'White bark and triangular leaves with a serrated edge.'}, {id:"birch",nl:"Ruwe berk",en:"Silver birch",latin:"Betula pendula",tier:1,hintNl:"Witte bast met donkere strepen en driehoekige, gezaagde blaadjes.",hintEn:"White bark with dark markings and triangular, serrated leaves."},
{id:'horse_chestnut',nl:'Paardenkastanje',en:'Horse chestnut',tier:1,hintNl:'Vijf tot zeven grote blaadjes vormen samen één handvormig blad.',hintEn:'Five to seven large leaflets form one hand-shaped leaf.'}, {id:"horse_chestnut",nl:"Witte paardenkastanje",en:"Horse chestnut",latin:"Aesculus hippocastanum",tier:1,hintNl:"Vijf tot zeven grote blaadjes vormen samen één handvormig blad.",hintEn:"Five to seven large leaflets form one hand-shaped leaf."},
{id:'pine',nl:'Grove den',en:'Scots pine',tier:1,hintNl:'Naalden staan per twee bij elkaar en de boom draagt kegels.',hintEn:'Needles grow in pairs and the tree carries cones.'}, {id:"pine",nl:"Grove den",en:"Scots pine",latin:"Pinus sylvestris",tier:1,hintNl:"Oranjebruine schors bovenin; de blauwgroene naalden staan per twee.",hintEn:"Orange-brown upper bark; blue-green needles grow in pairs."},
{id:'beech',nl:'Beuk',en:'Beech',tier:2,hintNl:'Gladgrijze stam en ovale blaadjes met een golvende rand.',hintEn:'Smooth grey trunk and oval leaves with a wavy edge.'}, {id:"beech",nl:"Beuk",en:"European beech",latin:"Fagus sylvatica",tier:2,hintNl:"Gladde grijze stam en glanzende ovale bladeren met een golvende rand.",hintEn:"Smooth grey trunk and glossy oval leaves with a wavy edge."},
{id:'maple',nl:'Esdoorn',en:'Maple',tier:2,hintNl:'Puntig handvormig blad en gevleugelde zaden.',hintEn:'Pointed hand-shaped leaf and winged seeds.'}, {id:"maple",nl:"Gewone esdoorn",en:"Sycamore maple",latin:"Acer pseudoplatanus",tier:2,hintNl:"Groot handvormig blad met vijf puntige lobben en gevleugelde zaden.",hintEn:"Large hand-shaped leaf with five pointed lobes and winged seeds."},
{id:'willow',nl:'Wilg',en:'Willow',tier:3,hintNl:'Lange smalle blaadjes aan vaak hangende takken.',hintEn:'Long narrow leaves on often drooping branches.'}, {id:"willow",nl:"Schietwilg",en:"White willow",latin:"Salix alba",tier:3,hintNl:"Brede, losse kroon en lange smalle bladeren met een zilverige onderkant.",hintEn:"Broad open crown and long narrow leaves with a silvery underside."},
{id:'alder',nl:'Els',en:'Alder',tier:3,hintNl:'Ronde blaadjes met een ingedeukte top en kleine houtige propjes.',hintEn:'Rounded leaves with a notched tip and small woody cones.'}, {id:"alder",nl:"Zwarte els",en:"Black alder",latin:"Alnus glutinosa",tier:3,hintNl:"Ronde bladeren met een ingedeukte top en kleine houtige elzenproppen.",hintEn:"Rounded leaves with a notched tip and small woody alder cones."},
]); ]);
const WORLD_BIRDS=Object.freeze([ const WORLD_BIRDS=Object.freeze([
{id:'robin',nl:'Roodborst',en:'European robin',tier:1,body:'#8a6a4b',head:'#786047',wing:'#6e543e',breast:'#ef6c32',bill:'#33261d',hintNl:'Oranjerode borst en gezicht, met een bruine rug.',hintEn:'Orange-red breast and face with a brown back.'}, {id:"robin",nl:"Roodborst",en:"European robin",latin:"Erithacus rubecula",tier:1,hintNl:"Oranjerode borst en gezicht, met een olijfbruine rug.",hintEn:"Orange-red breast and face with an olive-brown back."},
{id:'great_tit',nl:'Koolmees',en:'Great tit',tier:1,body:'#f2cf3f',head:'#18222b',wing:'#4b6670',breast:'#f2cf3f',bill:'#222',hintNl:'Zwarte kop, witte wangen en een zwarte streep over de gele buik.',hintEn:'Black head, white cheeks and a black stripe down the yellow belly.'}, {id:"great_tit",nl:"Koolmees",en:"Great tit",latin:"Parus major",tier:1,hintNl:"Zwarte kop, witte wangen en een zwarte streep over de gele buik.",hintEn:"Black head, white cheeks and a black stripe down the yellow belly."},
{id:'blackbird',nl:'Merel',en:'Common blackbird',tier:1,body:'#22262b',head:'#1b1e22',wing:'#30343a',breast:'#25292e',bill:'#f0a521',hintNl:'Het mannetje is zwart met een opvallend gele snavel.',hintEn:'The male is black with a striking yellow bill.'}, {id:"blackbird",nl:"Merel",en:"Common blackbird",latin:"Turdus merula",tier:1,hintNl:"Het mannetje is helemaal zwart met een gele snavel en gele oogring.",hintEn:"The male is all black with a yellow bill and yellow eye-ring."},
{id:'mallard',nl:'Wilde eend',en:'Mallard',tier:1,body:'#a8a9a2',head:'#14604e',wing:'#766953',breast:'#7b4832',bill:'#e2b43f',hintNl:'Het mannetje heeft een groene kop, witte halsring en bruine borst.',hintEn:'The male has a green head, white neck ring and brown breast.'}, {id:"mallard",nl:"Wilde eend",en:"Mallard",latin:"Anas platyrhynchos",tier:1,hintNl:"Het mannetje heeft een groene kop, witte halsring en kastanjebruine borst.",hintEn:"The male has a green head, white neck ring and chestnut breast."},
{id:'blue_tit',nl:'Pimpelmees',en:'Blue tit',tier:2,body:'#f3d85a',head:'#3a78a7',wing:'#39759a',breast:'#f3d85a',bill:'#30343c',hintNl:'Blauw petje en vleugels, witte wangen en een gele buik.',hintEn:'Blue cap and wings, white cheeks and a yellow belly.'}, {id:"blue_tit",nl:"Pimpelmees",en:"Blue tit",latin:"Cyanistes caeruleus",tier:2,hintNl:"Blauw petje en vleugels, witte wangen en een gele buik.",hintEn:"Blue cap and wings, white cheeks and a yellow belly."},
{id:'house_sparrow',nl:'Huismus',en:'House sparrow',tier:2,body:'#9a7654',head:'#80654e',wing:'#6e5038',breast:'#b79a7c',bill:'#403329',hintNl:'Bruin gestreepte rug; het mannetje heeft een zwarte bef.',hintEn:'Brown streaked back; the male has a black bib.'}, {id:"house_sparrow",nl:"Huismus",en:"House sparrow",latin:"Passer domesticus",tier:2,hintNl:"Bruin gestreepte rug; het mannetje heeft een grijze kruin en zwarte bef.",hintEn:"Brown streaked back; the male has a grey crown and black bib."},
{id:'magpie',nl:'Ekster',en:'Eurasian magpie',tier:3,body:'#f4f5f5',head:'#182127',wing:'#172a33',breast:'#f4f5f5',bill:'#20262a',hintNl:'Zwart-wit verenkleed en een opvallend lange donkere staart.',hintEn:'Black-and-white plumage and a strikingly long dark tail.'}, {id:"magpie",nl:"Ekster",en:"Eurasian magpie",latin:"Pica pica",tier:3,hintNl:"Zwart-wit verenkleed met blauwgroene glans en een opvallend lange staart.",hintEn:"Black-and-white plumage with a blue-green sheen and a very long tail."},
{id:'woodpecker',nl:'Grote bonte specht',en:'Great spotted woodpecker',tier:3,body:'#f4f1e8',head:'#20262b',wing:'#1f252a',breast:'#eee9dc',bill:'#373a3b',hintNl:'Zwart-wit met een rode vlek onder de staart.',hintEn:'Black and white with a red patch beneath the tail.'}, {id:"woodpecker",nl:"Grote bonte specht",en:"Great spotted woodpecker",latin:"Dendrocopos major",tier:3,hintNl:"Zwart-wit met rode veren onder de staart; het mannetje heeft rood in de nek.",hintEn:"Black and white with red beneath the tail; the male has red on the nape."},
]); ]);
const NL_PROVINCES=Object.freeze([ const NL_PROVINCES=Object.freeze([
{id:'friesland',nl:'Friesland',en:'Friesland',capital:'leeuwarden',tier:2,color:'#7bc8e8',d:'M90 85L95 55 145 38 205 45 235 75 220 115 175 135 125 115Z',x:158,y:82}, {id:"friesland",shape:"friesland",nl:"Friesland",en:"Friesland",capital:"leeuwarden",tier:2,color:"#93c5fd",x:518,y:166},
{id:'groningen',nl:'Groningen',en:'Groningen',capital:'groningen_city',tier:2,color:'#ef8d71',d:'M205 45L265 30 330 50 315 90 270 105 235 75Z',x:275,y:67}, {id:"groningen",shape:"groningen",nl:"Groningen",en:"Groningen",capital:"groningen_city",tier:2,color:"#fca5a5",x:615,y:72},
{id:'drenthe',nl:'Drenthe',en:'Drenthe',capital:'assen',tier:3,color:'#e9c66b',d:'M220 115L270 105 310 125 300 180 245 190 220 160Z',x:266,y:145}, {id:"drenthe",shape:"drenthe",nl:"Drenthe",en:"Drenthe",capital:"assen",tier:3,color:"#fde68a",x:611,y:222},
{id:'overijssel',nl:'Overijssel',en:'Overijssel',capital:'zwolle',tier:2,color:'#78c49b',d:'M245 190L300 180 330 210 310 260 260 270 225 235Z',x:282,y:222}, {id:"overijssel",shape:"overijssel",nl:"Overijssel",en:"Overijssel",capital:"zwolle",tier:2,color:"#c4b5fd",x:598,y:351},
{id:'flevoland',nl:'Flevoland',en:'Flevoland',capital:'lelystad',tier:3,color:'#b5d969',d:'M150 170L215 160 245 190 225 235 175 245 145 210Z',x:195,y:202}, {id:"flevoland",shape:"flevoland",nl:"Flevoland",en:"Flevoland",capital:"lelystad",tier:3,color:"#fdba74",x:465,y:272},
{id:'noord_holland',nl:'Noord-Holland',en:'North Holland',capital:'haarlem',tier:1,color:'#f3a9c6',d:'M90 85L125 115 150 170 145 210 165 245 145 285 105 270 85 205 75 145Z',x:112,y:198}, {id:"noord_holland",shape:"noord-holland",nl:"Noord-Holland",en:"North Holland",capital:"haarlem",tier:1,color:"#f9a8d4",x:308,y:244},
{id:'utrecht',nl:'Utrecht',en:'Utrecht',capital:'utrecht_city',tier:1,color:'#d0a6df',d:'M145 245L175 245 210 265 200 305 160 305 135 280Z',x:174,y:276}, {id:"utrecht",shape:"utrecht",nl:"Utrecht",en:"Utrecht",capital:"utrecht_city",tier:1,color:"#d8b4fe",x:393,y:471},
{id:'gelderland',nl:'Gelderland',en:'Gelderland',capital:'arnhem',tier:1,color:'#f0aa62',d:'M210 265L260 270 310 260 315 310 275 345 205 335 200 305Z',x:258,y:302}, {id:"gelderland",shape:"gelderland",nl:"Gelderland",en:"Gelderland",capital:"arnhem",tier:1,color:"#86efac",x:493,y:452},
{id:'zuid_holland',nl:'Zuid-Holland',en:'South Holland',capital:'den_haag',tier:1,color:'#7fb2e6',d:'M105 270L145 285 160 305 205 335 175 355 115 345 75 315Z',x:132,y:318}, {id:"zuid_holland",shape:"zuid-holland",nl:"Zuid-Holland",en:"South Holland",capital:"den_haag",tier:1,color:"#67e8f9",x:245,y:451},
{id:'zeeland',nl:'Zeeland',en:'Zeeland',capital:'middelburg',tier:3,color:'#86cfbf',d:'M45 326L90 320 112 337 82 349 38 342ZM35 351L78 350 105 365 75 378 30 370ZM55 382L103 378 120 391 82 402 45 395Z',x:73,y:369}, {id:"zeeland",shape:"zeeland",nl:"Zeeland",en:"Zeeland",capital:"middelburg",tier:3,color:"#5eead4",x:132,y:698},
{id:'noord_brabant',nl:'Noord-Brabant',en:'North Brabant',capital:'den_bosch',tier:1,color:'#e88c91',d:'M115 345L175 355 205 335 275 345 285 385 250 410 175 400 120 380Z',x:202,y:376}, {id:"noord_brabant",shape:"noord-brabant",nl:"Noord-Brabant",en:"North Brabant",capital:"den_bosch",tier:1,color:"#fcd34d",x:367,y:599},
{id:'limburg',nl:'Limburg',en:'Limburg',capital:'maastricht',tier:1,color:'#e6c85f',d:'M250 410L285 385 300 420 285 450 295 485 278 520 255 490 260 455 242 430Z',x:272,y:458}, {id:"limburg",shape:"limburg",nl:"Limburg",en:"Limburg",capital:"maastricht",tier:1,color:"#a7f3d0",x:489,y:714},
]); ]);
const NL_CITIES=Object.freeze([ const NL_CITIES=Object.freeze([
{id:'leeuwarden',nl:'Leeuwarden',en:'Leeuwarden',province:'friesland',capital:true,tier:2,x:150,y:72}, {id:"leeuwarden",nl:"Leeuwarden",en:"Leeuwarden",province:"friesland",capital:true,tier:2,x:467,y:119},
{id:'groningen_city',nl:'Groningen',en:'Groningen',province:'groningen',capital:true,tier:2,x:281,y:66}, {id:"groningen_city",nl:"Groningen",en:"Groningen",province:"groningen",capital:true,tier:2,x:612,y:112},
{id:'assen',nl:'Assen',en:'Assen',province:'drenthe',capital:true,tier:3,x:264,y:140}, {id:"assen",nl:"Assen",en:"Assen",province:"drenthe",capital:true,tier:3,x:612,y:175},
{id:'zwolle',nl:'Zwolle',en:'Zwolle',province:'overijssel',capital:true,tier:2,x:266,y:218}, {id:"zwolle",nl:"Zwolle",en:"Zwolle",province:"overijssel",capital:true,tier:2,x:538,y:332},
{id:'lelystad',nl:'Lelystad',en:'Lelystad',province:'flevoland',capital:true,tier:3,x:195,y:203}, {id:"lelystad",nl:"Lelystad",en:"Lelystad",province:"flevoland",capital:true,tier:3,x:417,y:327},
{id:'haarlem',nl:'Haarlem',en:'Haarlem',province:'noord_holland',capital:true,tier:1,x:119,y:250}, {id:"haarlem",nl:"Haarlem",en:"Haarlem",province:"noord_holland",capital:true,tier:1,x:261,y:371},
{id:'utrecht_city',nl:'Utrecht',en:'Utrecht',province:'utrecht',capital:true,tier:1,x:173,y:278}, {id:"utrecht_city",nl:"Utrecht",en:"Utrecht",province:"utrecht",capital:true,tier:1,x:355,y:460},
{id:'arnhem',nl:'Arnhem',en:'Arnhem',province:'gelderland',capital:true,tier:1,x:270,y:310}, {id:"arnhem",nl:"Arnhem",en:"Arnhem",province:"gelderland",capital:true,tier:1,x:500,y:486},
{id:'den_haag',nl:'Den Haag',en:'The Hague',province:'zuid_holland',capital:true,tier:1,x:96,y:312}, {id:"den_haag",nl:"Den Haag",en:"The Hague",province:"zuid_holland",capital:true,tier:1,x:192,y:460,side:"left"},
{id:'middelburg',nl:'Middelburg',en:'Middelburg',province:'zeeland',capital:true,tier:3,x:69,y:374}, {id:"middelburg",nl:"Middelburg",en:"Middelburg",province:"zeeland",capital:true,tier:3,x:65,y:635},
{id:'den_bosch',nl:"'s-Hertogenbosch",en:"'s-Hertogenbosch",province:'noord_brabant',capital:true,tier:1,x:207,y:369}, {id:"den_bosch",nl:"'s-Hertogenbosch",en:"'s-Hertogenbosch",province:"noord_brabant",capital:true,tier:1,x:388,y:577},
{id:'maastricht',nl:'Maastricht',en:'Maastricht',province:'limburg',capital:true,tier:1,x:275,y:493}, {id:"maastricht",nl:"Maastricht",en:"Maastricht",province:"limburg",capital:true,tier:1,x:463,y:836},
{id:'amsterdam',nl:'Amsterdam',en:'Amsterdam',province:'noord_holland',capital:false,tier:1,x:139,y:246}, {id:"amsterdam",nl:"Amsterdam",en:"Amsterdam",province:"noord_holland",capital:false,tier:1,x:311,y:373},
{id:'rotterdam',nl:'Rotterdam',en:'Rotterdam',province:'zuid_holland',capital:false,tier:1,x:120,y:333}, {id:"rotterdam",nl:"Rotterdam",en:"Rotterdam",province:"zuid_holland",capital:false,tier:1,x:234,y:508},
{id:'eindhoven',nl:'Eindhoven',en:'Eindhoven',province:'noord_brabant',capital:false,tier:1,x:230,y:395}, {id:"eindhoven",nl:"Eindhoven",en:"Eindhoven",province:"noord_brabant",capital:false,tier:1,x:424,y:653},
{id:'nijmegen',nl:'Nijmegen',en:'Nijmegen',province:'gelderland',capital:false,tier:1,x:285,y:330}, {id:"nijmegen",nl:"Nijmegen",en:"Nijmegen",province:"gelderland",capital:false,tier:1,x:497,y:536},
{id:'enschede',nl:'Enschede',en:'Enschede',province:'overijssel',capital:false,tier:2,x:315,y:230}, {id:"enschede",nl:"Enschede",en:"Enschede",province:"overijssel",capital:false,tier:2,x:688,y:409,side:"left"},
{id:'alkmaar',nl:'Alkmaar',en:'Alkmaar',province:'noord_holland',capital:false,tier:2,x:105,y:185}, {id:"alkmaar",nl:"Alkmaar",en:"Alkmaar",province:"noord_holland",capital:false,tier:2,x:283,y:296},
{id:'breda',nl:'Breda',en:'Breda',province:'noord_brabant',capital:false,tier:3,x:158,y:383}, {id:"breda",nl:"Breda",en:"Breda",province:"noord_brabant",capital:false,tier:3,x:286,y:611,side:"left"},
{id:'tilburg',nl:'Tilburg',en:'Tilburg',province:'noord_brabant',capital:false,tier:3,x:193,y:391}, {id:"tilburg",nl:"Tilburg",en:"Tilburg",province:"noord_brabant",capital:false,tier:3,x:347,y:625},
]); ]);
const worldName=item=>LANG==='nl'?item.nl:item.en; const worldName=item=>LANG==="nl"?item.nl:item.en;
const worldHint=item=>LANG==='nl'?item.hintNl:item.hintEn; const worldHint=item=>LANG==="nl"?item.hintNl:item.hintEn;
const worldSvgUrl=svg=>'data:image/svg+xml;charset=utf-8,'+encodeURIComponent(svg); const worldShuffle=items=>{
const worldShuffle=items=>{const copy=[...items];for(let i=copy.length-1;i>0;i--){const j=Math.floor(Math.random()*(i+1));[copy[i],copy[j]]=[copy[j],copy[i]]}return copy}; const copy=[...items];
const worldSelect=(value,items)=>{const select=document.createElement('select');items.forEach(([id,key])=>select.add(new Option(T(key),id)));select.value=value;return select}; for(let i=copy.length-1;i>0;i--){
const j=Math.floor(Math.random()*(i+1));
[copy[i],copy[j]]=[copy[j],copy[i]];
}
return copy;
};
const worldSelect=(value,items)=>{
const select=document.createElement("select");
items.forEach(([id,key])=>select.add(new Option(T(key),id)));
select.value=value;
return select;
};
const worldLevelPool=(items,level)=>items.filter(item=>item.tier<=level); const worldLevelPool=(items,level)=>items.filter(item=>item.tier<=level);
function treeSvg(item,view){ function worldPhoto(item,type,view){
const crowns={ const kind=type==="birds"?"bird":view;
oak:'<circle cx="82" cy="82" r="42"/><circle cx="132" cy="64" r="48"/><circle cx="174" cy="90" r="40"/><circle cx="125" cy="103" r="54"/>', const media=WORLD_MEDIA[item.id]?.[kind];
birch:'<ellipse cx="105" cy="76" rx="50" ry="62"/><ellipse cx="150" cy="82" rx="45" ry="68"/>', const figure=document.createElement("figure");
horse_chestnut:'<circle cx="78" cy="86" r="40"/><circle cx="120" cy="57" r="48"/><circle cx="168" cy="84" r="43"/><ellipse cx="122" cy="102" rx="72" ry="48"/>', figure.className="world-photo world-photo-"+kind;
pine:'<path d="M120 25L58 132H182Z"/><path d="M120 55L45 155H195Z"/>', const image=document.createElement("img");
beech:'<ellipse cx="120" cy="84" rx="78" ry="66"/>', image.className="world-picture";
maple:'<circle cx="75" cy="83" r="41"/><circle cx="120" cy="62" r="52"/><circle cx="169" cy="85" r="42"/><circle cx="120" cy="103" r="55"/>', image.alt=T(kind==="bird"?"worldPhotoBird":kind==="leaf"?"worldPhotoLeaf":"worldPhotoTree");
willow:'<ellipse cx="120" cy="72" rx="72" ry="50"/><path d="M62 78Q55 132 72 160M88 75Q80 135 92 165M120 72Q116 140 122 168M151 76Q160 134 150 164M178 82Q188 132 174 157" fill="none" stroke="#4b8d54" stroke-width="9" stroke-linecap="round"/>', image.draggable=false;
alder:'<ellipse cx="90" cy="82" rx="52" ry="58"/><ellipse cx="150" cy="80" rx="55" ry="61"/><ellipse cx="120" cy="105" rx="65" ry="45"/>', image.loading="lazy";
}; image.decoding="async";
const leaves={ image.src=WORLD_MEDIA_ROOT+media.file;
oak:'<path d="M120 24C105 34 110 50 96 53C80 57 88 73 76 81C65 91 80 103 78 116C76 132 96 132 105 143C112 152 116 162 120 171C124 158 130 149 138 141C148 131 166 131 163 115C160 102 175 92 164 81C153 72 161 57 145 53C132 50 136 34 120 24Z" fill="#5ca554"/><path d="M120 35V166M120 76L91 61M120 96L151 77M120 118L91 113M120 137L149 126" stroke="#35763d" stroke-width="4" fill="none"/>', const badge=document.createElement("span");
birch:'<path d="M120 25L178 122Q147 151 120 170Q91 151 62 122Z" fill="#72b85d"/><path d="M120 36V169M120 70L88 105M120 88L151 116M120 110L93 133" stroke="#397d42" stroke-width="4" fill="none"/>', badge.className="world-photo-badge";
horse_chestnut:'<g fill="#4f9b4e" stroke="#34723b" stroke-width="3"><ellipse cx="120" cy="60" rx="18" ry="43"/><ellipse cx="83" cy="80" rx="17" ry="42" transform="rotate(-42 83 80)"/><ellipse cx="157" cy="80" rx="17" ry="42" transform="rotate(42 157 80)"/><ellipse cx="75" cy="122" rx="16" ry="38" transform="rotate(-68 75 122)"/><ellipse cx="165" cy="122" rx="16" ry="38" transform="rotate(68 165 122)"/></g><path d="M120 83V173" stroke="#34723b" stroke-width="5"/>', badge.textContent=T(kind==="bird"?"worldPhotoBird":kind==="leaf"?"worldPhotoLeaf":"worldPhotoTree");
pine:'<g stroke="#2f7747" stroke-width="4" stroke-linecap="round"><path d="M120 28V166"/><path d="M120 62L72 34M120 62L168 34M120 83L61 65M120 83L179 65M120 106L63 103M120 106L177 103M120 128L76 143M120 128L164 143"/></g><ellipse cx="155" cy="131" rx="13" ry="22" fill="#8b633e"/>', const credit=document.createElement("a");
beech:'<ellipse cx="120" cy="91" rx="58" ry="70" fill="#6eb05a"/><path d="M120 28V170M120 63L88 83M120 82L153 105M120 107L88 130M120 128L147 148" stroke="#3c7942" stroke-width="4" fill="none"/>', credit.className="world-photo-credit";
maple:'<path d="M120 20L136 59L168 42L158 78L200 85L163 108L179 139L138 130L120 171L102 130L61 139L77 108L40 85L82 78L72 42L104 59Z" fill="#62a94e"/><path d="M120 38V169M120 94L73 80M120 94L168 81M120 111L82 133M120 111L158 133" stroke="#39743c" stroke-width="4" fill="none"/>', credit.href=media.source;
willow:'<path d="M120 25C155 45 161 105 120 169C79 105 85 45 120 25Z" fill="#78b866"/><path d="M120 32V166M120 63L100 85M120 82L141 105M120 108L101 132" stroke="#447c44" stroke-width="4" fill="none"/>', credit.target="_blank";
alder:'<path d="M120 29C163 36 181 75 168 120C158 151 135 163 120 170C105 163 82 151 72 120C59 75 77 36 120 29Z" fill="#6daa54"/><path d="M120 35V169M120 73L88 95M120 93L152 116M120 116L93 139" stroke="#3c7640" stroke-width="4" fill="none"/><path d="M105 29Q120 43 135 29" stroke="#3c7640" stroke-width="4" fill="none"/>', credit.rel="noopener noreferrer";
}; credit.textContent=T("worldPhotoCredit").replace("{author}",media.author).replace("{license}",media.license);
if(view==='leaf')return worldSvgUrl(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 180"><rect width="240" height="180" rx="18" fill="#edf7e8"/>${leaves[item.id]}</svg>`); figure.append(image,badge,credit);
const trunk=item.id==='birch'?'#f2eee2':'#815939',marks=item.id==='birch'?'<path d="M108 78h24M105 105h29M109 132h21" stroke="#5e6060" stroke-width="4"/>':''; return figure;
return worldSvgUrl(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 180"><rect width="240" height="180" rx="18" fill="#dff1fb"/><path d="M0 153Q70 135 120 149T240 150V180H0Z" fill="#b8d891"/><path d="M102 62L96 157H145L137 62Z" fill="${trunk}"/>${marks}<g fill="${item.id==='pine'?'#2f7547':'#5c9f4e'}">${crowns[item.id]}</g></svg>`);
} }
function birdSvg(item){ function worldVisual(item,type,view){
const extras={ const visual=document.createElement("div");
great_tit:'<circle cx="157" cy="61" r="15" fill="#fff"/><path d="M139 89Q151 118 151 148" stroke="#1e2427" stroke-width="8"/>', visual.className="world-visual "+type+(view==="mixed"?" mixed":"");
blue_tit:'<circle cx="157" cy="62" r="14" fill="#fff"/><path d="M138 55Q154 42 171 52" stroke="#2f6f9d" stroke-width="8"/>', if(type==="trees"&&view==="mixed")visual.append(worldPhoto(item,type,"tree"),worldPhoto(item,type,"leaf"));
house_sparrow:'<path d="M135 82Q149 92 154 117" stroke="#302b27" stroke-width="13"/><path d="M78 90l55 34M88 77l54 31" stroke="#d0b08b" stroke-width="4"/>', else visual.append(worldPhoto(item,type,view));
mallard:'<path d="M136 79Q150 91 169 82" stroke="#fff" stroke-width="7"/>', return visual;
magpie:'<path d="M70 108L20 151L88 128Z" fill="#17323b"/><ellipse cx="115" cy="110" rx="32" ry="43" fill="#fff"/>',
woodpecker:'<path d="M97 82L126 119" stroke="#fff" stroke-width="14"/><path d="M82 80L104 95M94 68L118 83" stroke="#fff" stroke-width="7"/><path d="M88 130Q105 151 125 142" fill="#d94137"/>',
robin:'<ellipse cx="145" cy="97" rx="31" ry="39" fill="#ef6c32"/>',
blackbird:'',
}[item.id]||'';
const tail=item.id==='magpie'?'M82 111L26 158L101 132Z':'M84 112L45 142L98 130Z';
return worldSvgUrl(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 180"><rect width="240" height="180" rx="18" fill="#e7f3fa"/><path d="M22 150Q112 132 218 147" stroke="#7d5837" stroke-width="10" fill="none" stroke-linecap="round"/><path d="${tail}" fill="${item.id==='magpie'?'#172a33':item.wing}"/><ellipse cx="116" cy="105" rx="60" ry="43" fill="${item.body}" transform="rotate(7 116 105)"/><ellipse cx="95" cy="105" rx="34" ry="27" fill="${item.wing}" transform="rotate(12 95 105)"/><ellipse cx="139" cy="111" rx="31" ry="35" fill="${item.breast}"/><circle cx="157" cy="65" r="34" fill="${item.head}"/><path d="M186 62L218 71L187 79Z" fill="${item.bill}"/>${extras}<circle cx="168" cy="57" r="5" fill="#fff"/><circle cx="169" cy="58" r="2.5" fill="#15191c"/><path d="M112 141v18M137 141v18" stroke="#6b4b34" stroke-width="4"/><path d="M104 159h18M130 159h18" stroke="#6b4b34" stroke-width="3"/></svg>`);
} }
function worldPicture(item,type,view){const img=document.createElement('img');img.className='world-picture';img.alt=worldName(item);img.draggable=false;img.src=type==='trees'?treeSvg(item,view):birdSvg(item);return img} function worldIdentity(item){
function worldVisual(item,type,view){const visual=document.createElement('div');visual.className='world-visual'+(view==='mixed'?' mixed':'');if(type==='trees'&&view==='mixed')visual.append(worldPicture(item,type,'tree'),worldPicture(item,type,'leaf'));else visual.append(worldPicture(item,type,view));return visual} const identity=document.createElement("div");
identity.className="world-identity";
const name=document.createElement("h3");
name.textContent=worldName(item);
const latin=document.createElement("em");
latin.textContent=item.latin;
identity.append(name,latin);
return identity;
}
function mountWorldQuiz(root,init,config){ function mountWorldQuiz(root,init,config){
const S=Object.assign({activity:'quiz',level:1,view:config.type==='trees'?'mixed':'bird',score:0,streak:0,target:'',taskView:'',options:[],solved:false,feedback:''},init||{});S.level=Math.max(1,Math.min(3,+S.level||1)); const S=Object.assign({
const activity=worldSelect(S.activity,[['learn','worldLearn'],['quiz','worldQuiz']]),level=worldSelect(String(S.level),[['1','lvl1'],['2','lvl2'],['3','lvl3']]),stage=document.createElement('div'),settings=[field(T('worldActivity'),activity),field(T('worldLevel'),level)]; activity:"quiz",level:1,view:config.type==="trees"?"mixed":"bird",
let view;if(config.type==='trees'){view=worldSelect(S.view,[['tree','worldTreeWhole'],['leaf','worldTreeLeaf'],['mixed','worldTreeMixed']]);settings.push(field(T('worldTreeView'),view))} score:0,streak:0,target:"",taskView:"",options:[],solved:false,feedback:"",
const pool=()=>worldLevelPool(config.items,S.level),current=()=>config.items.find(item=>item.id===S.target)||pool()[0]; },init||{});
const newTask=()=>{const items=pool(),previous=S.target;let target=items[Math.floor(Math.random()*items.length)];if(items.length>1)while(target.id===previous)target=items[Math.floor(Math.random()*items.length)];S.target=target.id;S.taskView=config.type==='trees'?(S.view==='mixed'?(Math.random()<.5?'tree':'leaf'):S.view):'bird';S.options=worldShuffle([target,...worldShuffle(items.filter(item=>item.id!==target.id)).slice(0,3)]).map(item=>item.id);S.solved=false;S.feedback='';draw()}; S.level=Math.max(1,Math.min(3,+S.level||1));
const answer=id=>{if(S.solved)return;if(id===S.target){S.solved=true;S.feedback='good';S.score++;S.streak++;sndGood()}else{S.feedback='bad';S.streak=0;sndBad()}draw()}; const activity=worldSelect(S.activity,[["learn","worldLearn"],["quiz","worldQuiz"]]);
const gallery=()=>{const grid=document.createElement('div');grid.className='world-gallery';pool().forEach(item=>{const card=document.createElement('article');card.className='world-specimen';card.append(worldVisual(item,config.type,config.type==='trees'?S.view:'bird'));const name=document.createElement('h3');name.textContent=worldName(item);const hint=document.createElement('p');hint.textContent=worldHint(item);card.append(name,hint);grid.append(card)});return grid}; const level=worldSelect(String(S.level),[["1","lvl1"],["2","lvl2"],["3","lvl3"]]);
function draw(){S.activity=activity.value;S.level=+level.value;if(view)S.view=view.value;stage.className='world-app';stage.innerHTML='';const top=document.createElement('div');top.className='world-top';const title=document.createElement('strong');title.textContent=T('wg_'+config.type);const score=document.createElement('span');score.textContent=T('worldScore').replace('{score}',S.score).replace('{streak}',S.streak);top.append(title,score);stage.append(top);if(S.activity==='learn'){const intro=document.createElement('p');intro.className='world-intro';intro.textContent=T('worldExploreHint');stage.append(intro,gallery());return}const item=current(),card=document.createElement('section');card.className='world-task-card';const question=document.createElement('h3');question.textContent=T(config.type==='birds'?'worldQuestionBird':S.taskView==='leaf'?'worldQuestionLeaf':'worldQuestionTree');card.append(question,worldVisual(item,config.type,S.taskView));const answers=document.createElement('div');answers.className='world-answers';S.options.forEach(id=>{const option=config.items.find(entry=>entry.id===id);if(!option)return;const button=btn(worldName(option),()=>answer(id));if(S.solved&&id===S.target)button.classList.add('correct');answers.append(button)});card.append(answers);const feedback=document.createElement('p');feedback.className='world-feedback '+(S.feedback==='good'?'ok':S.feedback?'bad':'');feedback.setAttribute('role','status');if(S.feedback)feedback.textContent=T(S.feedback==='good'?'worldCorrect':'worldTryAgain');card.append(feedback);const actions=document.createElement('div');actions.className='world-actions';actions.append(btn(T('worldNext'),newTask,'ghost'));card.append(actions);if(S.solved){const fact=document.createElement('p');fact.className='world-fact';fact.textContent=worldHint(item);card.append(fact)}stage.append(card)} const stage=document.createElement("div");
activity.onchange=()=>{S.activity=activity.value;if(S.activity==='quiz')newTask();else draw()};level.onchange=()=>{S.level=+level.value;if(S.activity==='quiz')newTask();else draw()};if(view)view.onchange=()=>{S.view=view.value;if(S.activity==='quiz')newTask();else draw()};learningShell(root,settings,stage);if(S.activity==='quiz')newTask();else draw();return{getState:()=>({...S,options:[...S.options]})}; const settings=[field(T("worldActivity"),activity),field(T("worldLevel"),level)];
let view;
if(config.type==="trees"){
view=worldSelect(S.view,[["tree","worldTreeWhole"],["leaf","worldTreeLeaf"],["mixed","worldTreeMixed"]]);
settings.push(field(T("worldTreeView"),view));
}
const pool=()=>worldLevelPool(config.items,S.level);
const current=()=>config.items.find(item=>item.id===S.target)||pool()[0];
const newTask=()=>{
const items=pool(),previous=S.target;
let target=items[Math.floor(Math.random()*items.length)];
if(items.length>1)while(target.id===previous)target=items[Math.floor(Math.random()*items.length)];
S.target=target.id;
S.taskView=config.type==="trees"?(S.view==="mixed"?(Math.random()<.5?"tree":"leaf"):S.view):"bird";
S.options=worldShuffle([target,...worldShuffle(items.filter(item=>item.id!==target.id)).slice(0,3)]).map(item=>item.id);
S.solved=false;
S.feedback="";
draw();
};
const answer=id=>{
if(S.solved)return;
if(id===S.target){
S.solved=true;S.feedback="good";S.score++;S.streak++;sndGood();
}else{
S.feedback="bad";S.streak=0;sndBad();
}
draw();
};
const gallery=()=>{
const grid=document.createElement("div");
grid.className="world-gallery";
pool().forEach(item=>{
const card=document.createElement("article");
card.className="world-specimen";
card.append(worldVisual(item,config.type,config.type==="trees"?S.view:"bird"),worldIdentity(item));
const hint=document.createElement("p");
hint.textContent=worldHint(item);
card.append(hint);
grid.append(card);
});
return grid;
};
function draw(){
S.activity=activity.value;
S.level=+level.value;
if(view)S.view=view.value;
stage.className="world-app";
stage.innerHTML="";
const top=document.createElement("div");
top.className="world-top";
const title=document.createElement("strong");
title.textContent=T("wg_"+config.type);
const score=document.createElement("span");
score.textContent=T("worldScore").replace("{score}",S.score).replace("{streak}",S.streak);
top.append(title,score);
stage.append(top);
if(S.activity==="learn"){
const intro=document.createElement("p");
intro.className="world-intro";
intro.textContent=T("worldExploreHint");
stage.append(intro,gallery());
return;
}
const item=current(),card=document.createElement("section");
card.className="world-task-card";
const question=document.createElement("h3");
question.textContent=T(config.type==="birds"?"worldQuestionBird":S.taskView==="leaf"?"worldQuestionLeaf":"worldQuestionTree");
card.append(question,worldVisual(item,config.type,S.taskView));
const answers=document.createElement("div");
answers.className="world-answers";
S.options.forEach(id=>{
const option=config.items.find(entry=>entry.id===id);
if(!option)return;
const button=btn(worldName(option),()=>answer(id));
if(S.solved&&id===S.target)button.classList.add("correct");
answers.append(button);
});
card.append(answers);
const feedback=document.createElement("p");
feedback.className="world-feedback "+(S.feedback==="good"?"ok":S.feedback?"bad":"");
feedback.setAttribute("role","status");
if(S.feedback)feedback.textContent=T(S.feedback==="good"?"worldCorrect":"worldTryAgain");
card.append(feedback);
const actions=document.createElement("div");
actions.className="world-actions";
actions.append(btn(T("worldNext"),newTask,"ghost"));
card.append(actions);
if(S.solved){
const fact=document.createElement("div");
fact.className="world-fact";
fact.append(worldIdentity(item));
const hint=document.createElement("p");
hint.textContent=worldHint(item);
fact.append(hint);
card.append(fact);
}
stage.append(card);
}
activity.onchange=()=>{S.activity=activity.value;if(S.activity==="quiz")newTask();else draw()};
level.onchange=()=>{S.level=+level.value;if(S.activity==="quiz")newTask();else draw()};
if(view)view.onchange=()=>{S.view=view.value;if(S.activity==="quiz")newTask();else draw()};
learningShell(root,settings,stage);
if(S.activity==="quiz")newTask();else draw();
return{getState:()=>({...S,options:[...S.options]})};
}
function mountTrees(root,init){return mountWorldQuiz(root,init,{type:"trees",items:WORLD_TREES})}
function mountBirds(root,init){return mountWorldQuiz(root,init,{type:"birds",items:WORLD_BIRDS})}
const SVG_NS="http://www.w3.org/2000/svg";
const TOPO_MAP_ASSET=WORLD_MEDIA_ROOT+"netherlands-provinces.svg";
const TOPO_MAP_SOURCE="https://commons.wikimedia.org/wiki/File:Netherlands_provinces_map_blank.svg";
function svgNode(name,attributes={}){
const node=document.createElementNS(SVG_NS,name);
Object.entries(attributes).forEach(([key,value])=>node.setAttribute(key,value));
return node;
}
function svgAction(node,label,action){
node.setAttribute("role","button");
node.setAttribute("tabindex","0");
node.setAttribute("aria-label",label);
node.addEventListener("click",action);
node.addEventListener("keydown",event=>{
if(event.key==="Enter"||event.key===" "){
event.preventDefault();
action();
}
});
} }
function mountTrees(root,init){return mountWorldQuiz(root,init,{type:'trees',items:WORLD_TREES})}
function mountBirds(root,init){return mountWorldQuiz(root,init,{type:'birds',items:WORLD_BIRDS})}
const SVG_NS='http://www.w3.org/2000/svg';
function svgNode(name,attributes={}){const node=document.createElementNS(SVG_NS,name);Object.entries(attributes).forEach(([key,value])=>node.setAttribute(key,value));return node}
function svgAction(node,label,action){node.setAttribute('role','button');node.setAttribute('tabindex','0');node.setAttribute('aria-label',label);node.addEventListener('click',action);node.addEventListener('keydown',event=>{if(event.key==='Enter'||event.key===' '){event.preventDefault();action()}})}
function makeTopoMap(mode,pool,onPick,showLabels,picked,target){ function makeTopoMap(mode,pool,onPick,showLabels,picked,target){
const wrap=document.createElement('div');wrap.className='topo-map-wrap';const svg=svgNode('svg',{class:'topo-map',viewBox:'20 20 330 510',role:'img','aria-label':T('topoMapLabel')});svg.append(svgNode('rect',{x:20,y:20,width:330,height:510,rx:22,class:'topo-water'})); const wrap=document.createElement("div");
NL_PROVINCES.forEach(province=>{const path=svgNode('path',{d:province.d,fill:province.color,class:'topo-province'+(province.id===target?' target':'')+(province.id===picked?' picked':''),'data-id':province.id});const title=svgNode('title');title.textContent=worldName(province);path.append(title);if(onPick&&mode==='provinces')svgAction(path,worldName(province),()=>onPick(province.id));svg.append(path);if(showLabels&&mode==='provinces'&&pool.some(item=>item.id===province.id)){const label=svgNode('text',{x:province.x,y:province.y,class:'topo-label'});label.textContent=worldName(province);svg.append(label)}}); wrap.className="topo-map-wrap";
if(mode!=='provinces')pool.forEach(city=>{const group=svgNode('g',{class:'topo-marker'+(city.id===target?' target':'')+(city.id===picked?' picked':'')});const hit=svgNode('circle',{cx:city.x,cy:city.y,r:15,class:'topo-hit'}),dot=svgNode('circle',{cx:city.x,cy:city.y,r:6,class:'topo-dot'});group.append(hit,dot);if(showLabels){const label=svgNode('text',{x:city.x+9,y:city.y-8,class:'topo-city-label'});label.textContent=worldName(city);group.append(label)}if(onPick)svgAction(group,worldName(city),()=>onPick(city.id));svg.append(group)});wrap.append(svg);return wrap; const svg=svgNode("svg",{
class:"topo-map",viewBox:"0 0 743 881",
role:onPick?"group":"img","aria-label":T("topoMapLabel"),
});
svg.append(svgNode("rect",{x:0,y:0,width:743,height:881,rx:34,class:"topo-water"}));
NL_PROVINCES.forEach(province=>{
const shape=svgNode("use",{
href:TOPO_MAP_ASSET+"#"+province.shape,
fill:province.color,
class:"topo-province"+(province.id===target?" target":"")+(province.id===picked?" picked":""),
"data-id":province.id,
});
const title=svgNode("title");
title.textContent=worldName(province);
shape.append(title);
if(onPick&&mode==="provinces")svgAction(shape,worldName(province),()=>onPick(province.id));
svg.append(shape);
if(showLabels&&mode==="provinces"&&pool.some(item=>item.id===province.id)){
const label=svgNode("text",{x:province.x,y:province.y,class:"topo-label"});
label.textContent=worldName(province);
svg.append(label);
}
});
if(mode!=="provinces")pool.forEach(city=>{
const group=svgNode("g",{
class:"topo-marker"+(city.id===target?" target":"")+(city.id===picked?" picked":""),
});
const hit=svgNode("circle",{cx:city.x,cy:city.y,r:24,class:"topo-hit"});
const dot=svgNode("circle",{cx:city.x,cy:city.y,r:10,class:"topo-dot"});
group.append(hit,dot);
if(showLabels){
const left=city.side==="left";
const label=svgNode("text",{
x:city.x+(left?-16:16),y:city.y-13,
class:"topo-city-label"+(left?" left":""),
});
label.textContent=worldName(city);
group.append(label);
}
if(onPick)svgAction(group,worldName(city),()=>onPick(city.id));
svg.append(group);
});
const source=document.createElement("a");
source.className="topo-source";
source.href=TOPO_MAP_SOURCE;
source.target="_blank";
source.rel="noopener noreferrer";
source.textContent=T("topoMapSource");
wrap.append(svg,source);
return wrap;
} }
function mountTopography(root,init){ function mountTopography(root,init){
const S=Object.assign({activity:'quiz',mode:'provinces',level:1,score:0,streak:0,target:'',picked:'',solved:false,feedback:''},init||{});S.level=Math.max(1,Math.min(3,+S.level||1)); const S=Object.assign({
const activity=worldSelect(S.activity,[['learn','worldLearn'],['quiz','worldQuiz']]),mode=worldSelect(S.mode,[['provinces','topoProvinces'],['capitals','topoCapitals'],['cities','topoCities']]),level=worldSelect(String(S.level),[['1','lvl1'],['2','lvl2'],['3','lvl3']]),stage=document.createElement('div'); activity:"quiz",mode:"provinces",level:1,score:0,streak:0,
const pool=()=>S.mode==='provinces'?NL_PROVINCES.filter(item=>item.tier<=S.level):S.mode==='capitals'?NL_CITIES.filter(item=>item.capital&&item.tier<=S.level):NL_CITIES.filter(item=>!item.capital&&item.tier<=S.level),current=()=>pool().find(item=>item.id===S.target)||pool()[0]; target:"",picked:"",solved:false,feedback:"",
const newTask=()=>{const items=pool(),previous=S.target;let target=items[Math.floor(Math.random()*items.length)];if(items.length>1)while(target.id===previous)target=items[Math.floor(Math.random()*items.length)];S.target=target.id;S.picked='';S.solved=false;S.feedback='';draw()}; },init||{});
const answer=id=>{if(S.solved)return;S.picked=id;if(id===S.target){S.solved=true;S.feedback='good';S.score++;S.streak++;sndGood()}else{S.feedback='bad';S.streak=0;sndBad()}draw()}; S.level=Math.max(1,Math.min(3,+S.level||1));
function draw(){S.activity=activity.value;S.mode=mode.value;S.level=+level.value;stage.className='world-app topo-app';stage.innerHTML='';const top=document.createElement('div');top.className='world-top';const title=document.createElement('strong');title.textContent=T('wg_topography');const score=document.createElement('span');score.textContent=T('worldScore').replace('{score}',S.score).replace('{streak}',S.streak);top.append(title,score);stage.append(top);const card=document.createElement('section');card.className='world-task-card topo-card';if(S.activity==='learn'){const intro=document.createElement('p');intro.className='world-intro';intro.textContent=T('topoExploreHint');card.append(intro,makeTopoMap(S.mode,pool(),null,true,'',''))}else{const item=current(),question=document.createElement('h3');question.textContent=T(S.mode==='provinces'?'topoFindProvince':S.mode==='capitals'?'topoFindCapital':'topoFindCity').replace('{name}',worldName(item));card.append(question,makeTopoMap(S.mode,pool(),answer,S.solved,S.picked,S.solved?S.target:''));const feedback=document.createElement('p');feedback.className='world-feedback '+(S.feedback==='good'?'ok':S.feedback?'bad':'');feedback.setAttribute('role','status');if(S.feedback)feedback.textContent=T(S.feedback==='good'?'worldCorrect':'worldTryAgain');const actions=document.createElement('div');actions.className='world-actions';actions.append(btn(T('worldNext'),newTask,'ghost'));card.append(feedback,actions)}stage.append(card)} const activity=worldSelect(S.activity,[["learn","worldLearn"],["quiz","worldQuiz"]]);
activity.onchange=()=>{S.activity=activity.value;if(S.activity==='quiz')newTask();else draw()};mode.onchange=()=>{S.mode=mode.value;if(S.activity==='quiz')newTask();else draw()};level.onchange=()=>{S.level=+level.value;if(S.activity==='quiz')newTask();else draw()};learningShell(root,[field(T('worldActivity'),activity),field(T('topoMode'),mode),field(T('worldLevel'),level)],stage);if(S.activity==='quiz')newTask();else draw();return{getState:()=>({...S})}; const mode=worldSelect(S.mode,[["provinces","topoProvinces"],["capitals","topoCapitals"],["cities","topoCities"]]);
const level=worldSelect(String(S.level),[["1","lvl1"],["2","lvl2"],["3","lvl3"]]);
const stage=document.createElement("div");
const pool=()=>S.mode==="provinces"
?NL_PROVINCES.filter(item=>item.tier<=S.level)
:S.mode==="capitals"
?NL_CITIES.filter(item=>item.capital&&item.tier<=S.level)
:NL_CITIES.filter(item=>!item.capital&&item.tier<=S.level);
const current=()=>pool().find(item=>item.id===S.target)||pool()[0];
const newTask=()=>{
const items=pool(),previous=S.target;
let target=items[Math.floor(Math.random()*items.length)];
if(items.length>1)while(target.id===previous)target=items[Math.floor(Math.random()*items.length)];
S.target=target.id;S.picked="";S.solved=false;S.feedback="";
draw();
};
const answer=id=>{
if(S.solved)return;
S.picked=id;
if(id===S.target){
S.solved=true;S.feedback="good";S.score++;S.streak++;sndGood();
}else{
S.feedback="bad";S.streak=0;sndBad();
}
draw();
};
function draw(){
S.activity=activity.value;S.mode=mode.value;S.level=+level.value;
stage.className="world-app topo-app";
stage.innerHTML="";
const top=document.createElement("div");
top.className="world-top";
const title=document.createElement("strong");
title.textContent=T("wg_topography");
const score=document.createElement("span");
score.textContent=T("worldScore").replace("{score}",S.score).replace("{streak}",S.streak);
top.append(title,score);
stage.append(top);
const card=document.createElement("section");
card.className="world-task-card topo-card";
if(S.activity==="learn"){
const intro=document.createElement("p");
intro.className="world-intro";
intro.textContent=T("topoExploreHint");
card.append(intro,makeTopoMap(S.mode,pool(),null,true,"",""));
}else{
const item=current(),question=document.createElement("h3");
question.textContent=T(S.mode==="provinces"?"topoFindProvince":S.mode==="capitals"?"topoFindCapital":"topoFindCity").replace("{name}",worldName(item));
card.append(question,makeTopoMap(S.mode,pool(),answer,S.solved,S.picked,S.solved?S.target:""));
const feedback=document.createElement("p");
feedback.className="world-feedback "+(S.feedback==="good"?"ok":S.feedback?"bad":"");
feedback.setAttribute("role","status");
if(S.feedback)feedback.textContent=T(S.feedback==="good"?"worldCorrect":"worldTryAgain");
const actions=document.createElement("div");
actions.className="world-actions";
actions.append(btn(T("worldNext"),newTask,"ghost"));
card.append(feedback,actions);
}
stage.append(card);
}
activity.onchange=()=>{S.activity=activity.value;if(S.activity==="quiz")newTask();else draw()};
mode.onchange=()=>{S.mode=mode.value;if(S.activity==="quiz")newTask();else draw()};
level.onchange=()=>{S.level=+level.value;if(S.activity==="quiz")newTask();else draw()};
learningShell(root,[
field(T("worldActivity"),activity),
field(T("topoMode"),mode),
field(T("worldLevel"),level),
],stage);
if(S.activity==="quiz")newTask();else draw();
return{getState:()=>({...S})};
} }

View file

@ -66,16 +66,21 @@ test('wereldoriëntatiewidgets laden, registreren en vertalen', async()=>{
assert.equal((core.match(new RegExp('wg_'+id+'_d:','g'))||[]).length,2,id); assert.equal((core.match(new RegExp('wg_'+id+'_d:','g'))||[]).length,2,id);
} }
assert.equal((core.match(/catWorld:/g)||[]).length,2); assert.equal((core.match(/catWorld:/g)||[]).length,2);
for(const key of ['worldActivity','worldLearn','worldQuiz','worldTreeView','worldQuestionLeaf','worldQuestionBird','topoProvinces','topoCapitals','topoCities','topoFindProvince','topoFindCapital','topoFindCity'])assert.equal((core.match(new RegExp(key+':','g'))||[]).length,2,key); for(const key of ['worldActivity','worldLearn','worldQuiz','worldTreeView','worldQuestionLeaf','worldQuestionBird','worldPhotoTree','worldPhotoLeaf','worldPhotoBird','worldPhotoCredit','topoProvinces','topoCapitals','topoCities','topoFindProvince','topoFindCapital','topoFindCity','topoMapSource'])assert.equal((core.match(new RegExp(key+':','g'))||[]).length,2,key);
assert.match(source,/getState/); assert.match(source,/getState/);
assert.ok(source.includes('name.textContent=worldName(item)')); assert.ok(source.includes('name.textContent=worldName(item)'));
assert.ok(source.includes('hint.textContent=worldHint(item)')); assert.ok(source.includes('hint.textContent=worldHint(item)'));
assert.doesNotMatch(source,/innerHTML\s*=\s*(?:worldName|worldHint)\s*\(/); assert.doesNotMatch(source,/innerHTML\s*=\s*(?:worldName|worldHint)\s*\(/);
}); });
test('wereldoriëntatiedata bevat volledige soorten en Nederlandse topografie', async()=>{ test('wereldoriëntatiedata bevat echte soortmedia en volledige Nederlandse topografie', async()=>{
const source=await readFile('public/js/widgets/world.js','utf8'),context={}; const [source,map,attribution]=await Promise.all([
runInNewContext(source+';globalThis.__world={trees:WORLD_TREES,birds:WORLD_BIRDS,provinces:NL_PROVINCES,cities:NL_CITIES}',context); readFile('public/js/widgets/world.js','utf8'),
readFile('public/img/world/netherlands-provinces.svg','utf8'),
readFile('public/img/world/ATTRIBUTION.md','utf8'),
]);
const context={};
runInNewContext(source+';globalThis.__world={trees:WORLD_TREES,birds:WORLD_BIRDS,media:WORLD_MEDIA,provinces:NL_PROVINCES,cities:NL_CITIES}',context);
const data=JSON.parse(JSON.stringify(context.__world)); const data=JSON.parse(JSON.stringify(context.__world));
assert.equal(data.trees.length,8); assert.equal(data.trees.length,8);
assert.equal(data.birds.length,8); assert.equal(data.birds.length,8);
@ -83,13 +88,35 @@ test('wereldoriëntatiedata bevat volledige soorten en Nederlandse topografie',
assert.equal(data.cities.filter(city=>city.capital).length,12); assert.equal(data.cities.filter(city=>city.capital).length,12);
assert.equal(data.cities.filter(city=>!city.capital).length,8); assert.equal(data.cities.filter(city=>!city.capital).length,8);
for(const collection of [data.trees,data.birds,data.provinces,data.cities])assert.equal(new Set(collection.map(item=>item.id)).size,collection.length); for(const collection of [data.trees,data.birds,data.provinces,data.cities])assert.equal(new Set(collection.map(item=>item.id)).size,collection.length);
for(const item of [...data.trees,...data.birds])assert.ok(item.nl&&item.en&&item.hintNl&&item.hintEn&&item.tier>=1&&item.tier<=3,item.id); for(const item of [...data.trees,...data.birds])assert.ok(item.nl&&item.en&&item.latin&&item.hintNl&&item.hintEn&&item.tier>=1&&item.tier<=3,item.id);
const assetPaths=[];
for(const tree of data.trees){
assert.ok(data.media[tree.id]?.tree?.file&&data.media[tree.id]?.leaf?.file,tree.id);
assetPaths.push(data.media[tree.id].tree.file,data.media[tree.id].leaf.file);
}
for(const bird of data.birds){
assert.ok(data.media[bird.id]?.bird?.file,bird.id);
assetPaths.push(data.media[bird.id].bird.file);
}
assert.equal(new Set(assetPaths).size,24);
for(const relativePath of assetPaths){
const image=await readFile('public/img/world/'+relativePath);
assert.ok(image.length>10_000,relativePath+' is geen bruikbare foto');
assert.equal(image.subarray(0,4).toString(),'RIFF',relativePath);
assert.equal(image.subarray(8,12).toString(),'WEBP',relativePath);
assert.ok(attribution.includes('`'+relativePath+'`'),relativePath+' mist bronvermelding');
}
const cities=new Map(data.cities.map(city=>[city.id,city])); const cities=new Map(data.cities.map(city=>[city.id,city]));
const capitals=Object.fromEntries(data.provinces.map(province=>[province.nl,cities.get(province.capital)?.nl])); const capitals=Object.fromEntries(data.provinces.map(province=>[province.nl,cities.get(province.capital)?.nl]));
assert.deepEqual(capitals,{ assert.deepEqual(capitals,{
Friesland:'Leeuwarden',Groningen:'Groningen',Drenthe:'Assen',Overijssel:'Zwolle',Flevoland:'Lelystad','Noord-Holland':'Haarlem',Utrecht:'Utrecht',Gelderland:'Arnhem','Zuid-Holland':'Den Haag',Zeeland:'Middelburg','Noord-Brabant':"'s-Hertogenbosch",Limburg:'Maastricht', Friesland:'Leeuwarden',Groningen:'Groningen',Drenthe:'Assen',Overijssel:'Zwolle',Flevoland:'Lelystad','Noord-Holland':'Haarlem',Utrecht:'Utrecht',Gelderland:'Arnhem','Zuid-Holland':'Den Haag',Zeeland:'Middelburg','Noord-Brabant':"'s-Hertogenbosch",Limburg:'Maastricht',
}); });
for(const province of data.provinces)assert.ok(province.d&&Number.isFinite(province.x)&&Number.isFinite(province.y)&&cities.has(province.capital),province.id); for(const province of data.provinces){
assert.ok(province.shape&&Number.isFinite(province.x)&&Number.isFinite(province.y)&&cities.has(province.capital),province.id);
assert.match(map,new RegExp('<path id="'+province.shape+'"[^>]+d="[^"]+"'));
}
assert.equal((map.match(/<path id=/g)||[]).length,12);
assert.match(source,/href:TOPO_MAP_ASSET\+"#"\+province\.shape/);
}); });
test('widgetbibliotheek bevat zoeken favorieten sjablonen en borddata-acties', async()=>{ test('widgetbibliotheek bevat zoeken favorieten sjablonen en borddata-acties', async()=>{
@ -310,6 +337,8 @@ test('alle geregistreerde widgets hebben mobiele dekking en passen op een 320px-
'.edu-stage{align-items:safe center;justify-content:safe center', '.edu-stage{align-items:safe center;justify-content:safe center',
'.money-task-card,.clock-task-card,.world-task-card', '.money-task-card,.clock-task-card,.world-task-card',
'.world-gallery{grid-template-columns:1fr}', '.world-gallery{grid-template-columns:1fr}',
'.world-photo-credit',
'.topo-map-wrap{padding:6px}',
]){ ]){
assert.ok(compact.includes(selector),selector+' mist in de compacte containerregels'); assert.ok(compact.includes(selector),selector+' mist in de compacte containerregels');
} }