1+ #!/usr/bin/env python3
2+ """Bootstrap curated Finnish Digitraffic weather camera CSAPI resources."""
3+
4+ import argparse
5+ import json
6+ import os
7+ import sys
8+
9+ sys .path .insert (0 , os .path .join (os .path .dirname (__file__ ), ".." , ".." ))
10+ from publishers .bootstrap_helpers import (
11+ get_config , _auth_header ,
12+ ensure_procedure , ensure_datastream , ensure_deployment ,
13+ find_by_uid , clean_resource , add_bootstrap_args , print_summary ,
14+ )
15+
16+
17+ VALID_TIME_START = "2026-01-01T00:00:00Z"
18+ PUBLISH_INTERVAL_SECONDS = 300
19+
20+ PROC_UID = "urn:os4csapi:procedure:digitraffic-weathercam:v1"
21+ DEPLOY_ROOT_UID = "urn:os4csapi:deployment:digitraffic-weathercam-demo:v1"
22+ DEPLOY_GROUP_UID = "urn:os4csapi:deployment:digitraffic-weathercam-presets:v1"
23+ DS_OUTPUT_NAME = "digitrafficWeatherCamImage"
24+
25+ DIGITRAFFIC_HOME = "https://www.digitraffic.fi/en/road-traffic/"
26+ DIGITRAFFIC_LICENSE = "https://www.digitraffic.fi/en/terms-of-use/"
27+ DIGITRAFFIC_WEATHERCAM_STATIONS = "https://tie.digitraffic.fi/api/weathercam/v1/stations"
28+ DIGITRAFFIC_WEATHERCAM_IMAGE_BASE = "https://weathercam.digitraffic.fi"
29+
30+
31+ def _load_cameras () -> list [dict ]:
32+ here = os .path .dirname (os .path .abspath (__file__ ))
33+ with open (os .path .join (here , "cameras.json" ), encoding = "utf-8" ) as file :
34+ return json .load (file )["cameras" ]
35+
36+
37+ def _system_uid (road_weather_station_id : str ) -> str :
38+ return f"urn:os4csapi:system:digitraffic-road-weather:{ road_weather_station_id } :v1"
39+
40+
41+ def _deploy_uid (camera : dict ) -> str :
42+ return f"urn:os4csapi:deployment:digitraffic-weathercam-{ camera ['presetId' ]} :v1"
43+
44+
45+ def _datastream_uid (camera : dict ) -> str :
46+ return f"urn:os4csapi:datastream:digitraffic-weathercam:{ camera ['presetId' ]} :digitrafficWeatherCamImage:v1"
47+
48+
49+ def _station_data_url (camera_station_id : str ) -> str :
50+ return f"https://tie.digitraffic.fi/api/weathercam/v1/stations/{ camera_station_id } /data"
51+
52+
53+ def _image_url (preset_id : str ) -> str :
54+ return f"{ DIGITRAFFIC_WEATHERCAM_IMAGE_BASE } /{ preset_id } .jpg"
55+
56+
57+ def _thumb_url (preset_id : str ) -> str :
58+ return f"{ _image_url (preset_id )} ?thumbnail=true"
59+
60+
61+ PROCEDURE_STUB = {
62+ "type" : "Feature" ,
63+ "geometry" : None ,
64+ "properties" : {
65+ "uid" : PROC_UID ,
66+ "featureType" : "sosa:ObservingProcedure" ,
67+ "name" : "Digitraffic Weather Camera Image v1" ,
68+ "description" : "Publishes image-reference observations from curated Fintraffic Digitraffic road-weather camera presets." ,
69+ "validTime" : [VALID_TIME_START , ".." ],
70+ },
71+ }
72+
73+ PROCEDURE_SML = {
74+ "type" : "SimpleProcess" ,
75+ "id" : PROC_UID ,
76+ "uniqueId" : PROC_UID ,
77+ "definition" : "sosa:ObservingProcedure" ,
78+ "label" : "Digitraffic Weather Camera Image v1" ,
79+ "description" : (
80+ "Polls selected Fintraffic Digitraffic road-weather camera preset metadata, "
81+ "derives direct JPEG and thumbnail URLs, and publishes image-reference observations "
82+ "on the companion Digitraffic road-weather station systems."
83+ ),
84+ "keywords" : ["Fintraffic" , "Digitraffic" , "Finland" , "road weather" , "weather camera" , "image reference" ],
85+ "documents" : [
86+ {"role" : "http://dbpedia.org/resource/Web_page" , "name" : "Digitraffic Road Traffic" , "link" : {"href" : DIGITRAFFIC_HOME , "type" : "text/html" }},
87+ {"role" : "http://dbpedia.org/resource/Web_page" , "name" : "Digitraffic Weather Cameras" , "link" : {"href" : DIGITRAFFIC_WEATHERCAM_STATIONS , "type" : "application/json" }},
88+ {"role" : "http://dbpedia.org/resource/Web_page" , "name" : "Digitraffic Terms of Use" , "link" : {"href" : DIGITRAFFIC_LICENSE , "type" : "text/html" }},
89+ ],
90+ "contacts" : [
91+ {"role" : "operator" , "organisationName" : "Fintraffic / Digitraffic" , "contactInfo" : {"onlineResource" : {"linkage" : DIGITRAFFIC_HOME }}},
92+ {"role" : "publisher" , "organisationName" : "OS4CSAPI" , "contactInfo" : {"onlineResource" : {"linkage" : "https://github.com/OS4CSAPI/OSHConnect-Python" }}},
93+ ],
94+ }
95+
96+
97+ def _datastream_schema (camera : dict ) -> dict :
98+ preset_id = camera ["presetId" ]
99+ return {
100+ "uid" : _datastream_uid (camera ),
101+ "outputName" : DS_OUTPUT_NAME ,
102+ "name" : "Digitraffic Weather Camera Image" ,
103+ "description" : (
104+ f"Image-reference observations for Digitraffic camera preset { preset_id } "
105+ f"at { camera ['cameraStationName' ]} , attached to road-weather station "
106+ f"{ camera ['roadWeatherStationId' ]} ({ camera ['roadWeatherStationName' ]} )."
107+ ),
108+ "documentation" : [
109+ {"title" : "Weathercam Station Latest Data" , "href" : _station_data_url (camera ["cameraStationId" ]), "rel" : "service" },
110+ {"title" : "Latest JPEG" , "href" : _image_url (preset_id ), "rel" : "alternate" },
111+ {"title" : "Latest Thumbnail" , "href" : _thumb_url (preset_id ), "rel" : "preview" },
112+ {"title" : "Digitraffic Terms of Use" , "href" : DIGITRAFFIC_LICENSE , "rel" : "license" },
113+ ],
114+ "schema" : {
115+ "obsFormat" : "application/om+json" ,
116+ "resultSchema" : {
117+ "type" : "DataRecord" ,
118+ "label" : "Digitraffic Weather Camera Image Reference" ,
119+ "fields" : [
120+ {"type" : "Text" , "name" : "stationId" , "label" : "Road Weather Station ID" , "definition" : "http://sensorml.com/ont/swe/property/StationID" },
121+ {"type" : "Text" , "name" : "camId" , "label" : "Weather Camera Preset ID" , "definition" : "http://sensorml.com/ont/swe/property/SensorID" },
122+ {"type" : "Text" , "name" : "imageUrl" , "label" : "Full-Size Image URL" , "definition" : "http://www.opengis.net/def/property/OGC/0/ImageURL" },
123+ {"type" : "Text" , "name" : "thumbUrl" , "label" : "Thumbnail Image URL" , "definition" : "http://www.opengis.net/def/property/OGC/0/ImageURL" },
124+ {"type" : "Text" , "name" : "latestImageUrl" , "label" : "Latest Image URL" , "definition" : "http://www.opengis.net/def/property/OGC/0/ImageURL" },
125+ {"type" : "Text" , "name" : "mediaType" , "label" : "Media Type" , "definition" : "http://purl.org/dc/elements/1.1/format" },
126+ {"type" : "Text" , "name" : "sourceUrl" , "label" : "Source URL" , "definition" : "http://sensorml.com/ont/swe/property/ReferenceURL" },
127+ ],
128+ },
129+ },
130+ }
131+
132+
133+ def _deploy_root () -> dict :
134+ return {"type" : "Feature" , "geometry" : {"type" : "Point" , "coordinates" : [25.0 , 63.5 ]}, "properties" : {"uid" : DEPLOY_ROOT_UID , "featureType" : "sosa:Deployment" , "name" : "Digitraffic Weather Camera Demo" , "description" : "Top-level grouping for curated Finnish road-weather camera image-reference resources." , "validTime" : [VALID_TIME_START , ".." ]}}
135+
136+
137+ def _deploy_group () -> dict :
138+ return {"type" : "Feature" , "geometry" : {"type" : "Point" , "coordinates" : [25.0 , 63.5 ]}, "properties" : {"uid" : DEPLOY_GROUP_UID , "featureType" : "sosa:Deployment" , "name" : "Digitraffic Weather Camera Presets" , "description" : "Grouping deployment for curated Fintraffic Digitraffic weather camera presets." , "validTime" : [VALID_TIME_START , ".." ]}}
139+
140+
141+ def _deploy_camera (camera : dict , system_server_id : str , base_url : str ) -> dict :
142+ return {
143+ "type" : "Feature" ,
144+ "geometry" : {"type" : "Point" , "coordinates" : [camera ["lon" ], camera ["lat" ]]},
145+ "properties" : {
146+ "uid" : _deploy_uid (camera ),
147+ "featureType" : "sosa:Deployment" ,
148+ "name" : f"Digitraffic Weather Camera { camera ['cameraStationName' ]} { camera ['presetId' ]} " ,
149+ "description" : (
150+ f"Weather camera preset { camera ['presetId' ]} attached to Finnish road-weather "
151+ f"station { camera ['roadWeatherStationId' ]} ({ camera ['roadWeatherStationName' ]} )."
152+ ),
153+ "validTime" : [VALID_TIME_START , ".." ],
154+ "platform@link" : {"href" : f"{ base_url .rstrip ('/' )} /systems/{ system_server_id } " , "uid" : _system_uid (camera ["roadWeatherStationId" ]), "title" : f"Digitraffic Road Weather { camera ['roadWeatherStationName' ]} " },
155+ },
156+ }
157+
158+
159+ def clean_all (base_url : str , auth : str , * , dry_run : bool , stats : dict ):
160+ for camera in _load_cameras ():
161+ clean_resource (base_url , auth , "deployments" , _deploy_uid (camera ), dry_run = dry_run , stats = stats , cascade = True )
162+ clean_resource (base_url , auth , "deployments" , DEPLOY_GROUP_UID , dry_run = dry_run , stats = stats , cascade = True )
163+ clean_resource (base_url , auth , "deployments" , DEPLOY_ROOT_UID , dry_run = dry_run , stats = stats , cascade = True )
164+ clean_resource (base_url , auth , "procedures" , PROC_UID , dry_run = dry_run , stats = stats )
165+
166+
167+ def bootstrap (* , clean : bool = False , clean_only : bool = False , dry_run : bool = False , force_sml : bool = False ):
168+ config = get_config ()
169+ base_url = config ["base_url" ]
170+ auth = _auth_header (config ["user" ], config ["password" ])
171+ cameras = _load_cameras ()
172+ stats : dict [str , int ] = {}
173+
174+ print ("\n " + "=" * 70 )
175+ print (" Digitraffic Weathercam -- Bootstrap" )
176+ print ("=" * 70 )
177+ print (f" Server: { base_url } " )
178+ print (f" Cameras: { len (cameras )} " )
179+ print (f" Clean: { clean } Clean-only: { clean_only } Dry-run: { dry_run } Force-SML: { force_sml } \n " )
180+
181+ if clean or clean_only :
182+ print (" -- Cleaning existing resources --" )
183+ clean_all (base_url , auth , dry_run = dry_run , stats = stats )
184+ if clean_only :
185+ print_summary (stats , dry_run )
186+ return
187+
188+ print (" -- Procedure --" )
189+ ensure_procedure (base_url , auth , PROC_UID , PROCEDURE_STUB , PROCEDURE_SML , dry_run = dry_run , stats = stats , force_sml = force_sml )
190+
191+ print (" -- Companion Datastreams --" )
192+ system_ids : dict [str , str ] = {}
193+ for camera in cameras :
194+ station_id = camera ["roadWeatherStationId" ]
195+ sys_id = find_by_uid (base_url , auth , "systems" , _system_uid (station_id ), no_cache = True )
196+ if not sys_id and not dry_run :
197+ print (f" [WARN] Missing road-weather system for station { station_id } ; skipping camera { camera ['presetId' ]} " )
198+ continue
199+ system_ids [station_id ] = sys_id or "pending"
200+ ensure_datastream (base_url , auth , sys_id or "pending" , DS_OUTPUT_NAME , _datastream_schema (camera ), dry_run = dry_run , stats = stats )
201+
202+ print (" -- Deployments --" )
203+ root_id = ensure_deployment (base_url , auth , DEPLOY_ROOT_UID , _deploy_root (), dry_run = dry_run , stats = stats )
204+ group_id = ensure_deployment (base_url , auth , DEPLOY_GROUP_UID , _deploy_group (), parent_id = root_id , dry_run = dry_run , stats = stats )
205+ for camera in cameras :
206+ station_id = camera ["roadWeatherStationId" ]
207+ sys_id = system_ids .get (station_id )
208+ if not sys_id and not dry_run :
209+ continue
210+ ensure_deployment (base_url , auth , _deploy_uid (camera ), _deploy_camera (camera , sys_id or "pending" , base_url ), parent_id = group_id , dry_run = dry_run , stats = stats )
211+
212+ print_summary (stats , dry_run )
213+
214+
215+ def main ():
216+ parser = argparse .ArgumentParser (description = "Bootstrap Digitraffic Weathercam resources on the CSAPI server." )
217+ add_bootstrap_args (parser )
218+ args = parser .parse_args ()
219+ bootstrap (clean = args .clean , clean_only = args .clean_only , dry_run = args .dry_run , force_sml = args .force_sml )
220+
221+
222+ if __name__ == "__main__" :
223+ main ()
0 commit comments