Add anonymised world map of active eggs
Server geolocates each egg by IP on first contact (ip-api.com), snaps coordinates to the nearest 1-degree grid (~111 km) and adds a small deterministic per-egg jitter so eggs in the same cell don't overlap. Only country + rounded lat/lon are stored; exact IPs are never saved. Front-end uses Leaflet with CartoDB dark tiles. Markers update on every poll cycle and are coloured to match the current coherence index. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Global Consciousness Project — Live Dot</title>
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||
<style>
|
||||
:root {
|
||||
--dot-color: #00cc44;
|
||||
@@ -107,6 +108,41 @@
|
||||
margin: .5rem 0 2rem;
|
||||
}
|
||||
|
||||
/* ── map ── */
|
||||
#map-wrap {
|
||||
width: min(480px, 96vw);
|
||||
background: #0e0e1c;
|
||||
border: 1px solid #18182a;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
#map-title {
|
||||
font-size: .65rem;
|
||||
letter-spacing: .14em;
|
||||
text-transform: uppercase;
|
||||
color: #303050;
|
||||
padding: 1rem 1.6rem .6rem;
|
||||
}
|
||||
#map {
|
||||
height: 240px;
|
||||
width: 100%;
|
||||
background: #080812;
|
||||
}
|
||||
/* Override Leaflet chrome for dark theme */
|
||||
.leaflet-container { background: #080812; }
|
||||
.leaflet-control-attribution { display: none; }
|
||||
.leaflet-tooltip {
|
||||
background: #10101e;
|
||||
border: 1px solid #202038;
|
||||
color: #9090b8;
|
||||
font-size: .75rem;
|
||||
padding: .3rem .6rem;
|
||||
border-radius: 6px;
|
||||
box-shadow: none;
|
||||
}
|
||||
.leaflet-tooltip-top::before { border-top-color: #202038; }
|
||||
|
||||
/* ── legend ── */
|
||||
.card {
|
||||
width: min(480px, 96vw);
|
||||
@@ -228,6 +264,12 @@
|
||||
|
||||
<canvas id="chart"></canvas>
|
||||
|
||||
<!-- map -->
|
||||
<div id="map-wrap">
|
||||
<p id="map-title">Active eggs — anonymised to ~111 km</p>
|
||||
<div id="map"></div>
|
||||
</div>
|
||||
|
||||
<!-- legend -->
|
||||
<div class="card">
|
||||
<p class="card-title">What the color means</p>
|
||||
@@ -278,6 +320,7 @@
|
||||
<p id="tick"></p>
|
||||
<div id="toast">Copied!</div>
|
||||
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
<script>
|
||||
// ── color mapping ──────────────────────────────────────────────────────
|
||||
function palette(index) {
|
||||
@@ -392,6 +435,7 @@
|
||||
try {
|
||||
const [status, history] = await Promise.all([fetchStatus(), fetchHistory()]);
|
||||
|
||||
currentIndex = status.index;
|
||||
applyColor(status.index);
|
||||
document.getElementById('status-label').innerHTML = label(status.index, status.n_eggs);
|
||||
document.getElementById('s-index').textContent = Math.round(status.index) + '%';
|
||||
@@ -399,11 +443,56 @@
|
||||
document.getElementById('s-age').textContent = ageStr(status.timestamp);
|
||||
|
||||
drawChart(history.slice().reverse());
|
||||
updateMap();
|
||||
} catch (e) {
|
||||
document.getElementById('status-label').textContent = 'Connection error — retrying…';
|
||||
}
|
||||
}
|
||||
|
||||
// ── map ───────────────────────────────────────────────────────────────
|
||||
const leafletMap = L.map('map', {
|
||||
center: [20, 10],
|
||||
zoom: 1,
|
||||
zoomControl: false,
|
||||
scrollWheelZoom: false,
|
||||
attributionControl: false,
|
||||
});
|
||||
|
||||
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}{r}.png', {
|
||||
subdomains: 'abcd',
|
||||
maxZoom: 6,
|
||||
}).addTo(leafletMap);
|
||||
|
||||
let eggMarkers = [];
|
||||
let currentIndex = 50;
|
||||
|
||||
async function updateMap() {
|
||||
try {
|
||||
const r = await fetch('/api/map');
|
||||
const eggs = await r.json();
|
||||
eggMarkers.forEach(m => m.remove());
|
||||
eggMarkers = [];
|
||||
|
||||
const [color] = palette(currentIndex);
|
||||
|
||||
eggs.forEach(egg => {
|
||||
const m = L.circleMarker([egg.lat, egg.lon], {
|
||||
radius: 7,
|
||||
fillColor: color,
|
||||
color: color,
|
||||
fillOpacity: 0.75,
|
||||
weight: 0,
|
||||
}).addTo(leafletMap);
|
||||
m.bindTooltip(egg.country || 'Unknown', { direction: 'top', offset: [0, -4] });
|
||||
eggMarkers.push(m);
|
||||
});
|
||||
|
||||
// Update egg count in map title
|
||||
document.getElementById('map-title').textContent =
|
||||
`Active eggs (${eggs.length}) — anonymised to ~111 km`;
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
// ── embed code ────────────────────────────────────────────────────────
|
||||
const origin = window.location.origin;
|
||||
document.querySelectorAll('.origin-span').forEach(el => el.textContent = origin);
|
||||
|
||||
Reference in New Issue
Block a user