SPB Git forge
3commits 1branches 0releases
417.0 KBsize
maindefault branch
10 days agolast push
TypeScript 66.5% Python 30.9% JavaScript 1.4% CSS 0.7%
10.8 KB · 184 lines tsx
Raw Blame History
1import type { Metadata } from 'next';2import Link from 'next/link';3import { HBars, StackedBars } from '@/components/charts/charts';4import { ChartBlock, Disclaimer, InlineRank, Note, StatGrid, TwoCol } from '@/components/stats/shared';5import { TypeBadge } from '@/components/ui/badges';6import { Container, PageHeader, Section, Stat } from '@/components/ui/section';7import { Unavailable } from '@/components/ui/unavailable';8import { api, safe } from '@/lib/api';9import { fmt2, fmtAgo, fmtDate, fmtInt, fmtKm, num } from '@/lib/format';10import { routes, SITE_NAME, SITE_URL } from '@/lib/site';1112export const revalidate = 900;1314const TITLE = 'Space debris — catalogued fragments, rocket bodies and their sources';15const DESC = 'Tracked space debris on orbit: totals, debris by country, altitude distribution, fragmentation events with the most fragments still on orbit, debris growth by launch year and the largest tracked objects. Catalogued object counts only.';1617export async function generateMetadata(): Promise<Metadata> {18  const url = `${SITE_URL}${routes.debris()}`;19  return {20    title: TITLE,21    description: DESC,22    alternates: { canonical: url },23    openGraph: { title: `${TITLE} | ${SITE_NAME}`, description: DESC, url, type: 'website', siteName: SITE_NAME },24    twitter: { card: 'summary_large_image', title: `${TITLE} | ${SITE_NAME}`, description: DESC },25  };26}2728export default async function DebrisPage() {29  const res = await safe(api.debris());30  const d = res?.data ?? null;3132  const byCountry = d ? [...d.by_country].sort((a, b) => (num(b.debris) ?? 0) - (num(a.debris) ?? 0)) : [];33  const debrisBars = byCountry.filter((c) => (num(c.debris) ?? 0) > 0).slice(0, 12).map((c) => ({ label: c.name, value: num(c.debris) ?? 0, color: 'var(--series-4)' }));34  const rbBars = [...byCountry].sort((a, b) => (num(b.rocket_bodies) ?? 0) - (num(a.rocket_bodies) ?? 0)).filter((c) => (num(c.rocket_bodies) ?? 0) > 0).slice(0, 12).map((c) => ({ label: c.name, value: num(c.rocket_bodies) ?? 0, color: 'var(--series-2)' }));3536  const byAlt = d ? [...d.by_altitude].sort((a, b) => (num(a.alt_km) ?? 0) - (num(b.alt_km) ?? 0)).map((r) => ({ x: String(num(r.alt_km) ?? 0), debris: num(r.debris) ?? 0, rocket_bodies: num(r.rocket_bodies) ?? 0 })) : [];37  const growth = d ? [...d.debris_growth].sort((a, b) => a.year - b.year).map((r) => {38    const cat = num(r.debris_catalogued) ?? 0;39    const still = num(r.debris_still_on_orbit) ?? 0;40    return { x: r.year, still_on_orbit: still, decayed: Math.max(0, cat - still) };41  }) : [];42  const decays = d ? [...d.decays_by_year].sort((a, b) => a.year - b.year).map((r) => ({ x: r.year, payloads: num(r.payloads) ?? 0, debris: num(r.debris) ?? 0, rocket_bodies: num(r.rocket_bodies) ?? 0 })) : [];43  const largest = d ? [...d.largest_objects].sort((a, b) => (num(b.rcs_m2) ?? 0) - (num(a.rcs_m2) ?? 0)) : [];4445  return (46    <Container>47      <PageHeader eyebrow="Debris" title="Space debris on orbit" lede="Catalogued fragments and spent rocket bodies currently tracked, where they come from and how the population has evolved since 1957. Counts come from the SATCAT as normalised by SatelliteIndex.">48        {res && <p className="mono mt-4 text-xs text-ink-3">Generated {fmtAgo(res.meta.generated_at)}</p>}49      </PageHeader>5051      {!d ? (52        <div className="py-6">53          <Unavailable what="Debris statistics" />54        </div>55      ) : (56        <>57          <div className="pb-8">58            <Disclaimer text={d.disclaimer} />59          </div>6061          <Section eyebrow="Totals" title="Non-operational objects on orbit" className="pt-0 md:pt-0">62            <StatGrid cols={4}>63              <Stat label="Debris" value={fmtInt(d.totals.debris)} accent hint="Catalogued fragments on orbit" />64              <Stat label="Rocket bodies" value={fmtInt(d.totals.rocket_bodies)} hint="Spent stages on orbit" />65              <Stat label="Unknown type" value={fmtInt(d.totals.unknown)} hint="Unclassified objects on orbit" />66              <Stat label="Inactive payloads" value={fmtInt(d.totals.inactive_payloads)} hint="Non-operational satellites on orbit" />67            </StatGrid>68          </Section>6970          <Section eyebrow="By country" title="Who the debris is attributed to" action={{ href: routes.rankings('countries-debris'), label: 'Full ranking' }}>71            <TwoCol>72              <ChartBlock title="Debris on orbit by country" hint="SATCAT owner code → country">73                {debrisBars.length ? <HBars data={debrisBars} /> : <Unavailable what="Debris by country" />}74                <ul className="mt-3 flex flex-wrap gap-x-4 gap-y-1 text-xs">75                  {byCountry.slice(0, 12).map((c) => (76                    <li key={c.code}>77                      <Link href={routes.country(c.slug)} className="link">{c.name}</Link>78                    </li>79                  ))}80                </ul>81              </ChartBlock>82              <ChartBlock title="Rocket bodies on orbit by country">{rbBars.length ? <HBars data={rbBars} /> : <Unavailable what="Rocket bodies by country" />}</ChartBlock>83            </TwoCol>84          </Section>8586          <Section eyebrow="Altitude" title="Where debris and rocket bodies orbit">87            <ChartBlock title="Debris and rocket bodies by perigee altitude" hint="50 km bins" derived>88              <StackedBars data={byAlt} keys={['debris', 'rocket_bodies']} labels={{ debris: 'Debris', rocket_bodies: 'Rocket bodies' }} title="Debris and rocket bodies on orbit by perigee altitude, 50 km bins (km)" height={220} xTicks={10} />89              <Note className="mt-2">x-axis = lower edge of each 50 km perigee bin (km). Objects without a current element set are not placed. Informational density only.</Note>90            </ChartBlock>91          </Section>9293          <Section eyebrow="Sources" title="Launches with the most fragments still on orbit" action={{ href: routes.launches(), label: 'All launches' }}>94            {d.by_launch.length === 0 ? (95              <Unavailable what="Fragmentation sources" />96            ) : (97              <table className="data-table stack text-sm">98                <thead>99                  <tr>100                    <th>Launch</th>101                    <th>Date</th>102                    <th>Site</th>103                    <th>Owners</th>104                    <th className="num">Debris on orbit</th>105                  </tr>106                </thead>107                <tbody>108                  {d.by_launch.map((l, i) => (109                    <tr key={l.cospar_launch_id}>110                      <td className="primary">111                        <InlineRank n={i + 1} />112                        <Link href={routes.launch(l.cospar_launch_id)} className="link font-medium">{l.primary_name ?? l.cospar_launch_id}</Link>113                        <span className="mono ml-2 text-xs text-ink-3">{l.cospar_launch_id}</span>114                      </td>115                      <td data-label="Date" className="mono text-xs">{fmtDate(l.launch_date)}</td>116                      <td data-label="Site" className="text-ink-2">{l.site_name ?? '—'}</td>117                      <td data-label="Owners" className="mono text-xs">{l.owner_codes?.join(', ') || '—'}</td>118                      <td data-label="Debris on orbit" className="num tnum font-medium">{fmtInt(l.debris_on_orbit)}</td>119                    </tr>120                  ))}121                </tbody>122              </table>123            )}124            <Note className="mt-3">Fragments are attributed to the COSPAR launch of their parent object. Owner codes are SATCAT designators (e.g. PRC, CIS, US).</Note>125          </Section>126127          <Section eyebrow="History" title="Debris growth and decay">128            <TwoCol>129              <ChartBlock title="Debris catalogued by launch year" hint="Still on orbit vs decayed">130                <StackedBars data={growth} keys={['still_on_orbit', 'decayed']} labels={{ still_on_orbit: 'Still on orbit', decayed: 'Decayed' }} title="Debris catalogued by launch year of the parent object: still on orbit vs decayed" height={220} xTicks={7} />131                <Note className="mt-2">Decayed = catalogued − still on orbit (derived from the two published series). Year = launch year of the parent object, not the fragmentation date.</Note>132              </ChartBlock>133              <ChartBlock title="Decays per year by object type">134                <StackedBars data={decays} keys={['payloads', 'debris', 'rocket_bodies']} labels={{ payloads: 'Payloads', debris: 'Debris', rocket_bodies: 'Rocket bodies' }} title="Objects decayed per year by type" height={220} xTicks={7} />135                <Note className="mt-2">136                  Published SATCAT decay dates. Recent reentries are listed on <Link href={routes.reentries()} className="text-accent hover:underline">/reentries</Link>.137                </Note>138              </ChartBlock>139            </TwoCol>140          </Section>141142          <Section eyebrow="Largest" title="Largest tracked debris and rocket bodies" action={{ href: routes.satellites('object_type=DEBRIS,ROCKET_BODY&on_orbit=true'), label: 'Browse all' }}>143            {largest.length === 0 ? (144              <Unavailable what="Largest objects" />145            ) : (146              <table className="data-table stack text-sm">147                <thead>148                  <tr>149                    <th>Object</th>150                    <th>Type</th>151                    <th className="num">RCS (m²)</th>152                    <th className="num">Perigee</th>153                    <th className="num">Apogee</th>154                    <th>Launched</th>155                    <th>Country</th>156                  </tr>157                </thead>158                <tbody>159                  {largest.map((o, i) => (160                    <tr key={o.id}>161                      <td className="primary">162                        <InlineRank n={i + 1} />163                        <Link href={routes.satellite(o.slug)} className="link font-medium">{o.name}</Link>164                        {o.norad_id && <span className="mono ml-2 text-xs text-ink-3">#{o.norad_id}</span>}165                      </td>166                      <td data-label="Type"><TypeBadge type={o.object_type} /></td>167                      <td data-label="RCS (m²)" className="num tnum">{fmt2(o.rcs_m2)}</td>168                      <td data-label="Perigee" className="num tnum">{fmtKm(o.perigee_km)}</td>169                      <td data-label="Apogee" className="num tnum">{fmtKm(o.apogee_km)}</td>170                      <td data-label="Launched" className="mono text-xs">{fmtDate(o.launch_date)}</td>171                      <td data-label="Country" className="mono text-xs">{o.country_code ?? '—'}</td>172                    </tr>173                  ))}174                </tbody>175              </table>176            )}177            <Note className="mt-3">RCS = radar cross-section as published in the SATCAT (m²); it is a size proxy, not mass. {d.disclaimer}</Note>178          </Section>179        </>180      )}181    </Container>182  );183}184