'Birds', 'fish' => 'Fish', 'plant' => 'Plants', 'invertebrate' => 'Invertebrates', 'mammal' => 'Mammals', 'other' => 'Other life']; $order = array_flip(array_keys($groups)); usort($species, fn ($a, $b) => ($order[$a['group']] ?? 9) <=> ($order[$b['group']] ?? 9) ?: strcmp($a['english'], $b['english'])); $counts = []; foreach ($species as $s) { $counts[$s['group']] = ($counts[$s['group']] ?? 0) + 1; } $monthNames = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; /* ---------------------------------------------------------------- year views Every species carries the months it is present and the months it breeds, spawns or flowers (West Iceland calendar). Two views: counts per month for each group (small multiples, one hue per group, the subset overlaid in the full hue), and a calendar strip per species. Inline SVG, no script. */ const SP_COLOURS = ['bird' => '#3987e5', 'fish' => '#d95926', 'plant' => '#199e70', 'invertebrate' => '#c98500', 'mammal' => '#9085e9', 'other' => '#d55181']; const SP_SUBLABEL = ['bird' => 'breeding', 'fish' => 'spawning', 'plant' => 'in flower', 'invertebrate' => '', 'mammal' => '', 'other' => '']; function sp_year_chart(array $species, array $groups, int $thisMonth, array $monthNames): string { $present = []; $subset = []; foreach ($species as $sp) { $g = $sp['group']; for ($m = 1; $m <= 12; $m++) { $present[$g][$m] = ($present[$g][$m] ?? 0) + (in_array($m, $sp['months']['present'] ?? [], true) ? 1 : 0); $subset[$g][$m] = ($subset[$g][$m] ?? 0) + (in_array($m, $sp['months']['breeding'] ?? [], true) ? 1 : 0); } } $out = '
'; $rows = []; foreach ($groups as $g => $label) { if (empty($present[$g])) { continue; } $max = max(1, max($present[$g])); $col = SP_COLOURS[$g] ?? '#a1a1aa'; $sub = SP_SUBLABEL[$g] ?? ''; $total = count(array_filter($species, fn ($x) => $x['group'] === $g)); $out .= '

'.h($label).'

'.$total.' species
'; $out .= '
present'.($sub !== '' ? ''.h($sub).'' : '').'
'; $W = 360; $H = 150; $left = 6; $right = 6; $top = 20; $base = 126; $slot = ($W - $left - $right) / 12; $bw = 20; $out .= ''; $nx = $left + ($thisMonth - 1) * $slot; $out .= ''; $out .= ''; for ($m = 1; $m <= 12; $m++) { $pv = $present[$g][$m]; $sv = $subset[$g][$m]; $x = $left + ($m - 1) * $slot + ($slot - $bw) / 2; $ph = ($base - $top - 6) * $pv / $max; $sh = ($base - $top - 6) * $sv / $max; $tip = $monthNames[$m - 1].': '.$pv.' '.strtolower($label).' present'.($sub !== '' ? ', '.$sv.' '.$sub : ''); $out .= ''.h($tip).''; if ($pv > 0) { $out .= sp_bar($x, $base - $ph, $bw, $ph, $col, .38); } if ($sv > 0) { $out .= sp_bar($x, $base - $sh, $bw, $sh, $col, 1); } $out .= ''.$pv.''; $out .= ''.substr($monthNames[$m - 1], 0, 1).''; } $out .= '
'; $rows[] = [$label, $present[$g], $subset[$g], $sub]; } $out .= '
'; /* table view for readers who want the numbers */ $out .= '
Table view'; foreach ($monthNames as $mn) { $out .= ''; } $out .= ''; foreach ($rows as [$label, $pv, $sv, $sub]) { $out .= ''; for ($m = 1; $m <= 12; $m++) { $out .= ''; } $out .= ''; if ($sub !== '') { $out .= ''; for ($m = 1; $m <= 12; $m++) { $out .= ''; } $out .= ''; } } $out .= '
Group'.h($mn).'
'.h($label).' present'.$pv[$m].'
'.h($label).' '.h($sub).''.$sv[$m].'
'; return $out; } /** A bar with rounded top corners, anchored to the baseline. */ function sp_bar(float $x, float $y, float $w, float $hgt, string $col, float $op): string { $r = min(4, $hgt / 2, $w / 2); $x2 = $x + $w; $y2 = $y + $hgt; $d = sprintf('M%.1f %.1f L%.1f %.1f L%.1f %.1f Q%.1f %.1f %.1f %.1f L%.1f %.1f Q%.1f %.1f %.1f %.1f Z', $x, $y2, $x, $y + $r, $x, $y + $r, $x, $y, $x + $r, $y, $x2 - $r, $y, $x2, $y, $x2, $y + $r) . sprintf(' M%.1f %.1f L%.1f %.1f', $x2, $y + $r, $x2, $y2); // simpler: build the closed shape explicitly $d = sprintf('M%.1f %.1f L%.1f %.1f Q%.1f %.1f %.1f %.1f L%.1f %.1f Q%.1f %.1f %.1f %.1f L%.1f %.1f Z', $x, $y2, $x, $y + $r, $x, $y, $x + $r, $y, $x2 - $r, $y, $x2, $y, $x2, $y + $r, $x2, $y2); return ''; } /* ------------------------------------------------------------ since the baseline The sightings register lives on the AI operations platform; its public feed carries species, date, count, place, the observer's role and a confidence, nothing personal. Cached here for five minutes. */ const SP_FEED = 'https://districthive.com/ai_discover/api/sightings.php'; function sp_feed(): array { $cache = sys_get_temp_dir().'/dh-research-sightings.json'; if (is_file($cache) && filemtime($cache) > time() - 300) { $j = json_decode((string) file_get_contents($cache), true); if (is_array($j) && isset($j['sightings'])) { return $j; } } $ctx = stream_context_create(['http' => ['timeout' => 4, 'header' => "User-Agent: DistricthiveResearchLabs/1.0\r\n"]]); $raw = @file_get_contents(SP_FEED, false, $ctx); $j = $raw !== false ? json_decode($raw, true) : null; if (is_array($j) && isset($j['sightings'])) { @file_put_contents($cache, $raw); return $j; } if (is_file($cache)) { $j = json_decode((string) file_get_contents($cache), true); if (is_array($j) && isset($j['sightings'])) { return $j; } } return ['sightings' => [], 'unavailable' => true, 'baseline_dates' => ['2021-06-04', '2021-07-03', '2021-08-04', '2021-09-02']]; } function sp_since_baseline(array $species, array $groups, int $thisMonth, array $monthNames): string { $feed = sp_feed(); $sightings = array_values(array_filter(is_array($feed['sightings'] ?? null) ? $feed['sightings'] : [], fn ($x) => is_array($x) && ($x['confidence'] ?? '') !== 'unsure' && is_string($x['date'] ?? null) && preg_match('/^\d{4}-\d\d-\d\d$/', $x['date']) && strtotime($x['date']) !== false && !empty($x['species']))); $byId = []; // species id => sightings $new = []; // name => sightings of species not in the baseline foreach ($sightings as $x) { if (!empty($x['species_id'])) { $byId[$x['species_id']][] = $x; } else { $new[mb_strtolower((string) $x['species'])][] = $x; } } $watch = array_values(array_filter($species, fn ($sp) => $sp['group'] !== 'plant')); // plants are checked by season, not by sighting $seen = array_values(array_filter($watch, fn ($sp) => !empty($byId[$sp['id']]))); $notYet = array_values(array_filter($watch, fn ($sp) => empty($byId[$sp['id']]))); $plantsSeen = count(array_filter($species, fn ($sp) => $sp['group'] === 'plant' && !empty($byId[$sp['id']]))); $base = $feed['baseline_dates'] ?? []; $baseLabel = $base !== [] ? date('j M Y', strtotime($base[0])).' to '.date('j M Y', strtotime(end($base))) : '2021'; $out = '

Since the baseline: what has checked out

'; $out .= '

Two comparisons. First, the surveyors\' own count days, eleven visits between 2011 and 2021, set against the period each species is expected here: a ring marks a month in which the species was actually counted on one of those days. Second, every sighting logged since by the team on the operations dashboard, and later by the wildlife scanner, set against the '.count($watch).' birds, fish and invertebrates of the baseline. Plants are checked by season rather than by sighting. Guest reports are not logged, since they cannot be checked.

'; if (!empty($feed['unavailable'])) { $out .= '
The sightings register could not be read just now; the last known state is shown.
'; } /* stats */ $out .= '
'.count($seen).'of '.count($watch).' seen again
'; $out .= '
'.count($notYet).'not seen yet
'; $out .= '
'.count($new).'species new to this place
'; $out .= '
'.count($sightings).'sightings logged
'; if ($plantsSeen > 0) { $out .= '
'.$plantsSeen.'plants logged as seen
'; } $out .= '
'; /* chart: expected and seen, by month */ $expected = array_fill(1, 12, 0); $seenM = array_fill(1, 12, 0); foreach ($watch as $sp) { $months = []; foreach ($byId[$sp['id']] ?? [] as $x) { $months[(int) date('n', strtotime($x['date']))] = true; } for ($m = 1; $m <= 12; $m++) { if (in_array($m, $sp['months']['present'] ?? [], true)) { $expected[$m]++; } if (isset($months[$m])) { $seenM[$m]++; } } } $max = max(1, max($expected)); $W = 720; $H = 190; $left = 8; $right = 8; $top = 22; $baseY = 164; $slot = ($W - $left - $right) / 12; $bw = 22; $gap = 3; $out .= '

Expected and seen, by month

species with any sighting in that month since the baseline
'; $out .= '
expected from the baselineseen again
'; $out .= ''; $nx = $left + ($thisMonth - 1) * $slot; $out .= ''; $out .= ''; for ($m = 1; $m <= 12; $m++) { $x0 = $left + ($m - 1) * $slot + ($slot - 2 * $bw - $gap) / 2; $eh = ($baseY - $top - 6) * $expected[$m] / $max; $sh = ($baseY - $top - 6) * $seenM[$m] / $max; $out .= ''.h($monthNames[$m - 1].': '.$expected[$m].' expected, '.$seenM[$m].' seen again').''; if ($expected[$m] > 0) { $out .= sp_bar($x0, $baseY - $eh, $bw, $eh, '#ffffff', .28); } if ($seenM[$m] > 0) { $out .= sp_bar($x0 + $bw + $gap, $baseY - $sh, $bw, $sh, '#ffd701', 1); } $out .= ''.$expected[$m].''; $out .= ''.$seenM[$m].''; $out .= ''.h($monthNames[$m - 1]).''; } $out .= '
'; /* timeline: sightings per month since the baseline */ $start = $base !== [] ? strtotime(substr($base[0], 0, 7).'-01') : strtotime('2021-06-01'); $months = []; $t = $start; $now = time(); while ($t <= $now && count($months) < 200) { $months[] = date('Y-m', $t); $t = strtotime('+1 month', $t); } $perMonth = array_fill_keys($months, 0); $spMonth = array_fill_keys($months, []); foreach ($sightings as $x) { $k = substr((string) $x['date'], 0, 7); if (isset($perMonth[$k])) { $perMonth[$k]++; $spMonth[$k][$x['species']] = true; } } $n = count($months); $max2 = max(1, max($perMonth)); $W2 = 720; $H2 = 150; $l2 = 8; $r2 = 8; $t2 = 18; $b2 = 122; $cw = ($W2 - $l2 - $r2) / max(1, $n); $out .= '

Sightings over time

logged per month since the baseline surveys; the survey days are marked
'; $out .= ''; $out .= ''; foreach ($base as $bd) { $bi = array_search(substr($bd, 0, 7), $months, true); if ($bi !== false) { $bx = $l2 + $bi * $cw + $cw * ((int) date('j', strtotime($bd)) / 31); $out .= 'Baseline survey '.h(date('j M Y', strtotime($bd))).''; } } foreach ($months as $i => $ym) { $x = $l2 + $i * $cw; $v = $perMonth[$ym]; $hgt = ($b2 - $t2 - 4) * $v / $max2; if ($v > 0) { $out .= ''.h(date('M Y', strtotime($ym.'-01')).': '.$v.' sighting'.($v === 1 ? '' : 's').', '.count($spMonth[$ym]).' species').''.sp_bar($x + 1, $b2 - $hgt, max(2, $cw - 2), $hgt, '#ffd701', 1).''.$v.''; } if (substr($ym, 5) === '01' || $i === 0) { $out .= ''.substr($ym, 0, 4).''; } } $out .= ''; if ($sightings === []) { $out .= '

No sightings logged yet. The register opens on the operations dashboard; the baseline stands alone until the first one.

'; } $out .= '
baseline survey daysightings logged
'; /* the survey days themselves */ $days = $GLOBALS['d']['survey_days'] ?? []; if ($days !== []) { $perDay = []; foreach ($species as $sp) { foreach ($sp['survey_counts'] ?? [] as $c) { $perDay[$c['date']][$sp['id']] = true; } } $out .= '

The survey days

what was done on each visit and how many species were recorded
'; foreach ($days as $dy) { $n = count($perDay[$dy['date']] ?? []); $out .= '
'.h(date('j M Y', strtotime($dy['date']))).''.h($dy['what']).''.$n.' species
'; } $out .= '

Counts on the lagoon were made by telescope from the main road; the 2021 visits also mapped territories on the point, fished the streams and walked the vegetation. Sampling on eleven days cannot show every month, so the rings below are evidence of presence, never of absence.

'; } /* per species: the baseline calendar with the sightings on it */ $out .= '

Species by species

expected period in colour; a ring where the surveyors counted it on a survey day; a dot where the register has a sighting
'; $rows = $seen; usort($rows, function ($a, $b) use ($byId) { $la = max(array_column($byId[$a['id']], 'date')); $lb = max(array_column($byId[$b['id']], 'date')); return strcmp($lb, $la) ?: strcmp($a['english'], $b['english']); }); $ny = $notYet; usort($ny, fn ($a, $b) => strcmp($a['group'], $b['group']) ?: strcmp($a['english'], $b['english'])); $out .= '
'; for ($m = 1; $m <= 12; $m++) { $out .= ''.substr($monthNames[$m - 1], 0, 1).''; } $out .= 'status
'; foreach (array_merge($rows, $ny) as $sp) { $col = SP_COLOURS[$sp['group']] ?? '#a1a1aa'; $pres = $sp['months']['present'] ?? []; $br = $sp['months']['breeding'] ?? []; $dots = []; $rings = []; foreach ($byId[$sp['id']] ?? [] as $x) { $mm = (int) date('n', strtotime($x['date'])); $dots[$mm] = ($dots[$mm] ?? 0) + 1; } foreach ($sp['survey_counts'] ?? [] as $c) { $mm = (int) date('n', strtotime($c['date'])); $rings[$mm][] = date('j M Y', strtotime($c['date'])).': '.$c['count'].' ('.$c['kind'].')'; } $out .= '
'.h($sp['english']).''; for ($m = 1; $m <= 12; $m++) { $cls = in_array($m, $br, true) ? 'b' : (in_array($m, $pres, true) ? 'p' : ''); $d = $dots[$m] ?? 0; $r = $rings[$m] ?? []; $tip = $sp['english'].', '.$monthNames[$m - 1].': '.($cls === 'b' ? 'breeding expected' : ($cls === 'p' ? 'expected' : 'not expected')).($r !== [] ? '. Survey: '.implode('; ', $r) : '').($d ? '. Register: '.$d.' sighting'.($d === 1 ? '' : 's') : ''); $out .= ''.($r !== [] ? '' : '').($d ? '' : '').''; } $sd = count(array_unique(array_column($sp['survey_counts'] ?? [], 'date'))); if (!empty($byId[$sp['id']])) { $last = max(array_column($byId[$sp['id']], 'date') ?: ['']); $out .= 'seen again · '.h($last !== '' ? date('j M Y', strtotime($last)) : '').''; } else { $out .= ''.($sd ? $sd.' survey day'.($sd === 1 ? '' : 's').' · not since' : 'not seen').''; } $out .= '
'; } $out .= '
'; if ($new !== []) { $out .= '

New to this place, not in the 2021 baseline

'; } $out .= '
expectedbreeding, spawningcounted on a survey dayregister sighting
'; $out .= '

Sightings are logged on the operations dashboard by the team, and later by the wildlife scanner and the acoustic monitors; each carries a date, a count where given, a place and a confidence, never a person. Guest reports are not logged.

'; return $out; } function sp_timeline(array $rows, string $group, int $thisMonth, array $monthNames): string { $col = SP_COLOURS[$group] ?? '#a1a1aa'; $sub = SP_SUBLABEL[$group] ?? ''; usort($rows, function ($a, $b) { $pa = $a['months']['present'] ?? []; $pb = $b['months']['present'] ?? []; return (min($pa ?: [13]) <=> min($pb ?: [13])) ?: (count($pb) <=> count($pa)) ?: strcmp($a['english'], $b['english']); }); $out = '
'; for ($m = 1; $m <= 12; $m++) { $out .= ''.substr($monthNames[$m - 1], 0, 1).''; } $out .= '
'; foreach ($rows as $sp) { $pres = $sp['months']['present'] ?? []; $br = $sp['months']['breeding'] ?? []; $out .= '
'.h($sp['english']).''; for ($m = 1; $m <= 12; $m++) { $cls = in_array($m, $br, true) ? 'b' : (in_array($m, $pres, true) ? 'p' : ''); $tip = $sp['english'].', '.$monthNames[$m - 1].': '.($cls === 'b' ? $sub : ($cls === 'p' ? 'present' : 'not expected')); $out .= ''; } $out .= '
'; } $out .= '
present'.($sub !== '' ? ''.h($sub).'' : '').'
'; return $out; } $thisMonth = (int) gmdate('n'); $mParam = (int) ($_GET['m'] ?? $thisMonth); if ($mParam < 1 || $mParam > 12) { $mParam = $thisMonth; } $hereNow = array_values(array_filter($species, fn ($s) => in_array($mParam, $s['months']['present'] ?? [], true) && $s['group'] !== 'plant')); $plantsNow = array_values(array_filter($species, fn ($s) => in_array($mParam, $s['months']['present'] ?? [], true) && $s['group'] === 'plant')); page_header('Species', 'species'); echo '

Species of Skerðingsstaðir and Lárvaðall

'; echo '

Before anything was built, two independent field surveys in 2021 recorded what lives on this land and in the lagoon beside it: the birds that breed on the point, the fish that run up from the fjord, the plants that make up each habitat. This page is that baseline, species by species, with what the surveyors found here, what is known about each one, and the months you are likely to meet it. The Outpost, Districthive\'s Human Recharging Station, now stands on the point; later counts will be compared against these.

'; if ($species === []) { echo '

Species

The species data has not been installed yet.
'; page_footer(); return; } /* summary strip */ echo '
'; foreach ($groups as $g => $label) { if (empty($counts[$g])) { continue; } echo ''.(int) $counts[$g].''.h($label).''; } echo '
'.count($species).'species recorded
'; echo '
'; /* this month */ echo '

Who is here in '.h($monthNames[$mParam - 1]).'

'; echo '
'; for ($i = 1; $i <= 12; $i++) { echo ''; } echo '
'; if ($hereNow === []) { echo '

No bird or fish record for this month; the surveys were made in summer and the winter picture is inferred from what is known of each species.

'; } else { echo '
'; foreach ($hereNow as $s) { $b = in_array($mParam, $s['months']['breeding'] ?? [], true); echo ''.($s['image']['file'] ?? '' ? '' : '').''.h($s['english']).''.($b ? 'breeding' : '').''; } echo '
'; } if ($plantsNow !== []) { echo '

'.count($plantsNow).' of the recorded plants are in leaf or flower this month.

'; } echo '

Months are drawn from each species\' known Icelandic calendar (breeding, passage, wintering, growing season), not from counts made here every month. The survey dates themselves are given on every card.

'; echo '
'; /* the year, month by month */ echo '

The year, month by month

'; echo '

How many of the recorded species you can expect in each month, and how many are breeding, spawning or in flower. The pale bar is every species present; the solid bar is the subset in its season. The current month is shaded.

'; echo sp_year_chart($species, $groups, $thisMonth, $monthNames); echo '
'; $byGroup = []; foreach ($species as $sp) { $byGroup[$sp['group']][] = $sp; } if (!empty($byGroup['bird'])) { echo '

Bird calendar

Residents first, then arrivals in order. Solid cells are the breeding months.

'.sp_timeline($byGroup['bird'], 'bird', $thisMonth, $monthNames).'
'; } echo '
'; if (!empty($byGroup['fish'])) { echo '

Fish calendar

Solid cells are the spawning months.

'.sp_timeline($byGroup['fish'], 'fish', $thisMonth, $monthNames).'
'; } if (!empty($byGroup['invertebrate'])) { echo '

Invertebrates

'.sp_timeline($byGroup['invertebrate'], 'invertebrate', $thisMonth, $monthNames).'
'; } echo '
'; if (!empty($byGroup['plant'])) { echo '
Plant calendar '.count($byGroup['plant']).' plants: growing season and flowering months'.sp_timeline($byGroup['plant'], 'plant', $thisMonth, $monthNames).'
'; } /* since the baseline */ echo sp_since_baseline($species, $groups, $thisMonth, $monthNames); /* filter */ echo '
'; echo ''; foreach ($groups as $g => $label) { if (!empty($counts[$g])) { echo ''; } } echo ''; echo '
'; /* cards by group */ $srcTitle = []; foreach ($sources as $src) { $srcTitle[$src['key']] = $src['short'] ?? $src['title']; } foreach ($groups as $g => $label) { if (empty($counts[$g])) { continue; } echo '

'.h($label).' '.(int) $counts[$g].'

'; echo '
'; foreach ($species as $s) { if ($s['group'] !== $g) { continue; } $img = $s['image']['file'] ?? ''; echo '
'; if ($img !== '') { echo '
'.h($s['english']).''; if (!empty($s['image']['credit'])) { echo '
'.h($s['image']['credit']).(!empty($s['image']['license']) ? ' · '.h($s['image']['license']) : '').'
'; } echo '
'; } else { echo '
'.h(mb_substr($s['english'], 0, 1)).'
'; } echo '

'.h($s['english']).'

'; echo '
'.h($s['icelandic']).($s['scientific'] !== '' ? ' · '.h($s['scientific']).'' : '').'
'; if (!empty($s['status'])) { echo '
'.h($s['status']).'
'; } if (!empty($s['site_facts'])) { echo '
In the survey
    '; foreach ($s['site_facts'] as $f) { echo '
  • '.h($f['text']).(isset($f['source']) ? ' '.h($srcTitle[$f['source']] ?? $f['source']).(isset($f['page']) ? ', p. '.(int) $f['page'] : '').'' : '').'
  • '; } echo '
'; } if (!empty($s['about'])) { echo '
About
    '; foreach ($s['about'] as $f) { echo '
  • '.h($f).'
  • '; } echo '
'; } $present = $s['months']['present'] ?? []; $breed = $s['months']['breeding'] ?? []; if ($present !== []) { echo '
'.h($s['months']['label'] ?? 'When').'
'; for ($i = 1; $i <= 12; $i++) { $cls = in_array($i, $breed, true) ? 'b' : (in_array($i, $present, true) ? 'p' : ''); echo ''.substr($monthNames[$i - 1], 0, 1).''; } echo '
'; if (!empty($s['months']['note'])) { echo '
'.h($s['months']['note']).'
'; } } if (!empty($s['conservation'])) { echo '
'.h($s['conservation']).'
'; } if (!empty($s['wiki'])) { echo ''; } echo '
'; } echo '
'; } /* gallery from the reports */ if ($gallery !== []) { echo '

From the surveys

'; foreach ($gallery as $gph) { echo '
'.h($gph['caption']).'
'.h($gph['caption']).''.h($gph['credit']).'
'; } echo '
'; } /* survey facts and sources */ if ($survey !== []) { echo '

How the surveys were made

'; foreach ($survey as $blk) { echo '

'.h($blk['title']).'

    '; foreach ($blk['facts'] as $f) { echo '
  • '.h($f).'
  • '; } echo '
'; } echo '
'; } if ($sources !== []) { echo '

Sources

Species pictures are from Wikimedia Commons under their stated licences, or from the survey reports where credited. General facts summarise the published literature on each species; the site facts are the surveyors\' own findings. Corrections are welcome at '.h(CONTACT_EMAIL).'.

'; } echo ''; page_footer();