Skip to content

Commit e6cd537

Browse files
committed
Update prefix-lookup.astro
1 parent a197f63 commit e6cd537

1 file changed

Lines changed: 64 additions & 1 deletion

File tree

src/pages/prefix-lookup.astro

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,16 +91,39 @@ const breadcrumb = {
9191
.lookup-note { margin-top: 2rem; font-size: .9rem; }
9292
.lk-spin { display:inline-block; width:14px; height:14px; border:2px solid rgba(79,140,255,.3); border-top-color: var(--accent); border-radius:50%; animation: lkspin .7s linear infinite; vertical-align:-2px; margin-right:.5rem; }
9393
@keyframes lkspin { to { transform: rotate(360deg); } }
94+
.lkm-wrap { border:1px solid var(--line); border-radius:12px; overflow:hidden; background:#070d1c; min-height:90px; }
95+
.lkm-svg { display:block; width:100%; height:auto; }
96+
.lkm-ocean { fill:#070d1c; }
97+
.lkm-land { fill: rgba(255,255,255,.07); stroke: rgba(255,255,255,.14); stroke-width:.5; }
98+
.lkm-cur { fill: rgba(45,212,191,.16); stroke: rgba(45,212,191,.55); stroke-width:.9; }
99+
.lkm-dot { fill: var(--accent); }
100+
.lkm-ring { fill:none; stroke: var(--accent); stroke-width:1.5; }
101+
.lkm-pulse { fill: var(--accent); opacity:.5; transform-box: fill-box; transform-origin:center; animation: lkmpulse 1.8s ease-out infinite; }
102+
@keyframes lkmpulse { 0%{ transform:scale(.5); opacity:.55; } 70%{ transform:scale(2.8); opacity:0; } 100%{ opacity:0; } }
103+
.lkm-cap { margin-top:.6rem; font-size:.85rem; }
104+
.lkm-fail { padding:1rem; }
94105
</style>
95106

96107
<script is:inline type="module">
97108
import { parquetReadObjects } from 'https://esm.sh/hyparquet@1';
98109
import { compressors } from 'https://esm.sh/hyparquet-compressors@1';
110+
import { geoMercator, geoPath, geoContains } from 'https://esm.sh/d3-geo@3';
111+
import { feature as topoFeature } from 'https://esm.sh/topojson-client@3';
99112

100113
const cfg = document.getElementById('lookupConfig').dataset;
101114
const RELEASE = cfg.release;
102115
const BASE = cfg.base;
103116

117+
// Natural Earth country outlines (public domain) — for the location pin map. Lazy-loaded once.
118+
const WORLD_URL = 'https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json';
119+
let WORLD = null;
120+
async function ensureWorld() {
121+
if (WORLD) return WORLD;
122+
const tj = await fetch(WORLD_URL).then((r) => r.json());
123+
WORLD = topoFeature(tj, tj.objects.countries).features;
124+
return WORLD;
125+
}
126+
104127
const form = document.getElementById('lookupForm');
105128
const input = document.getElementById('lookupInput');
106129
const statusEl = document.getElementById('lookupStatus');
@@ -169,9 +192,48 @@ const breadcrumb = {
169192

170193
const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));
171194

195+
// IPInfo-style location map: auto-zoom to the block's country (point-in-polygon, no code tables),
196+
// pulsing pin at the exact /24 coordinate. No tiles, no API — Natural Earth outline only.
197+
async function renderMap(row) {
198+
const mount = document.getElementById('lkmMount');
199+
const cap = document.getElementById('lkmCap');
200+
if (!mount) return;
201+
const lat = Number(val(row.Latitude));
202+
const lon = Number(val(row.Longitude));
203+
if (!isFinite(lat) || !isFinite(lon)) return;
204+
let feats;
205+
try { feats = await ensureWorld(); }
206+
catch { mount.innerHTML = '<div class="lkm-fail muted">Map unavailable right now.</div>'; return; }
207+
const W = 860, H = 340, PAD = 18;
208+
let country = null;
209+
for (const f of feats) { if (geoContains(f, [lon, lat])) { country = f; break; } }
210+
const proj = geoMercator();
211+
if (country) {
212+
proj.fitExtent([[PAD, PAD], [W - PAD, H - PAD]], country);
213+
if (proj.scale() > 2400) proj.scale(2400).center([lon, lat]).translate([W / 2, H / 2]);
214+
} else {
215+
proj.center([lon, lat]).scale(820).translate([W / 2, H / 2]);
216+
}
217+
proj.clipExtent([[0, 0], [W, H]]); // clip to viewport — keeps off-screen path data tiny
218+
const path = geoPath(proj);
219+
const land = feats.map((f) => { const d = path(f); return d ? `<path d="${d}" class="lkm-land${f === country ? ' lkm-cur' : ''}"/>` : ''; }).join('');
220+
const p = proj([lon, lat]);
221+
const px = p ? p[0] : W / 2, py = p ? p[1] : H / 2;
222+
mount.innerHTML =
223+
`<svg viewBox="0 0 ${W} ${H}" class="lkm-svg" role="img" aria-label="Approximate location of the block">` +
224+
`<rect width="${W}" height="${H}" class="lkm-ocean"/><g>${land}</g>` +
225+
`<g class="lkm-pin" transform="translate(${px.toFixed(1)},${py.toFixed(1)})">` +
226+
`<circle class="lkm-pulse" r="7"/><circle class="lkm-ring" r="7"/><circle class="lkm-dot" r="3.4"/></g></svg>`;
227+
const parts = [val(row.CityName), val(row.RegionName), val(row.CountryCode)].filter(Boolean);
228+
cap.textContent = (parts.length ? parts.join(', ') + ' · ' : '') + `${lat.toFixed(4)}, ${lon.toFixed(4)}`;
229+
}
230+
172231
function render(prefix, row) {
232+
const _lat = Number(val(row.Latitude)), _lon = Number(val(row.Longitude));
233+
const hasCoord = isFinite(_lat) && isFinite(_lon);
173234
const head = `<div class="lk-card"><div class="lk-head">${esc(prefix)}</div>
174235
<div class="muted" style="margin-top:.2rem">Snapshot ${esc(val(row.snapshot_month) || RELEASE)}</div></div>`;
236+
const mapCard = hasCoord ? `<div class="lk-card lkm-card"><h2>Approx. location</h2><div id="lkmMount" class="lkm-wrap"></div><div id="lkmCap" class="lkm-cap muted"></div></div>` : '';
175237
const cards = FIELDS.map(([title, fields]) => {
176238
const rows = fields.map(([key, label]) => {
177239
let v = val(row[key]);
@@ -188,8 +250,9 @@ const breadcrumb = {
188250
}).join('');
189251
return rows ? `<div class="lk-card"><h2>${title}</h2><div class="lk-grid">${rows}</div></div>` : '';
190252
}).join('');
191-
resultEl.innerHTML = head + cards;
253+
resultEl.innerHTML = head + mapCard + cards;
192254
resultEl.hidden = false;
255+
if (hasCoord) renderMap(row);
193256
}
194257

195258
async function doLookup(e) {

0 commit comments

Comments
 (0)