mikonvergence's picture
Sync from GitHub 6f2bc3d
bc66791 verified
Raw History Blame Contribute Delete
18.2 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Major TOM Raster Viewer</title>
<link rel="icon" href="mt-icon.svg">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=League+Spartan:wght@500&family=Source+Sans+3:wght@400;600;700&display=swap">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/maplibre-gl@6.14.0/dist/maplibre-gl.css">
<style>
:root {
/* Major TOM logo colours */
--mt-blue: #0071bc; --mt-navy: #2e3192; --mt-green: #39b54a; --mt-orange: #f15a24;
/* light interface, as Hugging Face in light mode */
--page: #f9fafb; --surface: #ffffff; --ink: #111827; --ink-2: #4b5563; --ink-3: #6b7280;
--line: #e5e7eb; --tint: #eff6fc; --globe-space: #f3f4f6;
--sans: 'Source Sans 3', system-ui, sans-serif; --mono: 'IBM Plex Mono', ui-monospace, monospace;
}
* { box-sizing: border-box; }
html, body { height: 100%; margin: 0; }
body { background: var(--page); color: var(--ink); font: 15px/1.45 var(--sans); display: flex; flex-direction: column; }
header { background: var(--surface); border-bottom: 1px solid var(--line); position: relative; }
.brand-strip { display: flex; height: 3px; }
.brand-strip span { flex: 1; }
.bar { display: flex; align-items: center; gap: 12px; padding: 10px 20px; }
.bar img { width: 30px; height: 26px; }
.crumbs { font-family: var(--mono); font-size: 15px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.crumbs .org { color: var(--ink-3); }
.crumbs b { font-weight: 500; }
.pill { font-size: 12px; color: var(--ink-2); border: 1px solid var(--line); border-radius: 999px; padding: 1px 9px; white-space: nowrap; }
#loader { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 3; display: flex;
flex-direction: column; align-items: center; gap: 6px; min-width: 220px; padding: 18px 22px 14px;
background: rgba(255,255,255,.94); border: 1px solid var(--line); border-radius: 12px; box-shadow: 0 4px 16px rgba(17,24,39,.08); }
#loader[hidden] { display: none; }
#loader svg { width: 64px; height: 56px; margin-bottom: 4px; }
#loader polygon { fill: var(--line); transition: fill .12s; }
#loader polygon.on { fill: var(--on); }
#loader-phase { font-size: 11px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: var(--ink-3); }
#loader-text { font-size: 14px; color: var(--ink); text-align: center; }
#loader-bytes { font-family: var(--mono); font-size: 12px; color: var(--ink-3); min-height: 1em; }
.bar nav { margin-left: auto; display: flex; gap: 16px; font-size: 14px; }
.bar nav a { color: var(--ink-2); text-decoration: none; white-space: nowrap; }
.bar nav a:hover { color: var(--mt-blue); }
main { flex: 1; display: flex; min-height: 0; }
#map-wrap { flex: 1; position: relative; background: var(--globe-space); }
#map { position: absolute; inset: 0; }
.places { position: absolute; inset: 0; pointer-events: none; overflow: hidden; z-index: 1; }
.place { position: absolute; left: 0; top: 0; margin: -7px 0 0 -4px; display: flex; align-items: center; gap: 4px;
font: 600 12px/14px var(--sans); color: var(--ink); white-space: nowrap;
text-shadow: 0 0 2px #fff, 0 0 3px #fff, 0 0 5px rgba(255,255,255,.9); }
.place i { font-style: normal; font-size: 8px; color: var(--ink-2); }
.place.capital span { font-weight: 700; }
.place.peak i, .place.low i, .place.pass i { color: #6b4f2a; }
.place.peak span, .place.low span, .place.pass span, .place.falls span { font-weight: 400; font-style: italic; }
.toggle { display: flex; gap: 8px; align-items: center; font-size: 14px; cursor: pointer; }
.toggle input { accent-color: var(--mt-blue); }
#status:not(:empty) { position: absolute; top: 14px; left: 14px; z-index: 2; background: var(--surface);
border: 1px solid var(--line); border-radius: 8px; padding: 4px 10px; font-size: 13px; color: var(--ink-2); }
aside { width: 344px; flex: none; background: var(--surface); border-left: 1px solid var(--line); overflow-y: auto; }
section { padding: 14px 18px 16px; border-bottom: 1px solid var(--line); }
h2 { font-size: 13px; font-weight: 600; color: var(--ink-3); margin: 0 0 10px; }
.option { display: flex; gap: 10px; align-items: flex-start; padding: 8px 10px; border: 1px solid var(--line);
border-radius: 8px; margin-bottom: 6px; cursor: pointer; }
.option:has(input:checked) { border-color: var(--mt-blue); background: var(--tint); }
.option input { margin: 4px 0 0; accent-color: var(--mt-blue); }
.option b { display: block; font-weight: 600; }
.option small { display: block; color: var(--ink-3); font-family: var(--mono); font-size: 12px; }
.note { font-size: 13px; color: var(--ink-3); margin: 0; }
.pick-head { display: flex; justify-content: space-between; align-items: baseline; }
.pick-head h2 { margin-bottom: 4px; }
.link { font: inherit; font-size: 13px; color: var(--mt-blue); background: none; border: 0; padding: 0; cursor: pointer; }
.pick-name { display: flex; justify-content: space-between; gap: 8px; font-family: var(--mono); font-size: 13px; margin-bottom: 10px; }
.pick-name b { font-weight: 500; }
.pick-name span { color: var(--ink-3); }
#thumb-wrap { position: relative; aspect-ratio: 1; background: var(--globe-space); border-radius: 8px; overflow: hidden;
margin-bottom: 10px; display: flex; align-items: center; justify-content: center; color: var(--ink-3); font-size: 13px; }
#thumb { width: 100%; height: 100%; object-fit: contain; display: block; }
#thumb[hidden] { display: none; }
#thumb { cursor: zoom-in; }
#thumb-badge { position: absolute; left: 8px; bottom: 8px; display: flex; align-items: center; gap: 7px; padding: 4px 10px 4px 8px;
border-radius: 999px; background: rgba(17, 24, 39, .78); color: #fff; font: 12px var(--sans); pointer-events: none; }
#thumb-badge[hidden] { display: none; }
#thumb-badge .spin { width: 11px; height: 11px; border-radius: 50%; border: 2px solid rgba(255, 255, 255, .35);
border-top-color: #fff; animation: spin .8s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
.image-actions { display: flex; flex-wrap: wrap; gap: 8px 12px; align-items: center; margin: -2px 0 10px; font-size: 13px; }
.image-actions button { font: inherit; padding: 4px 10px; border: 1px solid var(--line); border-radius: 8px;
background: var(--surface); color: var(--ink); cursor: pointer; }
.image-actions button:hover { border-color: var(--mt-blue); color: var(--mt-blue); }
.image-actions button:disabled { opacity: .45; cursor: not-allowed; border-color: var(--line); color: var(--ink); }
.image-actions label { display: flex; align-items: center; gap: 6px; color: var(--ink-2); cursor: pointer; }
.image-actions input[type=checkbox], .image-actions input[type=range] { accent-color: var(--mt-blue); }
.image-actions input[type=range] { width: 90px; }
.image-actions[hidden] { display: none; }
.view-row { display: flex; align-items: center; gap: 8px; margin: 0 0 8px; font-size: 13px; color: var(--ink-2); }
.view-row select { flex: 1; padding: 5px 8px; border: 1px solid var(--line); border-radius: 8px; background: var(--surface);
color: var(--ink); font: 13px var(--sans); }
.view-row[hidden] { display: none; }
#lightbox { position: fixed; inset: 0; z-index: 10; background: rgba(17,24,39,.88); overflow: hidden; cursor: grab;
touch-action: none; }
#lightbox[hidden] { display: none; }
#lightbox.dragging { cursor: grabbing; }
#lightbox img { position: absolute; left: 0; top: 0; transform-origin: 0 0; image-rendering: pixelated; user-select: none;
-webkit-user-drag: none; }
#lightbox-bar { position: absolute; left: 0; right: 0; top: 0; display: flex; justify-content: space-between; gap: 12px;
align-items: center; padding: 10px 16px; color: #fff; font: 13px var(--mono); background: rgba(17,24,39,.6); }
#lightbox-bar button { font: 13px var(--sans); padding: 4px 12px; border: 1px solid rgba(255,255,255,.4); border-radius: 8px;
background: transparent; color: #fff; cursor: pointer; }
#choices { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 8px; }
#choices:empty { display: none; }
#choices button { font: 12px var(--mono); padding: 3px 8px; border: 1px solid var(--line); border-radius: 999px;
background: var(--surface); color: var(--ink-2); cursor: pointer; }
#choices button[aria-pressed=true] { border-color: var(--mt-blue); color: var(--mt-blue); background: var(--tint); }
#weather { margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--line); }
#weather h3 { margin: 0 0 2px; font-size: 15px; font-weight: 600; }
.wx-head { font-size: 11px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: var(--ink-3); margin: 14px 0 4px; }
.wx-row { margin: 8px 0 0; }
.wx-top { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; }
.wx-label { font-size: 14px; color: var(--ink); }
.wx-label small { font-size: 12px; color: var(--ink-3); margin-left: 2px; }
.wx-value { font: 500 14px var(--mono); white-space: nowrap; }
.wx-value small { font-size: 11px; color: var(--ink-3); font-weight: 400; }
.wx-chart { display: block; width: 100%; height: auto; margin-top: 2px; overflow: visible; }
.wx-line { fill: none; stroke: var(--mt-blue); stroke-width: 1.6; stroke-linejoin: round; }
.wx-area { fill: var(--mt-blue); opacity: .08; }
.wx-bars rect { fill: var(--mt-blue); }
.wx-dots circle { fill: var(--mt-blue); }
.wx-base { stroke: var(--line); }
.wx-zero { stroke: var(--mt-orange); stroke-width: 1; stroke-dasharray: 2 2; }
.wx-now { fill: var(--mt-orange); stroke: var(--surface); stroke-width: 1.5; }
.wx-tick { font: 10px var(--mono); fill: var(--ink-3); text-anchor: end; }
.wx-none { font: 11px var(--sans); fill: var(--ink-3); text-anchor: middle; }
.wx-axis { display: flex; justify-content: space-between; font: 10px var(--mono); color: var(--ink-3); margin: 4px 40px 0 0; padding-top: 3px; border-top: 1px solid var(--line); }
.tiny { font: 11px/1.4 var(--mono); color: var(--ink-3); min-height: 1em; margin-top: 8px; }
dl { display: grid; grid-template-columns: auto 1fr; gap: 3px 14px; margin: 0; font-size: 14px; }
dt { color: var(--ink-3); }
dd { margin: 0; font-family: var(--mono); font-size: 13px; text-align: right; overflow-wrap: anywhere; }
dd a { color: var(--mt-blue); }
.source { font-size: 12px; color: var(--ink-3); padding: 12px 18px 18px; }
.credit { display: flex; flex-direction: column; gap: 8px; font-size: 12px; color: var(--ink-3); padding: 14px 18px 20px;
border-top: 1px solid var(--line); }
.credit p { margin: 0; }
.credit a { color: var(--ink-2); }
#asterisk { position: absolute; left: 14px; bottom: 14px; z-index: 2; display: flex; align-items: center; gap: 6px;
font: 500 17px/1 'League Spartan', sans-serif; color: #000; text-decoration: none; }
#asterisk img { height: 24px; } /* text at 0.7 x the mark, as on asterisk.coop */
#asterisk .by { font: 12px var(--sans); color: var(--ink-3); margin-right: 2px; }
#asterisk:hover { color: #492ae8; }
.source a { color: var(--mt-blue); }
.maplibregl-ctrl-attrib { font-family: var(--sans); }
@media (max-width: 820px) {
main { flex-direction: column; overflow-y: auto; }
#map-wrap { flex: none; height: 62vh; }
aside { width: 100%; border-left: 0; border-top: 1px solid var(--line); overflow: visible; }
.bar { padding: 10px 16px; }
.bar nav, .pill { display: none; }
}
</style>
</head>
<body>
<header>
<div class="brand-strip" aria-hidden="true">
<span style="background:var(--mt-blue)"></span><span style="background:var(--mt-navy)"></span>
<span style="background:var(--mt-green)"></span><span style="background:var(--mt-orange)"></span>
</div>
<div class="bar">
<img src="mt-icon.svg" alt="">
<div class="crumbs"><span class="org">Major-TOM /</span> <b>Raster Viewer</b></div>
<span class="pill">serverless</span>
<nav>
<a href="https://huggingface.co/Major-TOM">Datasets</a>
<a href="https://huggingface.co/spaces/Major-TOM/MajorTOM-Core-Viewer">Core Viewer</a>
<a href="https://github.com/ESA-PhiLab/Major-TOM">Code</a>
</nav>
</div>
</header>
<main>
<div id="map-wrap">
<div id="map"></div>
<div id="places" class="places" aria-hidden="true"></div>
<a id="asterisk" href="https://asterisk.coop" target="_blank" rel="noopener">
<span class="by">made by</span><img src="asterisk-mark.svg" alt="">asterisk labs</a>
<div id="status"></div>
<div id="loader" role="status" aria-live="polite" hidden>
<!-- the Major TOM logo mark (mt-icon.svg): its four panels light up in turn while data loads -->
<svg viewBox="0 0 345 296" aria-hidden="true">
<polygon style="--on:var(--mt-navy)" points="0 37 75 0 75 258 0 296"/>
<polygon style="--on:var(--mt-blue)" points="75 0 172 37 172 296 75 258"/>
<polygon style="--on:var(--mt-green)" points="172 37 269 0 269 258 172 296"/>
<polygon style="--on:var(--mt-orange)" points="269 0 345 37 345 296 269 258"/>
</svg>
<div id="loader-phase"></div>
<div id="loader-text"></div>
<div id="loader-bytes"></div>
</div>
</div>
<aside>
<section>
<h2>Dataset</h2>
<div id="datasets"></div>
<p class="note">Runs in your browser: reads the Core Parquet files on Hugging Face directly, no server. Click a
cell with a sample to load its image.</p>
</section>
<section id="pick" hidden>
<div class="pick-head"><h2>Selected cell</h2><button id="pick-clear" class="link">Clear</button></div>
<div class="pick-name"><b id="pick-cell"></b><span id="pick-pos"></span></div>
<div id="choices"></div>
<label class="view-row" id="view-row" hidden>View <select id="view" aria-label="Band or composite"></select></label>
<div id="thumb-wrap"><img id="thumb" alt="Thumbnail of the selected sample" hidden><span id="thumb-note"></span><div id="thumb-badge" hidden><span class="spin"></span><span id="thumb-badge-text"></span></div></div>
<div class="image-actions" id="image-actions" hidden>
<button id="open-image" title="The image in the full window: zoom, pan, value under the cursor">Enlarge</button>
</div>
<div class="image-actions" id="download-actions" hidden>
<button id="dl-band" title="The shown band as stored: a GeoTIFF">Download band (.tif)</button>
<button id="dl-all" title="Every band as stored, the thumbnail and metadata.json">All bands (.zip)</button>
<button id="dl-python" title="Python that reads this sample from Hugging Face">Copy Python</button>
</div>
<dl id="pick-meta"></dl>
<div class="tiny" id="pick-stats"></div>
<div id="weather" hidden>
<h3>Weather at acquisition</h3>
<p class="note" id="wx-from" hidden></p>
<div class="wx-head">Hourly · 24 hours either side</div><div id="wx-hourly"></div>
<div class="wx-head">Daily · 30 days either side</div><div id="wx-daily"></div>
<div class="tiny" id="wx-stats"></div>
<p class="note">Means over the 10 km cell, from Core-Weather on source.coop. Orange: the acquisition.</p>
</div>
</section>
<section>
<h2>Map</h2>
<label class="toggle"><input type="checkbox" id="show-places" checked> Place names (cities, peaks) from zoom 5</label>
</section>
<section>
<h2>Cursor</h2>
<dl>
<dt>Cell</dt><dd id="cursor-cell">–</dd>
<dt>Latitude, longitude</dt><dd id="cursor-pos">–</dd>
<dt>Sample</dt><dd id="cursor-value">–</dd>
</dl>
</section>
<section>
<h2>Last load</h2>
<dl>
<dt>Footprints</dt><dd id="stat-footprints">–</dd>
<dt>Samples</dt><dd id="stat-samples">–</dd>
<dt>Draw on globe</dt><dd id="stat-draw">–</dd>
</dl>
</section>
<div class="source">Reads <a id="dataset-link" href="https://huggingface.co/Major-TOM">huggingface.co/Major-TOM</a>
· footprints from each dataset's metadata.parquet</div>
<div class="credit">
<a href="https://asterisk.coop/project-pages/earth-compress.html" target="_blank" rel="noopener">
<img src="earth-compress.svg" alt="Earth Compress" height="28"></a>
<p>Developed by <a href="https://github.com/mikonvergence" target="_blank" rel="noopener">Mikolaj Czerkawski</a>,
<a href="https://asterisk.coop" target="_blank" rel="noopener">Asterisk Labs</a>, as part of Earth Compress, an
ARIA-funded project jointly led with the National Oceanography Centre.</p>
</div>
</aside>
</main>
<div id="lightbox" hidden role="dialog" aria-label="Selected image">
<img id="lightbox-img" alt="">
<div id="lightbox-bar"><span><span id="lightbox-title"></span> <span id="lightbox-value"></span></span><span>scroll to zoom · drag to pan · Esc to close
<button id="lightbox-close">Close</button></span></div>
</div>
<script>
// A private Space is shown in huggingface.co's frame through a signed link (?__sign=...), and only requests that
// carry the signature are let through there: modules imported by other modules would not. So every file of the app
// is asked for with it (an import map, then app.js). With no signature (public Space, local) nothing changes.
{
const sign = location.search, files = ['app.js', 'footprints.js', 'samples.js', 'bands.js', 'download.js', 'parquet.js', 'tiles.js', 'labels.js']
const url = file => new URL(file, location.href).href.split('?')[0]
const map = document.createElement('script')
map.type = 'importmap'
map.textContent = JSON.stringify({ imports: Object.fromEntries(files.map(f => [url(f), url(f) + sign])) })
document.head.append(map)
const app = document.createElement('script')
app.type = 'module'
app.src = 'app.js' + sign
document.body.append(app)
}
</script>
</body>
</html>