Skip to content

Commit d283464

Browse files
committed
Add sensor image metadata
1 parent d9b4cad commit d283464

4 files changed

Lines changed: 204 additions & 80 deletions

File tree

publishers/aviation_wx/bootstrap_aviation_wx.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
AWX_HOME = "https://aviationweather.gov/"
6262
AWX_API_DOC = "https://aviationweather.gov/data/api/"
6363
AWX_METAR_BASE = "https://aviationweather.gov/metar/data?ids="
64+
AWX_ASOS_IMAGE = "https://www.weather.gov/images/asos/IMG_1176%20blank.png"
6465

6566
# ── FAA contact ──────────────────────────────────────────────────────────
6667
FAA_CONTACT_ORG = "FAA / Aviation Weather Center (AWC)"
@@ -235,6 +236,12 @@ def _system_sml(station: dict) -> dict:
235236

236237
# ── Build documents list ──────────────────────────────────────────
237238
docs: list[dict] = [
239+
{
240+
"role": "http://dbpedia.org/resource/Photograph",
241+
"name": "ASOS Station Image",
242+
"description": "Representative photograph of a typical ASOS/AWOS aviation weather observing installation.",
243+
"link": {"href": AWX_ASOS_IMAGE, "type": "image/png"},
244+
},
238245
{
239246
"role": "http://dbpedia.org/resource/Web_page",
240247
"name": "METAR Data Page",

publishers/iss/bootstrap_iss.py

Lines changed: 84 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,13 @@
6060

6161
DEPLOY_ROOT_UID = "urn:os4csapi:deployment:orbital-tracking-demo:v1"
6262

63+
ISS_PHOTO_URL = (
64+
"https://upload.wikimedia.org/wikipedia/commons/thumb/d/d3/"
65+
"International_Space_Station_after_undocking_of_STS-132.jpg/"
66+
"640px-International_Space_Station_after_undocking_of_STS-132.jpg"
67+
)
68+
NASA_ISS_TRACK_URL = "https://spotthestation.nasa.gov/"
69+
6370

6471
# ═══════════════════════════════════════════════════════════════════════════
6572
# Resource definitions — Procedures
@@ -123,29 +130,6 @@ def _system_position() -> dict:
123130
"title": "SGP4 Propagation v1",
124131
"type": "application/sml+json",
125132
},
126-
"documentation": [
127-
{
128-
"role": "http://dbpedia.org/resource/Photograph",
129-
"name": "ISS Photograph",
130-
"description": (
131-
"NASA photograph of the International Space Station "
132-
"taken from the Space Shuttle Discovery during STS-119."
133-
),
134-
"link": {
135-
"href": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/d3/International_Space_Station_after_undocking_of_STS-132.jpg/640px-International_Space_Station_after_undocking_of_STS-132.jpg",
136-
"type": "image/jpeg",
137-
},
138-
},
139-
{
140-
"role": "http://dbpedia.org/resource/Web_page",
141-
"name": "ISS Tracking Page",
142-
"description": "NASA real-time ISS tracking page.",
143-
"link": {
144-
"href": "https://spotthestation.nasa.gov/",
145-
"type": "text/html",
146-
},
147-
},
148-
],
149133
},
150134
}
151135

@@ -172,6 +156,76 @@ def _system_orbit_track() -> dict:
172156
}
173157

174158

159+
def _system_sml(uid: str, label: str, description: str, procedure_uid: str, procedure_label: str) -> dict:
160+
return {
161+
"type": "PhysicalSystem",
162+
"id": uid,
163+
"uniqueId": uid,
164+
"definition": "sosa:System",
165+
"label": label,
166+
"description": description,
167+
"keywords": [
168+
"ISS", "International Space Station", "satellite", "orbit",
169+
"SGP4", "TLE", "space station", "orbital tracking",
170+
],
171+
"identifiers": [
172+
{"definition": "http://sensorml.com/ont/swe/property/ShortName",
173+
"label": "Short Name", "value": label},
174+
{"definition": "http://sensorml.com/ont/swe/property/UniqueID",
175+
"label": "OS4CSAPI UID", "value": uid},
176+
],
177+
"classifiers": [
178+
{"definition": "http://sensorml.com/ont/swe/property/SensorType",
179+
"label": "System Type", "value": "Software Agent"},
180+
{"definition": "http://sensorml.com/ont/swe/property/SystemRole",
181+
"label": "System Role", "value": "Orbit Feed"},
182+
{"definition": "http://sensorml.com/ont/swe/property/IntendedApplication",
183+
"label": "Intended Application", "value": "Orbital tracking demonstration"},
184+
],
185+
"documents": [
186+
{
187+
"role": "http://dbpedia.org/resource/Photograph",
188+
"name": "ISS Photograph",
189+
"description": "NASA photograph of the International Space Station.",
190+
"link": {"href": ISS_PHOTO_URL, "type": "image/jpeg"},
191+
},
192+
{
193+
"role": "http://dbpedia.org/resource/Web_page",
194+
"name": "NASA Spot the Station",
195+
"description": "NASA real-time ISS tracking and sighting page.",
196+
"link": {"href": NASA_ISS_TRACK_URL, "type": "text/html"},
197+
},
198+
],
199+
"validTime": [VALID_TIME_START, ".."],
200+
}
201+
202+
203+
def _system_position_sml() -> dict:
204+
return _system_sml(
205+
SYS_POS_UID,
206+
"ISS Position Publisher",
207+
(
208+
"Virtual sensor that computes the International Space Station's geodetic "
209+
"position using SGP4 propagation from NORAD TLE data."
210+
),
211+
PROC_SGP4_UID,
212+
"SGP4 Propagation v1",
213+
)
214+
215+
216+
def _system_orbit_track_sml() -> dict:
217+
return _system_sml(
218+
SYS_TRACK_UID,
219+
"ISS Orbit Track Publisher",
220+
(
221+
"Virtual sensor that generates predicted ISS ground-track products by "
222+
"propagating SGP4 positions at 60-second intervals."
223+
),
224+
PROC_ORBIT_UID,
225+
"Orbit Track Generation v1",
226+
)
227+
228+
175229
# ═══════════════════════════════════════════════════════════════════════════
176230
# Resource definitions — Datastreams
177231
# ═══════════════════════════════════════════════════════════════════════════
@@ -328,6 +382,7 @@ def main():
328382
print(f" ISS Tracking Bootstrap")
329383
print(f" Server: {base_url}")
330384
print(f" Mode: {'DRY RUN' if dry_run else 'LIVE'}")
385+
print(f" Force-SML: {args.force_sml}")
331386
print(f"{'='*60}\n")
332387

333388
# ── Clean if requested ────────────────────────────────────────────
@@ -352,9 +407,13 @@ def main():
352407
# ── Systems ───────────────────────────────────────────────────────
353408
print("\n── Systems ──")
354409
pos_sys_id = ensure_system(base_url, auth, SYS_POS_UID, _system_position(),
355-
dry_run=dry_run, stats=stats)
410+
_system_position_sml(),
411+
dry_run=dry_run, stats=stats,
412+
force_sml=args.force_sml)
356413
track_sys_id = ensure_system(base_url, auth, SYS_TRACK_UID, _system_orbit_track(),
357-
dry_run=dry_run, stats=stats)
414+
_system_orbit_track_sml(),
415+
dry_run=dry_run, stats=stats,
416+
force_sml=args.force_sml)
358417

359418
# ── Datastreams ───────────────────────────────────────────────────
360419
print("\n── Datastreams ──")

publishers/nws/bootstrap_nws.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,15 @@ def _system_sml(station: dict) -> dict:
414414
}
415415

416416

417+
def _go_compatible_system_sml(sml: dict, base_url: str) -> dict:
418+
if "csapi-go" not in base_url:
419+
return sml
420+
compat = dict(sml)
421+
compat.pop("characteristics", None)
422+
compat.pop("capabilities", None)
423+
return compat
424+
425+
417426
def _datastream_schema(station_id: str = "") -> dict:
418427
"""SWE DataRecord schema for surface observation datastream.
419428
@@ -634,7 +643,7 @@ def bootstrap(*, clean: bool = False, clean_only: bool = False,
634643

635644
# Build stub body — need procedure server ID for typeOf link
636645
stub = _system_stub(st, proc_id or "pending")
637-
sml = _system_sml(st)
646+
sml = _go_compatible_system_sml(_system_sml(st), base_url)
638647

639648
sys_id = ensure_system(base_url, auth, uid, stub, sml,
640649
dry_run=dry_run, stats=stats,

publishers/usgs_water/bootstrap_usgs_water.py

Lines changed: 103 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@
3434
import json
3535
import os
3636
import sys
37+
from urllib.error import HTTPError, URLError
38+
from urllib.parse import quote
39+
from urllib.request import Request, urlopen
3740

3841
# Add parent dir to path for shared helpers
3942
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", ".."))
@@ -75,6 +78,8 @@
7578
USGS_PARAMETER_CODES = "https://api.waterdata.usgs.gov/ogcapi/v0/collections/parameter-codes"
7679
USGS_STATISTIC_CODES = "https://api.waterdata.usgs.gov/ogcapi/v0/collections/statistic-codes"
7780
USGS_API_REGISTRATION = "https://api.usgs.gov/"
81+
USGS_NIMS_API_BASE = "https://api.waterdata.usgs.gov/nims/v0"
82+
USGS_NIMS_IMAGE_BASE = "https://usgs-nims-images.s3.amazonaws.com"
7883

7984
# Series semantics
8085
STATISTIC_INSTANTANEOUS = "00011"
@@ -131,6 +136,36 @@ def _combined_metadata_url(nwis_id: str, parameter_code: str) -> str:
131136
)
132137

133138

139+
def _nims_latest_image_doc(station: dict) -> dict | None:
140+
cam_id = station.get("camId")
141+
if not cam_id:
142+
return None
143+
144+
url = f"{USGS_NIMS_API_BASE}/listFiles?camId={quote(cam_id)}&limit=1&recent=true"
145+
try:
146+
req = Request(url, headers={"Accept": "application/json"})
147+
with urlopen(req, timeout=10) as resp:
148+
files = json.loads(resp.read().decode())
149+
except (HTTPError, URLError, TimeoutError, ValueError, OSError) as exc:
150+
print(f" [WARN] NIMS thumbnail lookup skipped for {cam_id}: {exc}")
151+
return None
152+
153+
if not isinstance(files, list) or not files:
154+
return None
155+
156+
filename = files[0]
157+
if not isinstance(filename, str) or not filename.lower().endswith(".jpg"):
158+
return None
159+
160+
thumb_url = f"{USGS_NIMS_IMAGE_BASE}/thumbnail/{quote(cam_id)}/{quote(filename)}"
161+
return {
162+
"role": "http://dbpedia.org/resource/Photograph",
163+
"name": "USGS NIMS Camera Image",
164+
"description": f"Latest available USGS NIMS camera thumbnail for {station['name']}.",
165+
"link": {"href": thumb_url, "type": "image/jpeg"},
166+
}
167+
168+
134169
# ======================================================================
135170
# Resource definitions
136171
# ======================================================================
@@ -350,6 +385,63 @@ def _system_sml(station: dict) -> dict:
350385
"value": station["camId"],
351386
})
352387

388+
documents = [
389+
doc for doc in [_nims_latest_image_doc(station)] if doc
390+
]
391+
documents.extend([
392+
{
393+
"role": "http://dbpedia.org/resource/Web_page",
394+
"name": "Monitoring Location",
395+
"description": f"USGS monitoring-location resource for site {nwis_id}.",
396+
"link": {
397+
"href": station.get("monitoringLocationUrl", _monitoring_location_url(nwis_id)),
398+
"type": "application/geo+json",
399+
},
400+
},
401+
{
402+
"role": "http://dbpedia.org/resource/Web_page",
403+
"name": "Latest Continuous - Discharge",
404+
"description": f"Latest discharge values for site {nwis_id}.",
405+
"link": {
406+
"href": station.get("latestContinuous00060Url", _latest_continuous_url(nwis_id, "00060")),
407+
"type": "application/geo+json",
408+
},
409+
},
410+
{
411+
"role": "http://dbpedia.org/resource/Web_page",
412+
"name": "Latest Continuous - Gage Height",
413+
"description": f"Latest gage-height values for site {nwis_id}.",
414+
"link": {
415+
"href": station.get("latestContinuous00065Url", _latest_continuous_url(nwis_id, "00065")),
416+
"type": "application/geo+json",
417+
},
418+
},
419+
{
420+
"role": "http://dbpedia.org/resource/Web_page",
421+
"name": "Time Series Metadata - Discharge",
422+
"description": f"Time-series metadata for discharge at site {nwis_id}.",
423+
"link": {
424+
"href": station.get("timeSeries00060Url", _time_series_metadata_url(nwis_id, "00060")),
425+
"type": "application/geo+json",
426+
},
427+
},
428+
{
429+
"role": "http://dbpedia.org/resource/Web_page",
430+
"name": "Time Series Metadata - Gage Height",
431+
"description": f"Time-series metadata for gage height at site {nwis_id}.",
432+
"link": {
433+
"href": station.get("timeSeries00065Url", _time_series_metadata_url(nwis_id, "00065")),
434+
"type": "application/geo+json",
435+
},
436+
},
437+
{
438+
"role": "http://dbpedia.org/resource/Web_page",
439+
"name": "USGS Water Data OGC API",
440+
"description": "Official USGS Water Data OGC API documentation.",
441+
"link": {"href": USGS_API_DOCS, "type": "text/html"},
442+
},
443+
])
444+
353445
return {
354446
"type": "PhysicalSystem",
355447
"id": _system_uid(nwis_id),
@@ -432,59 +524,7 @@ def _system_sml(station: dict) -> dict:
432524
},
433525
},
434526
],
435-
"documents": [
436-
{
437-
"role": "http://dbpedia.org/resource/Web_page",
438-
"name": "Monitoring Location",
439-
"description": f"USGS monitoring-location resource for site {nwis_id}.",
440-
"link": {
441-
"href": station.get("monitoringLocationUrl", _monitoring_location_url(nwis_id)),
442-
"type": "application/geo+json",
443-
},
444-
},
445-
{
446-
"role": "http://dbpedia.org/resource/Web_page",
447-
"name": "Latest Continuous - Discharge",
448-
"description": f"Latest discharge values for site {nwis_id}.",
449-
"link": {
450-
"href": station.get("latestContinuous00060Url", _latest_continuous_url(nwis_id, "00060")),
451-
"type": "application/geo+json",
452-
},
453-
},
454-
{
455-
"role": "http://dbpedia.org/resource/Web_page",
456-
"name": "Latest Continuous - Gage Height",
457-
"description": f"Latest gage-height values for site {nwis_id}.",
458-
"link": {
459-
"href": station.get("latestContinuous00065Url", _latest_continuous_url(nwis_id, "00065")),
460-
"type": "application/geo+json",
461-
},
462-
},
463-
{
464-
"role": "http://dbpedia.org/resource/Web_page",
465-
"name": "Time Series Metadata - Discharge",
466-
"description": f"Time-series metadata for discharge at site {nwis_id}.",
467-
"link": {
468-
"href": station.get("timeSeries00060Url", _time_series_metadata_url(nwis_id, "00060")),
469-
"type": "application/geo+json",
470-
},
471-
},
472-
{
473-
"role": "http://dbpedia.org/resource/Web_page",
474-
"name": "Time Series Metadata - Gage Height",
475-
"description": f"Time-series metadata for gage height at site {nwis_id}.",
476-
"link": {
477-
"href": station.get("timeSeries00065Url", _time_series_metadata_url(nwis_id, "00065")),
478-
"type": "application/geo+json",
479-
},
480-
},
481-
{
482-
"role": "http://dbpedia.org/resource/Web_page",
483-
"name": "USGS Water Data OGC API",
484-
"description": "Official USGS Water Data OGC API documentation.",
485-
"link": {"href": USGS_API_DOCS, "type": "text/html"},
486-
},
487-
],
527+
"documents": documents,
488528
"characteristics": [
489529
{
490530
"label": "Station Properties",
@@ -528,6 +568,15 @@ def _system_sml(station: dict) -> dict:
528568
}
529569

530570

571+
def _go_compatible_system_sml(sml: dict, base_url: str) -> dict:
572+
if "csapi-go" not in base_url:
573+
return sml
574+
compat = dict(sml)
575+
compat.pop("characteristics", None)
576+
compat.pop("capabilities", None)
577+
return compat
578+
579+
531580
def _discharge_datastream_schema(site_no: str = "") -> dict:
532581
"""SWE DataRecord schema for the discharge (streamflow) datastream."""
533582
uid_suffix = f":{site_no}" if site_no else ""
@@ -829,7 +878,7 @@ def bootstrap(*, clean: bool = False, clean_only: bool = False,
829878
uid = _system_uid(nwis_id)
830879

831880
stub = _system_stub(st, proc_id or "pending")
832-
sml = _system_sml(st)
881+
sml = _go_compatible_system_sml(_system_sml(st), base_url)
833882

834883
sys_id = ensure_system(base_url, auth, uid, stub, sml,
835884
dry_run=dry_run, stats=stats,

0 commit comments

Comments
 (0)