/* 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"), }, lime:{ tree:worldMedia("trees/lime.webp","Friedrich Haag","CC BY-SA 4.0","https://commons.wikimedia.org/wiki/File:012_2015_05_06_Winterlinde_(Wiki_Loves_Earth_2015).jpg"), leaf:worldMedia("leaves/lime.webp","Dmitry Makeev","CC BY-SA 4.0","https://commons.wikimedia.org/wiki/File:2020_year._Herbarium._Tilia_cordata._img-007.jpg"), }, ash:{ tree:worldMedia("trees/ash.webp","Willow","CC BY 2.5","https://commons.wikimedia.org/wiki/File:Fraxinus_excelsior_001.jpg"), leaf:worldMedia("leaves/ash.webp","Dmitry Makeev","CC BY-SA 4.0","https://commons.wikimedia.org/wiki/File:2020_year._Herbarium._Fraxinus._img-015.jpg"), }, sweet_chestnut:{ tree:worldMedia("trees/sweet-chestnut.webp","Zeynel Cebeci","CC BY-SA 4.0","https://commons.wikimedia.org/wiki/File:Castanea_sativa_-_Anatolian_sweet_chestnut_04.jpg"), leaf:worldMedia("leaves/sweet-chestnut.webp","Ekir","CC BY-SA 3.0","https://commons.wikimedia.org/wiki/File:Castanea_sativa_leaves_and_immature_fruit.jpg"), }, rowan:{ tree:worldMedia("trees/rowan.webp","Eeno11","CC BY-SA 3.0","https://commons.wikimedia.org/wiki/File:Rowan_tree_20081002b.jpg"), leaf:worldMedia("leaves/rowan.webp","Dmitry Makeev","CC BY-SA 4.0","https://commons.wikimedia.org/wiki/File:2020_year._Herbarium._Sorbus_aucuparia._img-005.jpg"), }, hawthorn:{ tree:worldMedia("trees/hawthorn.webp","Jean-Pol GRANDMONT","CC BY 2.5","https://commons.wikimedia.org/wiki/File:Crataegus_monogyna_in_Joncret_AR1aJPG.jpg"), leaf:worldMedia("leaves/hawthorn.webp","Didier Descouens","CC BY-SA 4.0","https://commons.wikimedia.org/wiki/File:(MHNT)_Crataegus_monogyna_-_Leaves_and_stipules.jpg"), }, aspen:{ tree:worldMedia("trees/aspen.webp","Willow","CC BY-SA 2.5","https://commons.wikimedia.org/wiki/File:Populus_tremula_004.jpg"), leaf:worldMedia("leaves/aspen.webp","Dmitry Makeev","CC BY-SA 4.0","https://commons.wikimedia.org/wiki/File:2020_year._Herbarium._Populus_tremula._img-017.jpg"), }, spruce:{ tree:worldMedia("trees/spruce.webp","Ivar Leidus","CC BY-SA 3.0","https://commons.wikimedia.org/wiki/File:Kuusk_Keila-Paldiski_rdt_%C3%A4%C3%A4res.jpg"), leaf:worldMedia("leaves/spruce.webp","Achird","CC BY-SA 4.0","https://commons.wikimedia.org/wiki/File:Picea_abies_needles_in_V%C3%A4smestorp_2024-10-29_(0261).jpg"), }, larch:{ tree:worldMedia("trees/larch.webp","MrPanyGoff","CC BY-SA 3.0","https://commons.wikimedia.org/wiki/File:European_larch_-_Larix_decidua.jpg"), leaf:worldMedia("leaves/larch.webp","Rosser1954 / Roger Griffith","Public domain","https://commons.wikimedia.org/wiki/File:Larix_decidua_needles_and_male_cones.JPG"), }, yew:{ tree:worldMedia("trees/yew.webp","Blarney Castle Estate","CC BY-SA 4.0","https://commons.wikimedia.org/wiki/File:2020_Witch%E2%80%99s_Yew_Tree_IRL.jpg"), leaf:worldMedia("leaves/yew.webp","hedera.baltica","CC BY-SA 2.0","https://commons.wikimedia.org/wiki/File:Yew_berries_(50578142398).jpg"), }, elm:{ tree:worldMedia("trees/elm.webp","Ptelea","Public domain","https://commons.wikimedia.org/wiki/File:Ulmus_minor_tree.jpg"), leaf:worldMedia("leaves/elm.webp","Daniel Capilla","CC BY-SA 4.0","https://commons.wikimedia.org/wiki/File:Elm_leaves_(Ulmus_minor).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")}, chaffinch:{bird:worldMedia("birds/chaffinch.webp","MichaelMaggs / Arad","CC BY-SA 2.5","https://commons.wikimedia.org/wiki/File:Fringilla_coelebs_chaffinch_male_edit2.jpg")}, starling:{bird:worldMedia("birds/starling.webp","Mark Robinson","CC BY 2.0","https://commons.wikimedia.org/wiki/File:Sturnus_vulgaris_-England_-standing-8.jpg")}, grey_heron:{bird:worldMedia("birds/grey-heron.webp","Alexis Lours","CC BY 4.0","https://commons.wikimedia.org/wiki/File:Grey_heron_2022_04_03_03.jpg")}, mute_swan:{bird:worldMedia("birds/mute-swan.webp","Giles Laurent","CC BY-SA 4.0","https://commons.wikimedia.org/wiki/File:093_Wild_Mute_swan_at_Lake_Geneva_during_sunset_Photo_by_Giles_Laurent.jpg")}, coot:{bird:worldMedia("birds/coot.webp","Zeynel Cebeci","CC BY-SA 4.0","https://commons.wikimedia.org/wiki/File:Fulica_atra_-_Common_coot,_Adana_2016-11-12_01-1.jpg")}, greylag_goose:{bird:worldMedia("birds/greylag-goose.webp","H005 / Pro2","Public domain","https://commons.wikimedia.org/wiki/File:Graugans_Anser_Anser.jpg")}, collared_dove:{bird:worldMedia("birds/collared-dove.webp","Zeynel Cebeci","CC BY-SA 4.0","https://commons.wikimedia.org/wiki/File:Streptopelia_decaocto_-_Eurasian_Collared_Dove_05.jpg")}, wren:{bird:worldMedia("birds/wren.webp","Alexis Lours","CC BY 4.0","https://commons.wikimedia.org/wiki/File:Eurasian_wren_(Troglodytes_troglodytes).jpg")}, buzzard:{bird:worldMedia("birds/buzzard.webp","Yerpo","CC BY-SA 4.0","https://commons.wikimedia.org/wiki/File:Buteo_buteo_juv_Log_(2).jpg")}, barn_owl:{bird:worldMedia("birds/barn-owl.webp","Luc Viatour","CC BY-SA 3.0","https://commons.wikimedia.org/wiki/File:Tyto_alba_1_Luc_Viatour.jpg")}, dandelion:{plant:worldMedia("plants/dandelion.webp","Dominicus Johannes Bergsma","CC BY-SA 3.0","https://commons.wikimedia.org/wiki/File:Paardenbloem_(Taraxacum_officinale)_06.JPG")}, nettle:{plant:worldMedia("plants/nettle.webp","Dominicus Johannes Bergsma","CC BY-SA 4.0","https://commons.wikimedia.org/wiki/File:Brandnetel,_Urtica_dioica,_Locatie,_Famberhorst.jpg")}, daisy:{plant:worldMedia("plants/daisy.webp","Grand-Duc","CC BY-SA 3.0","https://commons.wikimedia.org/wiki/File:Bellis_perennis_full_plant.jpg")}, red_clover:{plant:worldMedia("plants/red-clover.webp","Robert Flogaus-Faust","CC BY 4.0","https://commons.wikimedia.org/wiki/File:Trifolium_pratense_RF.jpg")}, white_deadnettle:{plant:worldMedia("plants/white-deadnettle.webp","Robert Flogaus-Faust","CC BY 4.0","https://commons.wikimedia.org/wiki/File:Lamium_album_RF.jpg")}, cow_parsley:{plant:worldMedia("plants/cow-parsley.webp","Agnes Monkelbaan","CC BY-SA 4.0","https://commons.wikimedia.org/wiki/File:Fluitenkruid_(Anthriscus_sylvestris)_26-04-2020._(actm.)_02.jpg")}, yarrow:{plant:worldMedia("plants/yarrow.webp","Rhododendrites","CC BY-SA 4.0","https://commons.wikimedia.org/wiki/File:Achillea_millefolium_(52354).jpg")}, ribwort_plantain:{plant:worldMedia("plants/ribwort-plantain.webp","Krzysztof Ziarnek, Kenraiz","CC BY-SA 4.0","https://commons.wikimedia.org/wiki/File:Plantago_lanceolata_kz09.jpg")}, common_poppy:{plant:worldMedia("plants/common-poppy.webp","Jörg Hempel","CC BY-SA 2.5","https://commons.wikimedia.org/wiki/File:Papaver_rhoeas_LC0050.jpg")}, chicory:{plant:worldMedia("plants/chicory.webp","T. Kebert","CC BY-SA 4.0","https://commons.wikimedia.org/wiki/File:Cichorium_intybus_TK_2021-07-12_1.jpg")}, foxglove:{plant:worldMedia("plants/foxglove.webp","Jörg Hempel","CC BY-SA 3.0","https://commons.wikimedia.org/wiki/File:Digitalis_purpurea_LC0101.jpg")}, common_sorrel:{plant:worldMedia("plants/common-sorrel.webp","Tournasol7","CC BY 4.0","https://commons.wikimedia.org/wiki/File:Rumex_acetosa_in_Aveyron_(7).jpg")}, }); const WORLD_TREES=Object.freeze([ {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:"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:"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",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:"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:"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:"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:"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."}, {id:"lime",nl:"Winterlinde",en:"Small-leaved lime",latin:"Tilia cordata",tier:1,hintNl:"Hartvormige bladeren en kleine nootjes aan een bleek, vleugelachtig schutblad.",hintEn:"Heart-shaped leaves and small nutlets attached to a pale, wing-like bract."}, {id:"ash",nl:"Gewone es",en:"European ash",latin:"Fraxinus excelsior",tier:1,hintNl:"Zwarte knoppen en geveerde bladeren met zeven tot dertien deelblaadjes.",hintEn:"Black buds and pinnate leaves with seven to thirteen leaflets."}, {id:"sweet_chestnut",nl:"Tamme kastanje",en:"Sweet chestnut",latin:"Castanea sativa",tier:1,hintNl:"Lange, scherp gezaagde bladeren en kastanjes in een dicht stekelige bolster.",hintEn:"Long, sharply toothed leaves and chestnuts in a densely spiny husk."}, {id:"rowan",nl:"Wilde lijsterbes",en:"Rowan",latin:"Sorbus aucuparia",tier:2,hintNl:"Geveerde bladeren, witte bloemschermen en trossen oranjerode bessen.",hintEn:"Pinnate leaves, clusters of white flowers and bunches of orange-red berries."}, {id:"hawthorn",nl:"Eenstijlige meidoorn",en:"Common hawthorn",latin:"Crataegus monogyna",tier:2,hintNl:"Doornige takken, diep gelobde bladeren, witte bloemen en rode vruchten.",hintEn:"Thorny branches, deeply lobed leaves, white flowers and red fruits."}, {id:"aspen",nl:"Ratelpopulier",en:"Aspen",latin:"Populus tremula",tier:2,hintNl:"Bijna ronde bladeren aan een afgeplatte steel die al bij weinig wind trillen.",hintEn:"Almost round leaves on a flattened stalk that tremble in the slightest breeze."}, {id:"spruce",nl:"Fijnspar",en:"Norway spruce",latin:"Picea abies",tier:2,hintNl:"Korte scherpe naalden rondom de twijg en lange kegels die omlaag hangen.",hintEn:"Short sharp needles all around the twig and long cones that hang down."}, {id:"larch",nl:"Europese lariks",en:"European larch",latin:"Larix decidua",tier:3,hintNl:"Zachte naalden in bundeltjes; als enige gewone naaldboom laat hij ze in de herfst vallen.",hintEn:"Soft needles in clusters; unlike most conifers it sheds them in autumn."}, {id:"yew",nl:"Taxus",en:"European yew",latin:"Taxus baccata",tier:3,hintNl:"Donkergroene platte naalden en rode besachtige zaadmantels; de plant is giftig.",hintEn:"Dark green flat needles and red berry-like arils; the plant is poisonous."}, {id:"elm",nl:"Gladde iep",en:"Field elm",latin:"Ulmus minor",tier:3,hintNl:"Ruwe gezaagde bladeren met een opvallend scheve bladvoet.",hintEn:"Rough, toothed leaves with a noticeably uneven base."}, ]); const WORLD_BIRDS=Object.freeze([ {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",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",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",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",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",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",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",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."}, {id:"chaffinch",nl:"Vink",en:"Common chaffinch",latin:"Fringilla coelebs",tier:1,hintNl:"Het mannetje heeft een blauwgrijze kop, roestbruine borst en twee witte vleugelstrepen.",hintEn:"The male has a blue-grey head, rusty breast and two white wing bars."}, {id:"starling",nl:"Spreeuw",en:"Common starling",latin:"Sturnus vulgaris",tier:1,hintNl:"Donker verenkleed met metaalglans en lichte spikkels, plus een korte staart.",hintEn:"Dark, iridescent plumage with pale speckles and a short tail."}, {id:"grey_heron",nl:"Blauwe reiger",en:"Grey heron",latin:"Ardea cinerea",tier:1,hintNl:"Grote grijze watervogel met lange poten, een lange hals en dolkvormige snavel.",hintEn:"Large grey waterbird with long legs, a long neck and dagger-shaped bill."}, {id:"mute_swan",nl:"Knobbelzwaan",en:"Mute swan",latin:"Cygnus olor",tier:1,hintNl:"Grote witte zwaan met een oranje snavel en zwarte knobbel aan de basis.",hintEn:"Large white swan with an orange bill and a black knob at its base."}, {id:"coot",nl:"Meerkoet",en:"Eurasian coot",latin:"Fulica atra",tier:2,hintNl:"Zwarte watervogel met een opvallend witte snavel en witte voorhoofdsplaat.",hintEn:"Black waterbird with a striking white bill and white forehead shield."}, {id:"greylag_goose",nl:"Grauwe gans",en:"Greylag goose",latin:"Anser anser",tier:2,hintNl:"Grote grijsbruine gans met een stevige oranje of roze snavel en roze poten.",hintEn:"Large grey-brown goose with a stout orange or pink bill and pink legs."}, {id:"collared_dove",nl:"Turkse tortel",en:"Eurasian collared dove",latin:"Streptopelia decaocto",tier:2,hintNl:"Licht beigegrijs met een smalle zwarte halsband en een lange, rechte staart.",hintEn:"Pale beige-grey with a narrow black neck collar and a long, square-ended tail."}, {id:"wren",nl:"Winterkoning",en:"Eurasian wren",latin:"Troglodytes troglodytes",tier:2,hintNl:"Heel klein bruin vogeltje met fijne bandjes en een kort staartje dat vaak rechtop staat.",hintEn:"Very small brown bird with fine barring and a short tail often held upright."}, {id:"buzzard",nl:"Buizerd",en:"Common buzzard",latin:"Buteo buteo",tier:3,hintNl:"Brede vleugels, korte staart en een verenkleed dat van donkerbruin tot bijna wit varieert.",hintEn:"Broad wings, a short tail and plumage varying from dark brown to nearly white."}, {id:"barn_owl",nl:"Kerkuil",en:"Barn owl",latin:"Tyto alba",tier:3,hintNl:"Lichte uil met een goudbruine rug en een wit, hartvormig gezicht.",hintEn:"Pale owl with a golden-brown back and a white, heart-shaped face."}, ]); const WORLD_PLANTS=Object.freeze([ {id:"dandelion",nl:"Paardenbloem",en:"Dandelion",latin:"Taraxacum officinale",tier:1,hintNl:"Gele bloemhoofdjes, een holle stengel met melksap en later een ronde pluizenbol.",hintEn:"Yellow flower heads, a hollow stem with milky sap and later a round seed clock."}, {id:"nettle",nl:"Grote brandnetel",en:"Stinging nettle",latin:"Urtica dioica",tier:1,hintNl:"Tegenoverstaande, scherp gezaagde bladeren met brandharen op stengel en blad.",hintEn:"Opposite, sharply toothed leaves with stinging hairs on stems and leaves."}, {id:"daisy",nl:"Madeliefje",en:"Common daisy",latin:"Bellis perennis",tier:1,hintNl:"Laag rozet met lepelvormige blaadjes en witte bloemen rond een geel hart.",hintEn:"Low rosette of spoon-shaped leaves and white flowers around a yellow centre."}, {id:"red_clover",nl:"Rode klaver",en:"Red clover",latin:"Trifolium pratense",tier:1,hintNl:"Ronde roze-paarse bloemhoofdjes en bladeren van drie vaak licht gevlekte deelblaadjes.",hintEn:"Round pink-purple flower heads and leaves of three often pale-marked leaflets."}, {id:"white_deadnettle",nl:"Witte dovenetel",en:"White dead-nettle",latin:"Lamium album",tier:2,hintNl:"Lijkt op brandnetel maar prikt niet; witte lipbloemen staan in kransen rond de stengel.",hintEn:"Looks like a nettle but does not sting; white lipped flowers circle the stem."}, {id:"cow_parsley",nl:"Fluitenkruid",en:"Cow parsley",latin:"Anthriscus sylvestris",tier:2,hintNl:"Fijne varenachtige bladeren en luchtige schermen met heel veel kleine witte bloemen.",hintEn:"Delicate fern-like leaves and airy umbels of many tiny white flowers."}, {id:"yarrow",nl:"Duizendblad",en:"Yarrow",latin:"Achillea millefolium",tier:2,hintNl:"Heel fijn verdeelde bladeren en platte schermen met kleine witte bloemhoofdjes.",hintEn:"Very finely divided leaves and flat clusters of small white flower heads."}, {id:"ribwort_plantain",nl:"Smalle weegbree",en:"Ribwort plantain",latin:"Plantago lanceolata",tier:2,hintNl:"Smalle bladeren met sterke evenwijdige nerven en bruine aren op geribde stelen.",hintEn:"Narrow leaves with strong parallel veins and brown spikes on ribbed stems."}, {id:"common_poppy",nl:"Grote klaproos",en:"Common poppy",latin:"Papaver rhoeas",tier:2,hintNl:"Vier papierdunne rode kroonbladen, vaak met een donkere vlek rond het zwarte hart.",hintEn:"Four papery red petals, often with a dark blotch around the black centre."}, {id:"chicory",nl:"Wilde cichorei",en:"Common chicory",latin:"Cichorium intybus",tier:3,hintNl:"Helderblauwe lintbloemen staan vrijwel direct langs een stijve vertakte stengel.",hintEn:"Bright blue ray flowers grow almost directly along a stiff branching stem."}, {id:"foxglove",nl:"Vingerhoedskruid",en:"Foxglove",latin:"Digitalis purpurea",tier:3,hintNl:"Hoge aar met roze-paarse klokvormige bloemen die van binnen gevlekt zijn; giftig.",hintEn:"Tall spike of pink-purple bell-shaped flowers, spotted inside; poisonous."}, {id:"common_sorrel",nl:"Veldzuring",en:"Common sorrel",latin:"Rumex acetosa",tier:3,hintNl:"Pijlvormige bladeren en een losse roodgroene pluim met heel kleine bloemetjes.",hintEn:"Arrow-shaped leaves and a loose red-green panicle of tiny flowers."}, ]); const NL_PROVINCES=Object.freeze([ {id:"friesland",shape:"friesland",nl:"Friesland",en:"Friesland",capital:"leeuwarden",tier:2,color:"#93c5fd",x:518,y:166}, {id:"groningen",shape:"groningen",nl:"Groningen",en:"Groningen",capital:"groningen_city",tier:2,color:"#fca5a5",x:615,y:72}, {id:"drenthe",shape:"drenthe",nl:"Drenthe",en:"Drenthe",capital:"assen",tier:3,color:"#fde68a",x:611,y:222}, {id:"overijssel",shape:"overijssel",nl:"Overijssel",en:"Overijssel",capital:"zwolle",tier:2,color:"#c4b5fd",x:598,y:351}, {id:"flevoland",shape:"flevoland",nl:"Flevoland",en:"Flevoland",capital:"lelystad",tier:3,color:"#fdba74",x:465,y:272}, {id:"noord_holland",shape:"noord-holland",nl:"Noord-Holland",en:"North Holland",capital:"haarlem",tier:1,color:"#f9a8d4",x:308,y:244}, {id:"utrecht",shape:"utrecht",nl:"Utrecht",en:"Utrecht",capital:"utrecht_city",tier:1,color:"#d8b4fe",x:393,y:471}, {id:"gelderland",shape:"gelderland",nl:"Gelderland",en:"Gelderland",capital:"arnhem",tier:1,color:"#86efac",x:493,y:452}, {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",shape:"zeeland",nl:"Zeeland",en:"Zeeland",capital:"middelburg",tier:3,color:"#5eead4",x:132,y:698}, {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",shape:"limburg",nl:"Limburg",en:"Limburg",capital:"maastricht",tier:1,color:"#a7f3d0",x:489,y:714}, ]); const NL_CITIES=Object.freeze([ {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:612,y:112}, {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:538,y:332}, {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:261,y:371}, {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:500,y:486}, {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:65,y:635}, {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:463,y:836}, {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:234,y:508}, {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:497,y:536}, {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:283,y:296}, {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:347,y:625}, ]); const NL_REGIONS=Object.freeze([ {id:"north",nl:"Noord-Nederland",en:"Northern Netherlands",tier:1,color:"#60a5fa",provinces:["friesland","groningen","drenthe"],x:565,y:172}, {id:"east",nl:"Oost-Nederland",en:"Eastern Netherlands",tier:1,color:"#a78bfa",provinces:["overijssel","gelderland","flevoland"],x:548,y:406}, {id:"west",nl:"West-Nederland",en:"Western Netherlands",tier:1,color:"#22d3ee",provinces:["noord_holland","zuid_holland","utrecht","zeeland"],x:252,y:454}, {id:"south",nl:"Zuid-Nederland",en:"Southern Netherlands",tier:1,color:"#fbbf24",provinces:["noord_brabant","limburg"],x:417,y:648}, ]); const NL_RIVERS=Object.freeze([ {id:"rijn",nl:"Rijn",en:"Rhine",tier:1,color:"#2563eb",labelX:399,labelY:477,path:"M 567 510 C 535 508 515 493 489 491 C 452 488 429 472 394 474 C 353 476 322 494 280 502 C 257 507 238 508 218 514"}, {id:"maas",nl:"Maas",en:"Meuse",tier:1,color:"#0891b2",labelX:401,labelY:581,path:"M 463 850 C 445 817 469 773 490 730 C 511 688 536 639 519 596 C 504 565 472 556 439 570 C 398 588 362 574 320 580 C 280 586 247 578 211 566"}, {id:"ijssel",nl:"IJssel",en:"IJssel",tier:2,color:"#0ea5e9",labelX:536,labelY:389,path:"M 497 489 C 516 459 525 426 526 390 C 527 356 520 329 502 306 C 484 282 463 270 447 250"}, {id:"waal",nl:"Waal",en:"Waal",tier:2,color:"#1d4ed8",labelX:421,labelY:548,path:"M 557 519 C 527 530 503 539 470 548 C 435 558 406 562 373 553 C 340 544 310 548 280 558 C 253 566 230 560 210 550"}, {id:"schelde",nl:"Schelde",en:"Scheldt",tier:3,color:"#0284c7",labelX:139,labelY:641,path:"M 41 660 C 82 654 119 649 151 642 C 176 637 191 624 207 609"}, ]); const WORLD_CONTINENTS=Object.freeze([ {id:"europe",shape:"europe",nl:"Europa",en:"Europe",tier:1,color:"#fbbf24",x:382,y:96}, {id:"africa",shape:"africa",nl:"Afrika",en:"Africa",tier:1,color:"#fb923c",x:383,y:190}, {id:"asia",shape:"asia",nl:"Azië",en:"Asia",tier:1,color:"#f87171",x:505,y:112}, {id:"north_america",shape:"north-america",nl:"Noord-Amerika",en:"North America",tier:2,color:"#60a5fa",x:154,y:112}, {id:"south_america",shape:"south-america",nl:"Zuid-Amerika",en:"South America",tier:2,color:"#34d399",x:245,y:252}, {id:"oceania",shape:"oceania",nl:"Oceanië",en:"Oceania",tier:2,color:"#c084fc",x:593,y:248}, {id:"antarctica",shape:"antarctica",nl:"Antarctica",en:"Antarctica",tier:3,color:"#cbd5e1",x:360,y:337}, ]); const worldName=item=>LANG==="nl"?item.nl:item.en; const worldHint=item=>LANG==="nl"?item.hintNl:item.hintEn; 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 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 worldMediaKind=(type,view)=>type==="birds"?"bird":type==="plants"?"plant":view; const worldPhotoLabel=kind=>T(kind==="bird"?"worldPhotoBird":kind==="plant"?"worldPhotoPlant":kind==="leaf"?"worldPhotoLeaf":"worldPhotoTree"); function worldPhoto(item,type,view){ const kind=worldMediaKind(type,view); const media=WORLD_MEDIA[item.id]?.[kind]; const figure=document.createElement("figure"); figure.className="world-photo world-photo-"+kind; const image=document.createElement("img"); image.className="world-picture"; image.alt=worldPhotoLabel(kind); image.draggable=false; image.loading="lazy"; image.decoding="async"; image.src=WORLD_MEDIA_ROOT+media.file; const badge=document.createElement("span"); badge.className="world-photo-badge"; badge.textContent=worldPhotoLabel(kind); const credit=document.createElement("a"); credit.className="world-photo-credit"; credit.href=media.source; credit.target="_blank"; credit.rel="noopener noreferrer"; credit.textContent=T("worldPhotoCredit").replace("{author}",media.author).replace("{license}",media.license); figure.append(image,badge,credit); return figure; } function worldVisual(item,type,view){ const visual=document.createElement("div"); visual.className="world-visual "+type+(view==="mixed"?" mixed":""); if(type==="trees"&&view==="mixed")visual.append(worldPhoto(item,type,"tree"),worldPhoto(item,type,"leaf")); else visual.append(worldPhoto(item,type,view)); return visual; } function worldPhotoOption(item,type,view,index,onChoose,status){ const kind=worldMediaKind(type,view); const media=WORLD_MEDIA[item.id]?.[kind]; const figure=document.createElement("figure"); figure.className="world-photo-choice"+(status?" "+status:""); const button=document.createElement("button"); button.type="button"; button.className="world-image-button"; button.setAttribute("aria-label",T("worldPhotoOption").replace("{number}",index+1)); const image=document.createElement("img"); image.className="world-picture"; image.alt=""; image.draggable=false; image.loading="lazy"; image.decoding="async"; image.src=WORLD_MEDIA_ROOT+media.file; const number=document.createElement("span"); number.className="world-choice-number"; number.textContent=String(index+1); button.append(image,number); button.onclick=onChoose; const credit=document.createElement("a"); credit.className="world-photo-credit"; credit.href=media.source; credit.target="_blank"; credit.rel="noopener noreferrer"; credit.textContent=T("worldPhotoCredit").replace("{author}",media.author).replace("{license}",media.license); figure.append(button,credit); return figure; } function worldIdentity(item){ 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; } /* ---- flitskaarten: naam kort tonen, dan het beeld + zelftoets, zoals flits.js - hier gevoed door de wereldoriëntatie-content i.p.v. woordkaarten. Hergebruikt de generieke fl*-teksten (zelfde vraag/knoppen als flits.js). ---- */ function worldFlashCard(card,name,visual,S,redraw,onNext,onAssess){ const wrap=document.createElement("div"); wrap.className="world-flash-card"; wrap.append(visual); const nameZone=document.createElement("div"); nameZone.className="world-flash-name"+(S.flashRevealed?"":" dim"); nameZone.textContent=S.flashRevealed?name:"• • •"; wrap.append(nameZone); card.append(wrap); if(!S.flashRevealed){ const hint=document.createElement("p"); hint.className="world-flash-hint"; hint.textContent=T("flMission"); card.append(hint); const actions=document.createElement("div"); actions.className="world-actions"; actions.append(btn(T("flShow"),()=>{S.flashRevealed=true;redraw()})); actions.append(btn(T("worldNext"),onNext,"ghost")); card.append(actions); }else if(!S.solved){ const reflect=document.createElement("div"); reflect.className="world-flash-reflect"; const q=document.createElement("p"); q.textContent=T("flQuestion"); reflect.append(q); const row=document.createElement("div"); row.className="world-actions"; row.append(btn(T("flGotIt"),()=>onAssess(true))); row.append(btn(T("flAgain"),()=>onAssess(false),"ghost")); reflect.append(row); card.append(reflect); } } /* ---- letterblokken: spel de naam met dezelfde sleep-blokken-mechaniek als letters.js, hier gevoed door de naam van de soort/plaats. Elk vak toont meteen het puntenpatroon (en de letter) van het juiste antwoord - bewust de makkelijkste ondersteuning, want deze namen zijn voor de meeste kinderen onbekende spelling; het quiz/foto/latijn-werk hierboven test het herkennen al veel strenger. */ const worldIsLetter=ch=>/\p{L}/u.test(ch); const worldSpellPool=name=>[...name.toLowerCase()].filter(worldIsLetter); function worldSpellNeedsNewTray(S,name){ const letters=worldSpellPool(name); return !Array.isArray(S.answerLetters)||S.answerLetters.length!==letters.length||!Array.isArray(S.trayLetters); } function worldSpellNewTray(S,name){ const letters=worldSpellPool(name); const distractPool="abcdefghijklmnopqrstuvwxyz".split("").filter(c=>!letters.includes(c)); S.trayLetters=worldShuffle([...letters,...worldShuffle(distractPool).slice(0,4)]); S.answerLetters=letters.map(()=>null); S.spellSolved=false; } function worldLetterBlock(ch,trayIndex,letters,S,redraw){ const b=document.createElement("div"); b.className="blok sm world-letter-block "+(VOWELS.includes(ch)?"vowel":"cons"); b.dataset.letter=ch; const label=document.createElement("div"); label.className="bl"; label.textContent=ch; b.append(label); const dg=dotGrid(ch); dg.classList.add("blok-dots"); b.append(dg); b.setAttribute("role","button"); b.setAttribute("aria-label",T("clockDigit").replace("{digit}",ch)); b.tabIndex=0; const settle=idx=>{S.answerLetters[idx]=trayIndex;sndGood();redraw()}; b.onkeydown=e=>{ if((e.key==="Enter"||e.key===" ")&&!S.spellSolved){ e.preventDefault(); const idx=letters.findIndex((need,i)=>need===ch&&S.answerLetters[i]===null); if(idx>=0)settle(idx); } }; b.onpointerdown=e=>{ if(S.spellSolved||(e.pointerType==="mouse"&&e.button!==0))return; e.preventDefault(); try{b.releasePointerCapture(e.pointerId)}catch(_){} const clone=b.cloneNode(true); clone.id="dragClone"; document.body.append(clone); b.classList.add("dragging"); const moveTo=ev=>{const zoom=VZ();clone.style.left=ev.clientX/zoom+"px";clone.style.top=ev.clientY/zoom+"px"}; moveTo(e); let hovered=null; const hit=ev=>{ let found=null; document.querySelectorAll(".world-letter-slot:not(.filled)").forEach(slot=>{ const r=slot.getBoundingClientRect(),pad=8; if(ev.clientX>=r.left-pad&&ev.clientX<=r.right+pad&&ev.clientY>=r.top-pad&&ev.clientY<=r.bottom+pad)found=slot; }); return found; }; const move=ev=>{ moveTo(ev); const target=hit(ev); if(hovered&&hovered!==target)hovered.classList.remove("droptarget"); hovered=target; if(hovered)hovered.classList.add("droptarget"); }; const clean=()=>{ document.removeEventListener("pointermove",move); document.removeEventListener("pointerup",up); document.removeEventListener("pointercancel",cancel); clone.remove(); b.classList.remove("dragging"); if(hovered)hovered.classList.remove("droptarget"); }; const up=ev=>{ const target=hit(ev); clean(); if(target){ const idx=+target.dataset.index; if(letters[idx]===ch)settle(idx); else{target.classList.add("shake");setTimeout(()=>target.classList.remove("shake"),400);sndBad()} } }; const cancel=clean; document.addEventListener("pointermove",move); document.addEventListener("pointerup",up); document.addEventListener("pointercancel",cancel); }; return b; } function worldSpellAnswer(name,S,redraw){ if(worldSpellNeedsNewTray(S,name))worldSpellNewTray(S,name); const letters=worldSpellPool(name); const wrap=document.createElement("div"); wrap.className="world-spell-answer"; const wordRow=document.createElement("div"); wordRow.className="world-spell-word"; let slotIdx=0; [...name.toLowerCase()].forEach(ch=>{ if(!worldIsLetter(ch)){ const gap=document.createElement("span"); gap.className="world-spell-gap"; wordRow.append(gap); return; } const idx=slotIdx++; const slot=document.createElement("div"); slot.className="slot sm world-letter-slot"+(S.answerLetters[idx]!==null?" filled":""); slot.dataset.index=idx; const ghost=document.createElement("div"); ghost.className="ghost"; ghost.textContent=letters[idx]; const dots=dotGrid(letters[idx]); dots.classList.add("slot-dots"); slot.append(ghost,dots); if(S.answerLetters[idx]!==null){ const trayIndex=S.answerLetters[idx]; const placed=worldLetterBlock(S.trayLetters[trayIndex],trayIndex,letters,S,redraw); placed.classList.add("in-slot"); placed.tabIndex=-1; placed.onpointerdown=null; placed.onkeydown=null; slot.append(placed); slot.onclick=()=>{if(!S.spellSolved){S.answerLetters[idx]=null;redraw()}}; } wordRow.append(slot); }); wrap.append(wordRow); const tray=document.createElement("div"); tray.className="lg-tray world-spell-tray"; S.trayLetters.forEach((ch,trayIndex)=>{ if(S.answerLetters.includes(trayIndex))return; tray.append(worldLetterBlock(ch,trayIndex,letters,S,redraw)); }); wrap.append(tray); return wrap; } function mountWorldQuiz(root,init,config){ const exerciseOptions=config.type==="trees" ?[["recognize","worldExerciseRecognize"],["pair","worldExercisePair"],["clue","worldExerciseClue"],["photo","worldExercisePhoto"],["latin","worldExerciseLatin"]] :[["recognize","worldExerciseRecognize"],["clue","worldExerciseClue"],["photo","worldExercisePhoto"],["latin","worldExerciseLatin"]]; const S=Object.assign({ activity:"quiz",exercise:"recognize",level:1,view:config.type==="trees"?"mixed":config.type==="birds"?"bird":"plant", score:0,streak:0,target:"",taskView:"",options:[],picked:"",solved:false,feedback:"", flashRevealed:false,answerLetters:null,trayLetters:null,spellSolved:false, },init||{}); S.level=Math.max(1,Math.min(3,+S.level||1)); if(!exerciseOptions.some(([id])=>id===S.exercise))S.exercise="recognize"; const activity=worldSelect(S.activity,[["learn","worldLearn"],["quiz","worldQuiz"],["flash","worldFlash"],["spell","worldSpell"]]); const exercise=worldSelect(S.exercise,exerciseOptions); const level=worldSelect(String(S.level),[["1","lvl1"],["2","lvl2"],["3","lvl3"]]); const stage=document.createElement("div"); const exerciseSetting=field(T("worldExercise"),exercise); const settings=[field(T("worldActivity"),activity),exerciseSetting,field(T("worldLevel"),level)]; let view,viewSetting; if(config.type==="trees"){ view=worldSelect(S.view,[["tree","worldTreeWhole"],["leaf","worldTreeLeaf"],["mixed","worldTreeMixed"]]); viewSetting=field(T("worldTreeView"),view); settings.push(viewSetting); } 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.exercise==="pair"?"mixed":S.view==="mixed"?(Math.random()<.5?"tree":"leaf"):S.view :config.type==="birds"?"bird":"plant"; S.options=worldShuffle([target,...worldShuffle(items.filter(item=>item.id!==target.id)).slice(0,3)]).map(item=>item.id); S.picked=""; S.solved=false; S.feedback=""; S.flashRevealed=false; S.answerLetters=null; S.trayLetters=null; S.spellSolved=false; 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(); }; const assessFlash=ok=>{ if(S.solved)return; S.solved=true; if(ok){S.feedback="good";S.score++;S.streak++;sndGood()} else{S.feedback="bad";S.streak=0;sndBad()} draw(); }; const worldViewFor=()=>config.type==="trees"?(S.view==="mixed"?"tree":S.view):config.type==="birds"?"bird":"plant"; 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:config.type==="birds"?"bird":"plant"),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.exercise=exercise.value; S.level=+level.value; if(view)S.view=view.value; exerciseSetting.hidden=S.activity!=="quiz"; if(viewSetting)viewSetting.hidden=S.activity==="quiz"&&!["recognize","photo"].includes(S.exercise); 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"; if(S.activity==="flash"){ worldFlashCard(card,worldName(item),worldVisual(item,config.type,worldViewFor()),S,draw,()=>{S.flashRevealed=true;draw()},assessFlash); if(S.solved){ const fact=document.createElement("div"); fact.className="world-fact"; fact.append(worldIdentity(item)); const factHint=document.createElement("p"); factHint.textContent=worldHint(item); fact.append(factHint); card.append(fact); } const flashFeedback=document.createElement("p"); flashFeedback.className="world-feedback "+(S.feedback==="good"?"ok":S.feedback?"bad":""); flashFeedback.setAttribute("role","status"); if(S.feedback)flashFeedback.textContent=T(S.feedback==="good"?"worldCorrect":"worldTryAgain"); card.append(flashFeedback); const flashActions=document.createElement("div"); flashActions.className="world-actions"; flashActions.append(btn(T("worldNext"),newTask,"ghost")); card.append(flashActions); stage.append(card); return; } if(S.activity==="spell"){ card.append(worldVisual(item,config.type,worldViewFor())); const spellHint=document.createElement("p"); spellHint.className="world-spell-hint"; spellHint.textContent=T("worldSpellHint"); card.append(spellHint); card.append(worldSpellAnswer(worldName(item),S,draw)); if(!S.spellSolved&&S.answerLetters.every(v=>v!==null)){ S.spellSolved=true;S.score++;S.streak++;sndGood(); } const spellFeedback=document.createElement("p"); spellFeedback.className="world-feedback "+(S.spellSolved?"ok":""); spellFeedback.setAttribute("role","status"); if(S.spellSolved)spellFeedback.textContent=T("worldSpellComplete"); card.append(spellFeedback); if(S.spellSolved){ const fact=document.createElement("div"); fact.className="world-fact"; fact.append(worldIdentity(item)); const factHint=document.createElement("p"); factHint.textContent=worldHint(item); fact.append(factHint); card.append(fact); } const spellActions=document.createElement("div"); spellActions.className="world-actions"; spellActions.append(btn(T("worldNext"),newTask,"ghost")); card.append(spellActions); stage.append(card); return; } const question=document.createElement("h3"); if(S.exercise==="photo"){ const photoQuestion=config.type==="birds"?"worldFindBirdPhoto":config.type==="plants"?"worldFindPlantPhoto":"worldFindTreePhoto"; question.textContent=T(photoQuestion).replace("{name}",worldName(item)); card.append(question); const answers=document.createElement("div"); answers.className="world-photo-answers"; S.options.forEach((id,index)=>{ const option=config.items.find(entry=>entry.id===id); if(!option)return; const status=S.solved&&id===S.target?"correct":S.feedback==="bad"&&id===S.picked?"wrong":""; const optionView=config.type==="trees"?(S.taskView==="mixed"?"tree":S.taskView):config.type==="birds"?"bird":"plant"; answers.append(worldPhotoOption(option,config.type,optionView,index,()=>answer(id),status)); }); card.append(answers); }else{ const prompt=document.createElement("div"); if(S.exercise==="clue"){ question.textContent=T("worldQuestionClue"); prompt.className="world-clue"; prompt.textContent=worldHint(item); }else if(S.exercise==="latin"){ question.textContent=T("worldQuestionLatin"); prompt.className="world-clue world-clue-latin"; prompt.textContent=item.latin; }else if(S.exercise==="pair"){ question.textContent=T("worldQuestionPair"); prompt.append(worldVisual(item,config.type,"mixed")); }else{ const questionKey=config.type==="birds"?"worldQuestionBird":config.type==="plants"?"worldQuestionPlant":S.taskView==="leaf"?"worldQuestionLeaf":"worldQuestionTree"; question.textContent=T(questionKey); prompt.append(worldVisual(item,config.type,S.taskView)); } card.append(question,prompt); 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"); else if(S.feedback==="bad"&&id===S.picked)button.classList.add("wrong"); 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); } const isTaskActivity=()=>["quiz","flash","spell"].includes(S.activity); activity.onchange=()=>{S.activity=activity.value;if(isTaskActivity())newTask();else draw()}; exercise.onchange=()=>{S.exercise=exercise.value;if(isTaskActivity())newTask();else draw()}; level.onchange=()=>{S.level=+level.value;if(isTaskActivity())newTask();else draw()}; if(view)view.onchange=()=>{S.view=view.value;if(isTaskActivity())newTask();else draw()}; learningShell(root,settings,stage); if(isTaskActivity())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})} function mountPlants(root,init){return mountWorldQuiz(root,init,{type:"plants",items:WORLD_PLANTS})} 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"; const TOPO_WORLD_ASSET=WORLD_MEDIA_ROOT+"world-continents.svg"; const TOPO_WORLD_SOURCE="https://www.naturalearthdata.com/downloads/"; 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){ const wrap=document.createElement("div"); wrap.className="topo-map-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"})); const active=new Set(pool.map(item=>item.id)); if(mode==="regions"){ NL_REGIONS.forEach(region=>{ const group=svgNode("g",{ class:"topo-region"+(region.id===target?" target":"")+(region.id===picked?" picked":""), "data-id":region.id, }); region.provinces.forEach(provinceId=>{ const province=NL_PROVINCES.find(item=>item.id===provinceId); if(province)group.append(svgNode("use",{href:TOPO_MAP_ASSET+"#"+province.shape,fill:region.color,class:"topo-province"})); }); const title=svgNode("title"); title.textContent=worldName(region); group.append(title); if(onPick&&active.has(region.id))svgAction(group,worldName(region),()=>onPick(region.id)); svg.append(group); if(showLabels&&active.has(region.id)){ const label=svgNode("text",{x:region.x,y:region.y,class:"topo-label topo-region-label"}); label.textContent=worldName(region); svg.append(label); } }); }else{ NL_PROVINCES.forEach(province=>{ const selectable=mode==="provinces"&&active.has(province.id); const shape=svgNode("use",{ href:TOPO_MAP_ASSET+"#"+province.shape, fill:mode==="rivers"?"#dce8df":province.color, class:"topo-province"+(province.id===target?" target":"")+(province.id===picked?" picked":"")+(mode==="provinces"&&!selectable?" inactive":""), "data-id":province.id, }); const title=svgNode("title"); title.textContent=worldName(province); shape.append(title); if(onPick&&selectable)svgAction(shape,worldName(province),()=>onPick(province.id)); svg.append(shape); if(showLabels&&mode==="provinces"&&selectable){ const label=svgNode("text",{x:province.x,y:province.y,class:"topo-label"}); label.textContent=worldName(province); svg.append(label); } }); } if(mode==="rivers")NL_RIVERS.forEach(river=>{ const selectable=active.has(river.id); const group=svgNode("g",{ class:"topo-river"+(river.id===target?" target":"")+(river.id===picked?" picked":"")+(!selectable?" inactive":""), "data-id":river.id,style:"--river-color:"+river.color, }); group.append( svgNode("path",{d:river.path,class:"topo-river-hit"}), svgNode("path",{d:river.path,class:"topo-river-line"}), ); const title=svgNode("title"); title.textContent=worldName(river); group.append(title); if(onPick&&selectable)svgAction(group,worldName(river),()=>onPick(river.id)); svg.append(group); if(showLabels&&selectable){ const label=svgNode("text",{x:river.labelX,y:river.labelY,class:"topo-river-label"}); label.textContent=worldName(river); svg.append(label); } }); if(mode==="capitals"||mode==="cities")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 makeWorldMap(pool,onPick,showLabels,picked,target){ const wrap=document.createElement("div"); wrap.className="topo-map-wrap world-map-wrap"; const svg=svgNode("svg",{ class:"topo-map world-map",viewBox:"0 0 720 360", role:onPick?"group":"img","aria-label":T("topoWorldMapLabel"), }); svg.append(svgNode("rect",{x:0,y:0,width:720,height:360,rx:24,class:"topo-water"})); const active=new Set(pool.map(item=>item.id)); WORLD_CONTINENTS.forEach(continent=>{ const selectable=active.has(continent.id); const shape=svgNode("use",{ href:TOPO_WORLD_ASSET+"#"+continent.shape, fill:continent.color, class:"world-continent"+(continent.id===target?" target":"")+(continent.id===picked?" picked":"")+(!selectable?" inactive":""), "data-id":continent.id, }); const title=svgNode("title"); title.textContent=worldName(continent); shape.append(title); if(onPick&&selectable)svgAction(shape,worldName(continent),()=>onPick(continent.id)); svg.append(shape); if(showLabels&&selectable){ const label=svgNode("text",{x:continent.x,y:continent.y,class:"world-continent-label"}); label.textContent=worldName(continent); svg.append(label); } }); const source=document.createElement("a"); source.className="topo-source"; source.href=TOPO_WORLD_SOURCE; source.target="_blank"; source.rel="noopener noreferrer"; source.textContent=T("topoWorldMapSource"); wrap.append(svg); if(showLabels){ const legend=document.createElement("div"); legend.className="topo-world-legend"; pool.forEach(continent=>{ const entry=document.createElement("span"); const swatch=document.createElement("i"); swatch.style.background=continent.color; entry.append(swatch,worldName(continent)); legend.append(entry); }); wrap.append(legend); } wrap.append(source); return wrap; } function mountTopography(root,init){ const modeOptions=[ ["provinces","topoProvinces"],["capitals","topoCapitals"],["cities","topoCities"], ["regions","topoRegions"],["rivers","topoRivers"],["continents","topoContinents"], ]; const S=Object.assign({ activity:"quiz",mode:"provinces",level:1,score:0,streak:0, target:"",picked:"",solved:false,feedback:"", flashRevealed:false,answerLetters:null,trayLetters:null,spellSolved:false, },init||{}); S.level=Math.max(1,Math.min(3,+S.level||1)); if(!modeOptions.some(([id])=>id===S.mode))S.mode="provinces"; const activity=worldSelect(S.activity,[["learn","worldLearn"],["quiz","worldQuiz"],["flash","worldFlash"],["spell","worldSpell"]]); const mode=worldSelect(S.mode,modeOptions); const level=worldSelect(String(S.level),[["1","lvl1"],["2","lvl2"],["3","lvl3"]]); const stage=document.createElement("div"); const levelSetting=field(T("worldLevel"),level); const pool=()=>{ const items=S.mode==="provinces"?NL_PROVINCES :S.mode==="capitals"?NL_CITIES.filter(item=>item.capital) :S.mode==="cities"?NL_CITIES.filter(item=>!item.capital) :S.mode==="regions"?NL_REGIONS :S.mode==="rivers"?NL_RIVERS :WORLD_CONTINENTS; return items.filter(item=>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=""; S.flashRevealed=false;S.answerLetters=null;S.trayLetters=null;S.spellSolved=false; 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(); }; const assessFlash=ok=>{ if(S.solved)return; S.solved=true; if(ok){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; levelSetting.hidden=S.mode==="regions"; 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"; const renderMap=(interactive,labels,pickedId,targetId)=>S.mode==="continents" ?makeWorldMap(pool(),interactive,labels,pickedId,targetId) :makeTopoMap(S.mode,pool(),interactive,labels,pickedId,targetId); if(S.activity==="learn"){ const intro=document.createElement("p"); intro.className="world-intro"; intro.textContent=T(S.mode==="regions"?"topoExploreRegions":S.mode==="rivers"?"topoExploreRivers":S.mode==="continents"?"topoExploreContinents":"topoExploreHint"); card.append(intro,renderMap(null,true,"","")); }else if(S.activity==="flash"){ const item=current(); worldFlashCard(card,worldName(item),renderMap(null,S.flashRevealed,"",S.flashRevealed?S.target:""),S,draw,()=>{S.flashRevealed=true;draw()},assessFlash); const flashFeedback=document.createElement("p"); flashFeedback.className="world-feedback "+(S.feedback==="good"?"ok":S.feedback?"bad":""); flashFeedback.setAttribute("role","status"); if(S.feedback)flashFeedback.textContent=T(S.feedback==="good"?"worldCorrect":"worldTryAgain"); card.append(flashFeedback); const flashActions=document.createElement("div"); flashActions.className="world-actions"; flashActions.append(btn(T("worldNext"),newTask,"ghost")); card.append(flashActions); }else if(S.activity==="spell"){ const item=current(); card.append(renderMap(null,true,"",S.target)); const spellHint=document.createElement("p"); spellHint.className="world-spell-hint"; spellHint.textContent=T("worldSpellHint"); card.append(spellHint); card.append(worldSpellAnswer(worldName(item),S,draw)); if(!S.spellSolved&&S.answerLetters.every(v=>v!==null)){ S.spellSolved=true;S.score++;S.streak++;sndGood(); } const spellFeedback=document.createElement("p"); spellFeedback.className="world-feedback "+(S.spellSolved?"ok":""); spellFeedback.setAttribute("role","status"); if(S.spellSolved)spellFeedback.textContent=T("worldSpellComplete"); card.append(spellFeedback); const spellActions=document.createElement("div"); spellActions.className="world-actions"; spellActions.append(btn(T("worldNext"),newTask,"ghost")); card.append(spellActions); }else{ const item=current(),question=document.createElement("h3"); const questionKey=S.mode==="provinces"?"topoFindProvince" :S.mode==="capitals"?"topoFindCapital" :S.mode==="cities"?"topoFindCity" :S.mode==="regions"?"topoFindRegion" :S.mode==="rivers"?"topoFindRiver" :"topoFindContinent"; question.textContent=T(questionKey).replace("{name}",worldName(item)); card.append(question,renderMap(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 isTaskActivity=()=>["quiz","flash","spell"].includes(S.activity); activity.onchange=()=>{S.activity=activity.value;if(isTaskActivity())newTask();else draw()}; mode.onchange=()=>{S.mode=mode.value;if(isTaskActivity())newTask();else draw()}; level.onchange=()=>{S.level=+level.value;if(isTaskActivity())newTask();else draw()}; learningShell(root,[ field(T("worldActivity"),activity), field(T("topoMode"),mode), levelSetting, ],stage); if(isTaskActivity())newTask();else draw(); return{getState:()=>({...S})}; }