natural disasters

DISASTER MAP

Live earthquakes, storms, volcanoes, wildfires & floods worldwide. Data from NASA EONET and USGS — updated in real-time, no signup needed.

Developer Reference

Core Algorithm & Standalone Script

Standalone, zero-dependency JavaScript implementation powering this tool. Free to inspect, copy, and build upon.

(function () {
    /* ── CONFIG ────────────────────────────────────────── */
    const EONET_URL = 'https://eonet.gsfc.nasa.gov/api/v3/events?status=all&days=30&limit=200';
    const USGS_URL  = 'https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/4.5_week.geojson';

    const CAT_META = {
      earthquakes:  { label: 'Earthquake',   color: '#ff0000', icon: '≋', bg: 'rgba(255,0,0,0.15)' },
      severeStorms: { label: 'Severe Storm', color: '#f5c518', icon: '⌁', bg: 'rgba(245,197,24,0.15)' },
      volcanoes:    { label: 'Volcano',      color: '#9d00ff', icon: '△', bg: 'rgba(157,0,255,0.15)' },
      wildfires:    { label: 'Wildfire',     color: '#ff8000', icon: '⬡', bg: 'rgba(255,128,0,0.15)' },
      floods:       { label: 'Flood',        color: '#4488ff', icon: '≈', bg: 'rgba(68,136,255,0.15)' },
      landslides:   { label: 'Landslide',    color: '#aa8855', icon: '◿', bg: 'rgba(170,136,85,0.15)' },
      seaLakeIce:   { label: 'Sea/Lake Ice', color: '#aaddff', icon: '❄', bg: 'rgba(170,221,255,0.15)' },
      drought:      { label: 'Drought',      color: '#cc9944', icon: '○', bg: 'rgba(204,153,68,0.15)' },
      dustHaze:     { label: 'Dust / Haze',  color: '#bbaa88', icon: '≀', bg: 'rgba(187,170,136,0.15)' },
      tempExtremes: { label: 'Temp Extreme', color: '#ff8855', icon: '◈', bg: 'rgba(255,136,85,0.15)' },
      other:        { label: 'Other',        color: '#888888', icon: '⬤', bg: 'rgba(136,136,136,0.15)' },
    };

    function getCatMeta(catId) {
      return CAT_META[catId] || CAT_META.other;
    }

    /* ── MAP INIT ──────────────────────────────────────── */
    const map = L.map('map', {
      center: [20, 0],
      zoom: 2,
      zoomControl: true,
      attributionControl: true,
      scrollWheelZoom: false,
    });

    L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
      attribution: '© <a href="https://openstreetmap.org/copyright">OpenStreetMap</a>',
      maxZoom: 18,
    }).addTo(map);

    let allMarkers = [];
    let allEvents  = [];
    let activeFilter = 'all';

    /* ── NORMALIZE EVENTS ──────────────────────────────── */
    function geomToLatLng(geom) {
      if (!geom || !geom.coordinates) return [null, null];
      if (geom.type === 'Point') {
        return [geom.coordinates[1], geom.coordinates[0]]; // [lat, lng]
      }
      if (geom.type === 'Polygon') {
        // centroid of exterior ring
        const ring = geom.coordinates[0] || [];
        if (!ring.length) return [null, null];
        const lat = ring.reduce((s, p) => s + p[1], 0) / ring.length;
        const lng = ring.reduce((s, p) => s + p[0], 0) / ring.length;
        return [lat, lng];
      }
      if (geom.type === 'MultiPolygon') {
        const ring = geom.coordinates?.[0]?.[0] || [];
        if (!ring.length) return [null, null];
        const lat = ring.reduce((s, p) => s + p[1], 0) / ring.length;
        const lng = ring.reduce((s, p) => s + p[0], 0) / ring.length;
        return [lat, lng];
      }
      return [null, null];
    }

    function normalizeEONET(data) {
      const out = [];
      for (const ev of data.events || []) {
        const catId = ev.categories?.[0]?.id || 'other';
        const geoms = ev.geometry || ev.geometries || [];
        if (!geoms.length) continue;
        // prefer latest Point, fall back to any geometry
        const geom = geoms.slice().reverse().find(g => g.type === 'Point')
                  || geoms[geoms.length - 1];
        const [lat, lng] = geomToLatLng(geom);
        // use date of the chosen geometry, fall back to first geometry date
        const date = new Date(geom?.date || geoms[0]?.date || Date.now());
        out.push({
          id:    ev.id,
          title: ev.title,
          catId,
          date,
          lat,
          lng,
          mag:   geom?.magnitudeValue ?? null,
          magUnit: geom?.magnitudeUnit ?? null,
          place: null,
          url:   ev.link || null,
          source: 'eonet',
        });
      }
      return out;
    }

    function normalizeUSGS(data) {
      return (data.features || []).map(f => {
        const p = f.properties;
        const [lng, lat] = f.geometry.coordinates;
        return {
          id:     f.id,
          title:  p.title || `M${p.mag} Earthquake`,
          catId:  'earthquakes',
          date:   new Date(p.time),
          lat,
          lng,
          mag:    p.mag,
          place:  p.place,
          url:    p.url || null,
          source: 'usgs',
        };
      });
    }

    /* ── RENDER MAP MARKERS ────────────────────────────── */
    const layerGroup = L.layerGroup().addTo(map);

    function renderMarkers(events) {
      layerGroup.clearLayers();
      allMarkers = [];

      for (const ev of events) {
        if (!isFinite(ev.lat) || !isFinite(ev.lng)) continue;
        const meta = getCatMeta(ev.catId);

        // radius scaled by magnitude for earthquakes
        let radius = 6;
        if (ev.mag !== null) {
          radius = Math.max(4, Math.min(20, ev.mag * 2.2));
        }

        const marker = L.circleMarker([ev.lat, ev.lng], {
          radius,
          color:       meta.color,
          fillColor:   meta.color,
          fillOpacity: 0.55,
          weight:      1.5,
          opacity:     0.9,
        });

        const dateStr = ev.date.toUTCString().replace(' GMT', ' UTC');
        const magLine = ev.mag !== null
          ? `<span style="color:${meta.color};font-size:14px;font-family:'Bebas Neue',sans-serif;letter-spacing:1px;">M ${ev.mag.toFixed(1)}</span><br>` : '';
        const placeLine = ev.place ? `<span style="color:#555;font-size:11px;">${ev.place}</span><br>` : '';
        const srcParam = ev.source === 'usgs' ? 'usgs' : 'eonet';
        const linkLine = `<a class="pop-link" href="/disasters/detail.html?id=${encodeURIComponent(ev.id)}&source=${srcParam}">details →</a>`;

        marker.bindPopup(`
          <div class="pop-type" style="color:${meta.color}">${meta.icon} ${meta.label}</div>
          <strong>${ev.title}</strong>
          ${magLine}${placeLine}
          <span class="pop-date">${dateStr}</span>
          ${linkLine}
        `, { maxWidth: 280 });

        marker._evData = ev;
        marker.addTo(layerGroup);
        allMarkers.push(marker);
      }
    }

    /* ── RENDER LIST ───────────────────────────────────── */
    function timeAgo(date) {
      const diff = Date.now() - date.getTime();
      const m = Math.floor(diff / 60000);
      if (m < 60) return `${m}m ago`;
      const h = Math.floor(m / 60);
      if (h < 24) return `${h}h ago`;
      const d = Math.floor(h / 24);
      return `${d}d ago`;
    }

    function renderList(events) {
      const list = document.getElementById('eventsList');
      const countEl = document.getElementById('eventsCount');

      countEl.textContent = `${events.length} events`;

      if (!events.length) {
        list.innerHTML = '<div class="events-empty">No events match the current filter.</div>';
        return;
      }

      const html = events.map(ev => {
        const meta = getCatMeta(ev.catId);
        const magClass = ev.mag >= 7 ? 'big' : ev.mag >= 5.5 ? 'med' : '';
        const magDisplay = ev.mag !== null
          ? `<div class="event-mag ${magClass}">${ev.catId === 'earthquakes' ? 'M' : ''}${Number(ev.mag).toFixed(1)}${ev.magUnit ? '<span style="font-size:11px;color:var(--muted);margin-left:2px;">' + ev.magUnit + '</span>' : ''}</div>`
          : '';
        return `
          <div class="event-item" data-id="${ev.id}" title="${ev.title}">
            <div class="event-icon" style="background:${meta.bg};border:1px solid ${meta.color}22;">
              <span style="color:${meta.color}">${meta.icon}</span>
            </div>
            <div class="event-body">
              <div class="event-title">${ev.title}</div>
              <div class="event-meta">
                <span class="event-type" style="color:${meta.color};border-color:${meta.color}44;background:${meta.bg};">${meta.label}</span>
                ${ev.place ? `<span class="event-place">${ev.place}</span>` : ''}
              </div>
            </div>
            <div class="event-right">
              <div class="event-date">${timeAgo(ev.date)}</div>
              ${magDisplay}
            </div>
          </div>
        `;
      }).join('');

      list.innerHTML = html;

      // Click to fly to on map
      list.querySelectorAll('.event-item').forEach(row => {
        row.addEventListener('click', () => {
          const id = row.dataset.id;
          const ev = allEvents.find(e => e.id === id);
          if (!ev) return;
          if (isFinite(ev.lat) && isFinite(ev.lng)) {
            map.flyTo([ev.lat, ev.lng], Math.max(5, map.getZoom()), { duration: 1.2 });
            const marker = allMarkers.find(m => m._evData?.id === id);
            if (marker) setTimeout(() => marker.openPopup(), 1300);
          }
          window.scrollTo({ top: 0, behavior: 'smooth' });
        });
      });
    }

    /* ── FILTER ────────────────────────────────────────── */
    const OTHER_CATS = ['landslides','seaLakeIce','drought','dustHaze','tempExtremes','manmade','waterColor','snow','fog','sand'];

    function getFilteredEvents() {
      if (activeFilter === 'all') return allEvents;
      if (activeFilter === 'other') return allEvents.filter(e => OTHER_CATS.includes(e.catId) || e.catId === 'other');
      return allEvents.filter(e => e.catId === activeFilter);
    }

    function applyFilter() {
      const filtered = getFilteredEvents();
      renderMarkers(filtered);
      renderList(filtered);
    }

    document.querySelectorAll('.filter-btn').forEach(btn => {
      btn.addEventListener('click', () => {
        document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
        btn.classList.add('active');
        activeFilter = btn.dataset.cat;
        applyFilter();
      });
    });

    /* ── STATS ─────────────────────────────────────────── */
    function updateStats(events) {
      const eqCount = events.filter(e => e.catId === 'earthquakes').length;
      document.getElementById('statTotal').textContent = events.length;
      document.getElementById('statEq').textContent = eqCount;
      document.getElementById('statUpdated').textContent =
        'Updated ' + new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
    }

    /* ── FETCH ─────────────────────────────────────────── */
    async function loadData() {
      const loadingEl = document.getElementById('mapLoading');

      try {
        const [eonetRes, usgsRes] = await Promise.allSettled([
          fetch(EONET_URL).then(r => r.json()),
          fetch(USGS_URL).then(r => r.json()),
        ]);

        const eonetEvents = eonetRes.status === 'fulfilled' ? normalizeEONET(eonetRes.value) : [];
        const usgsEvents  = usgsRes.status  === 'fulfilled' ? normalizeUSGS(usgsRes.value)   : [];

        // Merge: remove EONET earthquake duplicates (USGS is more detailed)
        const eonetNonEq = eonetEvents.filter(e => e.catId !== 'earthquakes');
        allEvents = [...eonetNonEq, ...usgsEvents]
          .sort((a, b) => b.date - a.date);

        loadingEl.classList.add('hidden');
        updateStats(allEvents);
        applyFilter();
      } catch (err) {
        loadingEl.innerHTML = '<span style="color:var(--error)">Failed to load events. Check your connection.</span>';
        console.error(err);
      }
    }

    loadData();
  })();
loading events
filter
Live
events
earthquakes (M4.5+, 7 days)
recent events — newest first
Loading events…
Sources: NASA EONET v3 USGS Earthquake Feed · Map tiles © OpenStreetMap contributors · Search history →