● Zion Boggan
repos/Sightline/backend/app/services/realsources.py
zionboggan.com ↗
409 lines · python
History for this file →
1
"""Connectors for REAL, publicly-available data feeds. Every function returns normalized rows
2
that the aggregation pipeline ingests. No keys, no scraping, no TOS violations - these are open
3
government / academic / OSM APIs. Provenance (source + external_id) is preserved on every row.
4
 
5
Fixed assets  -> SensorAsset dicts   (cameras, weather, radar, hydro, seismic)
6
Moving entities -> LiveEntity dicts   (aircraft via ADS-B, satellites via TLE)
7
"""
8
from __future__ import annotations
9
 
10
import json
11
import os
12
import re
13
from datetime import datetime, timezone
14
from urllib.parse import quote
15
 
16
import httpx
17
 
18
UA = {"User-Agent": "Sightline-OSINT/1.0 (public-data aggregation; research)"}
19
_OMM_CACHE = os.path.join(os.path.dirname(__file__), "..", "..", "sample_data", "sat_omm.json")
20
US_BBOX = (24.0, -125.0, 49.5, -66.0)
21
 
22
STATES = ["AL","AK","AZ","AR","CA","CO","CT","DE","FL","GA","HI","ID","IL","IN","IA","KS","KY",
23
          "LA","ME","MD","MA","MI","MN","MS","MO","MT","NE","NV","NH","NJ","NM","NY","NC","ND",
24
          "OH","OK","OR","PA","RI","SC","SD","TN","TX","UT","VT","VA","WA","WV","WI","WY"]
25
 
26
 
27
def _get(url, **kw):
28
    return httpx.get(url, headers=UA, timeout=kw.pop("timeout", 40), **kw)
29
 
30
 
31
def nyc_traffic_cameras() -> list[dict]:
32
    """NYC DOT traffic cameras - REAL public live JPEG snapshots, no key. ~1,000+ cameras with
33
    direct imageUrl that refreshes. The 'live feed' layer the ALPR-location layer can't provide."""
34
    r = _get("https://webcams.nyctmc.org/api/cameras", timeout=40)
35
    if r.status_code != 200:
36
        return []
37
    try:
38
        items = r.json()
39
    except Exception:
40
        return []
41
    if isinstance(items, dict):
42
        items = items.get("items") or items.get("cameras") or []
43
    out = []
44
    for c in items:
45
        cid = c.get("id")
46
        if not cid or c.get("latitude") is None:
47
            continue
48
        img = c.get("imageUrl") or f"https://webcams.nyctmc.org/api/cameras/{cid}/image"
49
        out.append({"external_id": f"NYCDOT-{cid}", "name": c.get("name"),
50
                    "operator": "NYC DOT", "state": "NY", "area": c.get("area"),
51
                    "lat": c.get("latitude"), "lon": c.get("longitude"),
52
                    "online": bool(c.get("isOnline", True)), "image_url": img,
53
                    "live_source": "nyc_dot"})
54
    return out
55
 
56
 
57
def caltrans_cameras() -> list[dict]:
58
    """Caltrans (California DOT) CCTV - REAL public live JPEG snapshots, no key, ~3,500 statewide
59
    across 12 districts, each with lat/lon, county, route, direction."""
60
    out = []
61
    for d in range(1, 13):
62
        url = f"https://cwwp2.dot.ca.gov/data/d{d}/cctv/cctvStatusD{d:02d}.json"
63
        try:
64
            r = _get(url, timeout=25)
65
            if r.status_code != 200:
66
                continue
67
            for rec in r.json().get("data", []):
68
                c = rec.get("cctv", {})
69
                loc = c.get("location", {})
70
                img = (c.get("imageData", {}).get("static", {}) or {}).get("currentImageURL")
71
                lat, lon = loc.get("latitude"), loc.get("longitude")
72
                if not img or not lat or not lon:
73
                    continue
74
                try:
75
                    lat, lon = float(lat), float(lon)
76
                except (TypeError, ValueError):
77
                    continue
78
                if lat == 0 or lon == 0:
79
                    continue
80
                out.append({"external_id": f"CALTRANS-D{d:02d}-{c.get('index') or loc.get('locationName','')[:20]}",
81
                            "name": loc.get("locationName") or loc.get("nearbyPlace"),
82
                            "operator": "Caltrans", "state": "CA",
83
                            "area": loc.get("county") or f"District {d}",
84
                            "lat": lat, "lon": lon, "online": True, "image_url": img,
85
                            "live_source": "caltrans"})
86
        except Exception:
87
            continue
88
    return out
89
 
90
 
91
def tripcheck_cameras() -> list[dict]:
92
    """Oregon DOT TripCheck CCTV - REAL public live JPEG snapshots, no key, ~1,100 statewide.
93
    Inventory is a public ArcGIS-style JSON; images refresh at /RoadCams/cams/<filename>."""
94
    try:
95
        r = _get("https://tripcheck.com/Scripts/map/data/cctvinventory.js", timeout=40)
96
        if r.status_code != 200:
97
            return []
98
        feats = json.loads(r.text).get("features", [])
99
    except Exception:
100
        return []
101
    out = []
102
    for f in feats:
103
        a = f.get("attributes", {})
104
        fn, lat, lon = a.get("filename"), a.get("latitude"), a.get("longitude")
105
        if not fn or not lat or not lon:
106
            continue
107
        try:
108
            lat, lon = float(lat), float(lon)
109
        except (TypeError, ValueError):
110
            continue
111
        out.append({"external_id": f"TRIPCHECK-{a.get('cameraId')}-{a.get('publishedImageId')}",
112
                    "name": (a.get("title") or fn).strip(),
113
                    "operator": "Oregon DOT", "state": "OR",
114
                    "area": (a.get("route") or "").strip() or "Oregon",
115
                    "lat": lat, "lon": lon, "online": True,
116
                    "image_url": "https://tripcheck.com/RoadCams/cams/" + quote(fn),
117
                    "live_source": "tripcheck"})
118
    return out
119
 
120
 
121
_WKT_POINT = re.compile(r"POINT \((-?[\d.]+) (-?[\d.]+)\)")
122
_511_STATE = {"Florida": "FL", "Arizona": "AZ", "Vermont": "VT", "New Hampshire": "NH",
123
              "Maine": "ME", "Massachusetts": "MA", "Connecticut": "CT", "Rhode Island": "RI",
124
              "Georgia": "GA"}
125
 
126
 
127
def _ibi_511_cameras(host: str, prefix: str, operator: str, default_state: str,
128
                     live_source: str, max_pages: int = 60) -> list[dict]:
129
    """Shared connector for IBI-platform 511 sites (fl511.com, az511.gov, newengland511.org...).
130
    Public DataTables endpoint, no key; live snapshots at https://<host>/map/Cctv/<id>."""
131
    import time
132
    out, seen, start, page_fails = [], set(), 0, 0
133
    for _ in range(max_pages):
134
        rows = None
135
        for attempt in range(3):
136
            try:
137
                r = httpx.post(f"https://{host}/List/GetData/Cameras", headers=UA, timeout=40,
138
                               data={"draw": "1", "start": str(start), "length": "100"})
139
                rows = r.json().get("data") or []
140
                break
141
            except Exception:
142
                time.sleep(1 + attempt)
143
        if rows is None:
144
            page_fails += 1
145
            if page_fails >= 2:
146
                break
147
            start += 100
148
            continue
149
        page_fails = 0
150
        if not rows:
151
            break
152
        for d in rows:
153
            wkt = ((d.get("latLng") or {}).get("geography") or {}).get("wellKnownText") or ""
154
            m = _WKT_POINT.search(wkt)
155
            img = next((i.get("imageUrl") for i in (d.get("images") or [])
156
                        if i.get("imageUrl") and not i.get("blocked")), None)
157
            ext = f"{prefix}-{d.get('id')}"
158
            if not m or not img or d.get("id") is None or ext in seen:
159
                continue
160
            seen.add(ext)
161
            if img.startswith("/"):
162
                img = f"https://{host}{img}"
163
            out.append({"external_id": ext,
164
                        "name": d.get("location") or f"{d.get('roadway')} {d.get('direction') or ''}".strip(),
165
                        "operator": operator,
166
                        "state": _511_STATE.get(d.get("state"), default_state),
167
                        "area": d.get("roadway") or d.get("region") or default_state,
168
                        "lat": float(m.group(2)), "lon": float(m.group(1)),
169
                        "online": bool(d.get("visible", True)), "image_url": img,
170
                        "live_source": live_source})
171
        start += 100
172
    return out
173
 
174
 
175
def fl511_cameras() -> list[dict]:
176
    """Florida FL511 CCTV - REAL public live JPEG snapshots, no key, ~4,800 statewide."""
177
    return _ibi_511_cameras("fl511.com", "FL511", "Florida DOT (FL511)", "FL", "fl511")
178
 
179
 
180
def az511_cameras() -> list[dict]:
181
    """Arizona AZ511 CCTV - REAL public live JPEG snapshots, no key, ~600 statewide."""
182
    return _ibi_511_cameras("az511.gov", "AZ511", "Arizona DOT (AZ511)", "AZ", "az511")
183
 
184
 
185
def ne511_cameras() -> list[dict]:
186
    """New England 511 CCTV (VT/NH/ME DOTs) - REAL public live JPEG snapshots, no key, ~400."""
187
    return _ibi_511_cameras("newengland511.org", "NE511", "New England 511", "VT", "ne511")
188
 
189
 
190
def nws_stations(state: str, cap: int = 120) -> list[dict]:
191
    r = _get(f"https://api.weather.gov/stations?state={state}&limit={cap}")
192
    if r.status_code != 200:
193
        return []
194
    out = []
195
    for f in r.json().get("features", []):
196
        p = f.get("properties", {})
197
        c = (f.get("geometry") or {}).get("coordinates") or [None, None]
198
        out.append({"kind": "weather_station", "category": "weather", "source": "nws",
199
                    "external_id": p.get("stationIdentifier"), "name": p.get("name"),
200
                    "operator": "NOAA / NWS", "lat": c[1], "lon": c[0], "state": state,
201
                    "attributes": {"elevation_m": (p.get("elevation") or {}).get("value"),
202
                                   "timezone": p.get("timeZone")}})
203
    return out
204
 
205
 
206
def nws_radar() -> list[dict]:
207
    r = _get("https://api.weather.gov/radar/stations")
208
    if r.status_code != 200:
209
        return []
210
    out = []
211
    for f in r.json().get("features", []):
212
        p = f.get("properties", {})
213
        c = (f.get("geometry") or {}).get("coordinates") or [None, None]
214
        out.append({"kind": "weather_radar", "category": "weather", "source": "nws",
215
                    "external_id": p.get("id") or p.get("stationId"),
216
                    "name": p.get("name"), "operator": "NOAA / NWS",
217
                    "lat": c[1], "lon": c[0], "state": None,
218
                    "attributes": {"type": p.get("stationType"), "rda": (p.get("rda") or {})}})
219
    return out
220
 
221
 
222
def usgs_gauges(state: str, cap: int = 200) -> list[dict]:
223
    r = _get(f"https://waterservices.usgs.gov/nwis/iv/?format=json&stateCd={state}"
224
             f"&parameterCd=00060&siteStatus=active")
225
    if r.status_code != 200:
226
        return []
227
    out = []
228
    for ts in r.json().get("value", {}).get("timeSeries", [])[:cap]:
229
        src = ts.get("sourceInfo", {})
230
        geo = (src.get("geoLocation") or {}).get("geogLocation") or {}
231
        vals = (ts.get("values") or [{}])[0].get("value") or []
232
        latest = vals[-1]["value"] if vals else None
233
        out.append({"kind": "stream_gauge", "category": "hydro", "source": "usgs",
234
                    "external_id": (src.get("siteCode") or [{}])[0].get("value"),
235
                    "name": src.get("siteName"), "operator": "USGS",
236
                    "lat": geo.get("latitude"), "lon": geo.get("longitude"),
237
                    "state": state, "attributes": {"discharge_cfs": latest}})
238
    return out
239
 
240
 
241
def usgs_quakes(min_mag: float = 1.0) -> list[dict]:
242
    r = _get("https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_day.geojson")
243
    if r.status_code != 200:
244
        return []
245
    out = []
246
    for f in r.json().get("features", []):
247
        p, g = f.get("properties", {}), (f.get("geometry") or {}).get("coordinates") or [None, None, None]
248
        if (p.get("mag") or 0) < min_mag:
249
            continue
250
        out.append({"kind": "seismic_event", "category": "seismic", "source": "usgs",
251
                    "external_id": f.get("id"), "name": p.get("place"), "operator": "USGS",
252
                    "lat": g[1], "lon": g[0], "state": None,
253
                    "attributes": {"magnitude": p.get("mag"), "depth_km": g[2],
254
                                   "time": p.get("time"), "felt": p.get("felt")}})
255
    return out
256
 
257
 
258
def opensky_aircraft(bbox=US_BBOX) -> list[dict]:
259
    la1, lo1, la2, lo2 = bbox
260
    r = _get(f"https://opensky-network.org/api/states/all?lamin={la1}&lomin={lo1}"
261
             f"&lamax={la2}&lomax={lo2}", timeout=60)
262
    if r.status_code != 200:
263
        return []
264
    now = datetime.now(timezone.utc)
265
    out = []
266
    for s in (r.json().get("states") or []):
267
        icao, callsign, country = s[0], (s[1] or "").strip(), s[2]
268
        lon, lat, alt, on_ground, vel, heading, vrate = s[5], s[6], s[7], s[8], s[9], s[10], s[11]
269
        if lat is None or lon is None:
270
            continue
271
        out.append({"kind": "aircraft", "source": "opensky", "identifier": icao,
272
                    "label": callsign or icao, "lat": lat, "lon": lon,
273
                    "altitude_m": alt, "heading": heading, "velocity": vel,
274
                    "origin_country": country,
275
                    "attributes": {"on_ground": on_ground, "vertical_rate": vrate,
276
                                   "geo_altitude": s[13] if len(s) > 13 else None,
277
                                   "squawk": s[14] if len(s) > 14 else None}})
278
    return out
279
 
280
 
281
def celestrak_satellites(cap: int = 800, group: str = "active") -> list[dict]:
282
    """Active satellites from CelesTrak (OMM JSON). Positions propagated to 'now' with SGP4 via
283
    sgp4.omm.initialize - real current sub-points, not placeholders. CelesTrak throttles repeat
284
    pulls of the same GROUP per IP for ~2h; on throttle this returns [] gracefully."""
285
    try:
286
        from sgp4.api import Satrec, jday
287
        from sgp4 import omm
288
    except Exception:
289
        return []
290
    r = _get(f"https://celestrak.org/NORAD/elements/gp.php?GROUP={group}&FORMAT=json", timeout=60)
291
    if r.status_code != 200 or not r.headers.get("content-type", "").startswith("application/json"):
292
        return []
293
    try:
294
        elements = r.json()
295
    except Exception:
296
        return []
297
    now = datetime.now(timezone.utc)
298
    jd, fr = jday(now.year, now.month, now.day, now.hour, now.minute,
299
                  now.second + now.microsecond / 1e6)
300
    gmst = _gmst(jd + fr)
301
    out = []
302
    for el in elements[:cap]:
303
        try:
304
            sat = Satrec()
305
            omm.initialize(sat, el)
306
            e, r_eci, _ = sat.sgp4(jd, fr)
307
            if e != 0:
308
                continue
309
            lat, lon, alt = _eci_to_geodetic(r_eci, gmst)
310
            out.append({"kind": "satellite", "source": "celestrak",
311
                        "identifier": str(el.get("NORAD_CAT_ID")), "label": el.get("OBJECT_NAME"),
312
                        "lat": lat, "lon": lon, "altitude_m": alt * 1000.0,
313
                        "heading": None, "velocity": None, "origin_country": None,
314
                        "attributes": {"inclination": el.get("INCLINATION"),
315
                                       "period_min": (1440.0 / float(el["MEAN_MOTION"])) if el.get("MEAN_MOTION") else None,
316
                                       "omm": el}})
317
        except Exception:
318
            continue
319
    return out
320
 
321
 
322
def _fetch_omm(group: str) -> list[dict]:
323
    r = _get(f"https://celestrak.org/NORAD/elements/gp.php?GROUP={group}&FORMAT=json", timeout=60)
324
    if r.status_code != 200 or not r.headers.get("content-type", "").startswith("application/json"):
325
        return []
326
    try:
327
        els = r.json()
328
    except Exception:
329
        return []
330
    for e in els:
331
        e["_group"] = group
332
    return els
333
 
334
 
335
def imaging_satellites(cap_per_group: int = 400) -> list[dict]:
336
    """REAL imaging / Earth-observation satellites ('cameras in orbit'): Planet SkySat & Dove +
337
    Earth-resources birds, positions propagated to now via SGP4. CelesTrak throttles repeat pulls
338
    per IP (~2h); orbital elements are cached so positions keep propagating locally regardless."""
339
    try:
340
        from sgp4.api import Satrec, jday
341
        from sgp4 import omm as _omm
342
    except Exception:
343
        return []
344
 
345
    elements: list[dict] = []
346
    for group in ("planet", "resource"):
347
        elements += _fetch_omm(group)[:cap_per_group]
348
    if elements:
349
        try:
350
            os.makedirs(os.path.dirname(_OMM_CACHE), exist_ok=True)
351
            with open(_OMM_CACHE, "w") as fh:
352
                json.dump(elements, fh)
353
        except Exception:
354
            pass
355
    elif os.path.exists(_OMM_CACHE):
356
        try:
357
            with open(_OMM_CACHE) as fh:
358
                elements = json.load(fh)
359
        except Exception:
360
            elements = []
361
    if not elements:
362
        return []
363
 
364
    now = datetime.now(timezone.utc)
365
    jd, fr = jday(now.year, now.month, now.day, now.hour, now.minute,
366
                  now.second + now.microsecond / 1e6)
367
    gmst = _gmst(jd + fr)
368
    seen, out = set(), []
369
    for el in elements:
370
        nid = str(el.get("NORAD_CAT_ID"))
371
        if nid in seen:
372
            continue
373
        try:
374
            sat = Satrec()
375
            _omm.initialize(sat, el)
376
            e, r_eci, _ = sat.sgp4(jd, fr)
377
            if e != 0:
378
                continue
379
            lat, lon, alt = _eci_to_geodetic(r_eci, gmst)
380
            seen.add(nid)
381
            out.append({"kind": "satellite_camera", "source": "celestrak", "identifier": nid,
382
                        "label": el.get("OBJECT_NAME"), "lat": lat, "lon": lon,
383
                        "altitude_m": alt * 1000.0, "heading": None, "velocity": None,
384
                        "origin_country": None,
385
                        "attributes": {"constellation": el.get("_group"),
386
                                       "inclination": el.get("INCLINATION"),
387
                                       "period_min": (1440.0 / float(el["MEAN_MOTION"])) if el.get("MEAN_MOTION") else None,
388
                                       "omm": el}})
389
        except Exception:
390
            continue
391
    return out
392
 
393
 
394
def _gmst(jd_ut1: float) -> float:
395
    import math
396
    t = (jd_ut1 - 2451545.0) / 36525.0
397
    g = 280.46061837 + 360.98564736629 * (jd_ut1 - 2451545.0) + 0.000387933 * t * t
398
    return math.radians(g % 360.0)
399
 
400
 
401
def _eci_to_geodetic(r_eci, gmst):
402
    import math
403
    x, y, z = r_eci
404
    lon = math.degrees(math.atan2(y, x) - gmst)
405
    lon = ((lon + 180) % 360) - 180
406
    r = math.sqrt(x * x + y * y)
407
    lat = math.degrees(math.atan2(z, r))
408
    alt = math.sqrt(x * x + y * y + z * z) - 6371.0
409
    return lat, lon, alt