Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 11 additions & 8 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
USGS_FEED_URL=https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_day.geojson
CACHE_TTL_SECONDS=300
SQLITE_DB_PATH=data/earthquakes.db
DEFAULT_FEED_WINDOW=day
MODEL_PATH=models/earthquake_risk.joblib
ENABLE_BOOTSTRAP_DATA=true
ENABLE_MODEL_TRAINING_ON_STARTUP=false
ENABLE_BOOTSTRAP_DATA=false
ALLOW_BOOTSTRAP_AS_FRESH_CACHE=false
ENABLED_SOURCES=usgs_geojson,usgs_fdsn,emsc,isc,geofon,bmkg,geonet
SOURCE_TIMEOUT_SECONDS=15
SOURCE_MAX_RETRIES=2
SOURCE_REFRESH_TTL_SECONDS=300
SOURCE_BMKG_RATE_LIMIT_PER_MINUTE=60
SOURCE_ENABLE_AUSPASS=false
SOURCE_ENABLE_EXPERIMENTAL=false
MAX_API_LIMIT=2000
ADMIN_TOKEN=change-me-for-admin-training
ADMIN_TOKEN=change-me
SECRET_KEY=change-me
2 changes: 1 addition & 1 deletion .python-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.11.15
3.11
90 changes: 90 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,93 @@ python -m compileall .
- Background scheduled ingestion.
- Authentication for admin operations.
- Screenshot automation for README assets.

## Multi-source earthquake data and coordinate safety

This Flask dashboard ingests public earthquake observations, normalizes every event into canonical `latitude` and `longitude` fields, stores them in SQLite, and serves the map, globe, dashboard, and probabilistic forecast from the same filtered API path.

### Wrong-location bug fix

The map/globe coordinate order was not globally swapped. USGS GeoJSON correctly uses `[longitude, latitude, depth]`, Leaflet correctly uses `[latitude, longitude]`, and globe.gl correctly uses `{ lat, lng }`. Wrong-looking points were primarily caused by random bootstrap demo records being mixed with real cached feeds, feed-specific cache results not being strictly filtered, bootstrap cache being treated as fresh data, and longitude bounds being sorted across the antimeridian. The API now excludes bootstrap records by default, filters by `source` and `source_feed`, tracks refresh metadata per source/feed, validates coordinates before storage, and handles dateline-crossing viewports with `longitude >= west OR longitude <= east`.

### Coordinate normalization rules

Different sources use different coordinate order. This app normalizes every source into canonical latitude/longitude fields before storage and visualization.

* Backend canonical fields are always `latitude` and `longitude`.
* Leaflet receives `[latitude, longitude]`.
* globe.gl receives `{ lat: latitude, lng: longitude }`.
* USGS GeoJSON, USGS FDSN GeoJSON, and GeoNet GeoJSON parse `[longitude, latitude, depth]`.
* BMKG `Coordinates` parse as `latitude,longitude`.
* Invalid, NaN, infinite, out-of-range, or silently swapped-invalid coordinates are rejected before reaching the frontend.

### Public data sources

| Source | Coverage | Format | Role |
| --- | --- | --- | --- |
| USGS GeoJSON | Global | GeoJSON | Primary live source |
| USGS FDSN | Global | GeoJSON/CSV/XML | Historical and custom search |
| EMSC / SeismicPortal | Europe/global | FDSN JSON/QuakeML/CSV | Cross-checking |
| ISC | Global reviewed catalogue | FDSN | Historical/reviewed (stubbed if unavailable) |
| GFZ/GEOFON | Global/Europe | FDSN | Additional global source (stubbed if unavailable) |
| BMKG | Indonesia | JSON/XML | Asia/Indonesia |
| GeoNet | New Zealand | GeoJSON/JSON | Oceania/New Zealand |
| AusPass | Australia/Oceania | FDSN | Optional, disabled by default |

### Attribution

Data remains attributed in `/api/sources` and frontend status areas. Primary attribution includes USGS Earthquake Hazards Program, EMSC / SeismicPortal, BMKG Indonesia, GeoNet, ISC, GFZ/GEOFON, and AusPass where enabled.

### Environment variables

Recommended production settings:

```text
ENABLE_BOOTSTRAP_DATA=false
ALLOW_BOOTSTRAP_AS_FRESH_CACHE=false
ENABLED_SOURCES=usgs_geojson,usgs_fdsn,emsc,isc,geofon,bmkg,geonet
SOURCE_TIMEOUT_SECONDS=15
SOURCE_MAX_RETRIES=2
SOURCE_REFRESH_TTL_SECONDS=300
MAX_API_LIMIT=2000
ADMIN_TOKEN=change-me
SECRET_KEY=change-me
```

### API examples

```bash
curl '/api/earthquakes?source=usgs&feed=day&include_demo=false'
curl '/api/earthquakes?source=usgs&feed=30day&mag_min=4.5'
curl '/api/earthquakes?source=all&feed=day&lon1=170&lon2=-170'
curl '/api/sources'
curl -X POST '/api/refresh' -H 'Content-Type: application/json' -d '{"source":"all","feed":"day"}'
```

If demo records are explicitly requested with `include_demo=true`, the API and UI warn: “Demo data — not real earthquake locations.”

### Render deployment

Keep deployment simple:

```bash
pip install -r requirements.txt
gunicorn main:app --bind 0.0.0.0:$PORT
```

Set `ENABLE_BOOTSTRAP_DATA=false` on Render so random bootstrap records are never displayed as real earthquake locations.

### Local verification

Run:

```bash
pytest
python -m compileall .
```

Open `/map` and `/globe`, select a source/feed, and test a dateline viewport near Alaska, Japan, Fiji, New Zealand, Indonesia, or the Pacific.

### Scientific disclaimer

The forecast page is probabilistic and educational. It summarizes recent regional seismicity and model-derived risk indicators; it does not predict exact earthquakes, times, or locations.
7 changes: 7 additions & 0 deletions app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@
def create_app(config_class=Config):
app=Flask(__name__, template_folder='../templates', static_folder='../static')
app.config.from_object(config_class)
# Re-read environment-backed settings at app creation time so tests and deployments
# can override values after the Config class has been imported.
import os
if os.getenv('SQLITE_DB_PATH'): app.config['SQLITE_DB_PATH']=os.getenv('SQLITE_DB_PATH')
if os.getenv('ENABLE_BOOTSTRAP_DATA') is not None: app.config['ENABLE_BOOTSTRAP_DATA']=os.getenv('ENABLE_BOOTSTRAP_DATA','false').lower() in {'1','true','yes','on'}
if os.getenv('ALLOW_BOOTSTRAP_AS_FRESH_CACHE') is not None: app.config['ALLOW_BOOTSTRAP_AS_FRESH_CACHE']=os.getenv('ALLOW_BOOTSTRAP_AS_FRESH_CACHE','false').lower() in {'1','true','yes','on'}
if os.getenv('ENABLED_SOURCES'): app.config['ENABLED_SOURCES']=os.getenv('ENABLED_SOURCES')
logging.basicConfig(level=logging.INFO)
init_db(app.config['SQLITE_DB_PATH'])
seed_bootstrap_if_empty(app.config['SQLITE_DB_PATH'], app.config['ENABLE_BOOTSTRAP_DATA'])
Expand Down
10 changes: 9 additions & 1 deletion app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,20 @@ class Config:
SQLITE_DB_PATH = os.getenv('SQLITE_DB_PATH', str(BASE_DIR / 'data' / 'earthquakes.db'))
DEFAULT_FEED_WINDOW = os.getenv('DEFAULT_FEED_WINDOW', 'day')
MODEL_PATH = os.getenv('MODEL_PATH', str(BASE_DIR / 'models' / 'earthquake_risk.joblib'))
ENABLE_BOOTSTRAP_DATA = os.getenv('ENABLE_BOOTSTRAP_DATA', 'true').lower() in {'1','true','yes','on'}
ENABLE_BOOTSTRAP_DATA = os.getenv('ENABLE_BOOTSTRAP_DATA', 'false').lower() in {'1','true','yes','on'}
ALLOW_BOOTSTRAP_AS_FRESH_CACHE = os.getenv('ALLOW_BOOTSTRAP_AS_FRESH_CACHE', 'false').lower() in {'1','true','yes','on'}
ENABLE_MODEL_TRAINING_ON_STARTUP = os.getenv('ENABLE_MODEL_TRAINING_ON_STARTUP', 'false').lower() in {'1','true','yes','on'}
MAX_API_LIMIT = int(os.getenv('MAX_API_LIMIT', '2000'))
ADMIN_TOKEN = os.getenv('ADMIN_TOKEN', '')
FLASK_ENV = os.getenv('FLASK_ENV', os.getenv('ENV', 'production'))
REQUEST_TIMEOUT_SECONDS = int(os.getenv('REQUEST_TIMEOUT_SECONDS', '15'))
SOURCE_TIMEOUT_SECONDS = int(os.getenv('SOURCE_TIMEOUT_SECONDS', os.getenv('REQUEST_TIMEOUT_SECONDS', '15')))
SOURCE_MAX_RETRIES = int(os.getenv('SOURCE_MAX_RETRIES', '2'))
SOURCE_REFRESH_TTL_SECONDS = int(os.getenv('SOURCE_REFRESH_TTL_SECONDS', os.getenv('CACHE_TTL_SECONDS', '300')))
SOURCE_BMKG_RATE_LIMIT_PER_MINUTE = int(os.getenv('SOURCE_BMKG_RATE_LIMIT_PER_MINUTE', '60'))
SOURCE_ENABLE_AUSPASS = os.getenv('SOURCE_ENABLE_AUSPASS', 'false').lower() in {'1','true','yes','on'}
SOURCE_ENABLE_EXPERIMENTAL = os.getenv('SOURCE_ENABLE_EXPERIMENTAL', 'false').lower() in {'1','true','yes','on'}
ENABLED_SOURCES = os.getenv('ENABLED_SOURCES', 'usgs_geojson,usgs_fdsn,emsc,isc,geofon,bmkg,geonet')

FEEDS = {
'hour': 'https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_hour.geojson',
Expand Down
Loading
Loading