Created a script to pull sighting data from
INatraulist in just the US based on species list
This commit is contained in:
1 parent
afa683b7e3
commit
b6873c829c
3 files changed
+659
No files matched your search
@@ -0,0 +1,88 @@
|
||||
"""
|
||||
Visualize species sighting coordinates overlaid on a map.
|
||||
Usage:
|
||||
python dev_scripts/map_viewer.py # shows all species
|
||||
python dev_scripts/map_viewer.py "Mallard" # filter by common name (partial match)
|
||||
python dev_scripts/map_viewer.py --list # list all available species
|
||||
|
||||
Requires: pip install folium
|
||||
"""
|
||||
|
||||
import csv
|
||||
import json
|
||||
import sys
|
||||
import webbrowser
|
||||
from pathlib import Path
|
||||
|
||||
SIGHTINGS_FILE = Path(__file__).parent.parent / "wildlife" / "portland_wildlife_sightings.csv"
|
||||
OUTPUT_HTML = Path(__file__).parent.parent / "wildlife" / "sightings_map.html"
|
||||
PORTLAND_CENTER = [45.5051, -122.6750]
|
||||
|
||||
|
||||
def load_sightings(filter_name: str | None = None) -> list[dict]:
|
||||
rows = []
|
||||
with open(SIGHTINGS_FILE, newline="", encoding="utf-8") as f:
|
||||
for row in csv.DictReader(f):
|
||||
coords = json.loads(row["sighting_coordinates"])
|
||||
if not coords:
|
||||
continue
|
||||
if filter_name and filter_name.lower() not in row["common_name"].lower():
|
||||
continue
|
||||
rows.append({
|
||||
"taxon_id": row["taxon_id"],
|
||||
"common_name": row["common_name"],
|
||||
"scientific_name": row["scientific_name"],
|
||||
"coords": coords,
|
||||
})
|
||||
return rows
|
||||
|
||||
|
||||
def build_map(species_list: list[dict]) -> str:
|
||||
try:
|
||||
import folium
|
||||
except ImportError:
|
||||
print("folium is not installed. Run: pip install folium")
|
||||
sys.exit(1)
|
||||
|
||||
m = folium.Map(location=PORTLAND_CENTER, zoom_start=11, tiles="OpenStreetMap")
|
||||
|
||||
for sp in species_list:
|
||||
for lat, lng in sp["coords"]:
|
||||
folium.CircleMarker(
|
||||
location=[lat, lng],
|
||||
radius=5,
|
||||
color="red",
|
||||
fill=True,
|
||||
fill_color="red",
|
||||
fill_opacity=0.7,
|
||||
tooltip=f"{sp['common_name']}<br><i>{sp['scientific_name']}</i>",
|
||||
).add_to(m)
|
||||
|
||||
return m._repr_html_()
|
||||
|
||||
|
||||
def main():
|
||||
if "--list" in sys.argv:
|
||||
with open(SIGHTINGS_FILE, newline="", encoding="utf-8") as f:
|
||||
for row in csv.DictReader(f):
|
||||
coords = json.loads(row["sighting_coordinates"])
|
||||
print(f" {row['common_name']:40s} {len(coords)} sightings")
|
||||
return
|
||||
|
||||
filter_name = next((a for a in sys.argv[1:] if not a.startswith("--")), None)
|
||||
species_list = load_sightings(filter_name)
|
||||
|
||||
if not species_list:
|
||||
print(f"No species found matching '{filter_name}'")
|
||||
return
|
||||
|
||||
print(f"Plotting {len(species_list)} species...")
|
||||
html = build_map(species_list)
|
||||
|
||||
OUTPUT_HTML.write_text(html, encoding="utf-8")
|
||||
print(f"Map saved to {OUTPUT_HTML}")
|
||||
webbrowser.open(OUTPUT_HTML.as_uri())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,112 @@
|
||||
import csv
|
||||
import json
|
||||
import time
|
||||
import requests
|
||||
from pathlib import Path
|
||||
|
||||
PLACE_ID = 2143
|
||||
BASE_URL = "https://api.inaturalist.org/v1/observations"
|
||||
INPUT_FILE = Path(__file__).parent.parent / "wildlife" / "portland_wildlife.csv"
|
||||
OUTPUT_FILE = Path(__file__).parent.parent / "wildlife" / "portland_wildlife_sightings.csv"
|
||||
OUTPUT_FIELDS = ["taxon_id", "scientific_name", "common_name", "sighting_coordinates"]
|
||||
|
||||
|
||||
def load_already_fetched(path: Path) -> set[int]:
|
||||
"""Return set of taxon_ids already written to the output CSV."""
|
||||
if not path.exists():
|
||||
return set()
|
||||
done = set()
|
||||
try:
|
||||
with open(path, newline="", encoding="utf-8") as f:
|
||||
for row in csv.DictReader(f):
|
||||
done.add(int(row["taxon_id"]))
|
||||
except Exception:
|
||||
pass
|
||||
return done
|
||||
|
||||
|
||||
def fetch_observations(taxon_id: int) -> list[list[float]]:
|
||||
"""Fetch up to 600 random research-grade observations (3 pages x 200) and return [lat, lng] pairs."""
|
||||
coords = []
|
||||
for page in range(1, 4):
|
||||
response = requests.get(
|
||||
BASE_URL,
|
||||
headers={"User-Agent": "WildDex (wildlife dex app; contact via colemanswenson@gmail.com)"},
|
||||
params={
|
||||
"taxon_id": taxon_id,
|
||||
"quality_grade": "research",
|
||||
"per_page": 200,
|
||||
"page": page,
|
||||
"order_by": "random",
|
||||
"swlat": 14.5,
|
||||
"swlng": -168.0,
|
||||
"nelat": 72.0,
|
||||
"nelng": -52.0,
|
||||
},
|
||||
)
|
||||
response.raise_for_status()
|
||||
results = response.json().get("results", [])
|
||||
if not results:
|
||||
break
|
||||
for obs in results:
|
||||
location = obs.get("location")
|
||||
if not location:
|
||||
continue
|
||||
try:
|
||||
lat, lng = location.split(",")
|
||||
coords.append([float(lat), float(lng)])
|
||||
except (ValueError, AttributeError):
|
||||
continue
|
||||
time.sleep(1)
|
||||
return coords
|
||||
|
||||
|
||||
def read_species(path: Path) -> list[dict]:
|
||||
"""Read portland_wildlife.csv and return all rows with taxon_id cast to int."""
|
||||
with open(path, newline="", encoding="utf-8") as f:
|
||||
rows = list(csv.DictReader(f))
|
||||
for row in rows:
|
||||
row["taxon_id"] = int(row["taxon_id"])
|
||||
return rows
|
||||
|
||||
|
||||
def main() -> None:
|
||||
species = read_species(INPUT_FILE)
|
||||
done = load_already_fetched(OUTPUT_FILE)
|
||||
is_new = not OUTPUT_FILE.exists() or OUTPUT_FILE.stat().st_size == 0
|
||||
total = len(species)
|
||||
|
||||
with open(OUTPUT_FILE, "a", newline="", encoding="utf-8") as f:
|
||||
writer = csv.DictWriter(f, fieldnames=OUTPUT_FIELDS)
|
||||
if is_new:
|
||||
writer.writeheader()
|
||||
|
||||
for i, sp in enumerate(species, start=1):
|
||||
taxon_id = sp["taxon_id"]
|
||||
|
||||
if taxon_id in done:
|
||||
print(f"[{i}/{total}] Skipping {sp['common_name']} (already done)")
|
||||
continue
|
||||
|
||||
try:
|
||||
coords = fetch_observations(taxon_id)
|
||||
except requests.HTTPError as e:
|
||||
print(f"[{i}/{total}] {sp['common_name']} — HTTP error {e}, skipping")
|
||||
coords = []
|
||||
|
||||
writer.writerow({
|
||||
"taxon_id": taxon_id,
|
||||
"scientific_name": sp["scientific_name"],
|
||||
"common_name": sp["common_name"],
|
||||
"sighting_coordinates": json.dumps(coords),
|
||||
})
|
||||
f.flush()
|
||||
|
||||
print(f"[{i}/{total}] {sp['common_name']} ({taxon_id}) — {len(coords)} coords")
|
||||
time.sleep(1)
|
||||
|
||||
print(f"\nDone. Output: {OUTPUT_FILE}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user