mirror of
				https://github.com/ciphervance/supercell-wx.git
				synced 2025-10-31 19:20:05 +00:00 
			
		
		
		
	added scwx-qt/tools/update_radar_sites.py to update radar sites from NOAA HOMR data
This commit is contained in:
		
							parent
							
								
									af25556a09
								
							
						
					
					
						commit
						507292385d
					
				
					 1 changed files with 281 additions and 0 deletions
				
			
		
							
								
								
									
										281
									
								
								scwx-qt/tools/update_radar_sites.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										281
									
								
								scwx-qt/tools/update_radar_sites.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,281 @@ | |||
| #!/usr/bin/env python3 | ||||
| import requests | ||||
| import json | ||||
| import argparse | ||||
| 
 | ||||
| NOAA_BASE = "http://www.ncdc.noaa.gov/homr/services/station" | ||||
| 
 | ||||
| # Get the noaa station data. | ||||
| # platform is what platform should be searched for | ||||
| # current (should) filter to only current stations (always is filtered) | ||||
| # icao is a ICAO identifier. Without it we search for all. | ||||
| def get_noaa_stations(platform, current, icao = None): | ||||
|     params = { | ||||
|         "definitions":  "false", | ||||
|         "phrData":      "false", | ||||
|     } | ||||
|     if platform is not None: | ||||
|         params["platform"] = platform | ||||
|     if current: | ||||
|         params["current"] = "true" | ||||
|     if icao is not None: | ||||
|         params["qid"] = f"ICAO:{icao}" | ||||
| 
 | ||||
|     res = requests.get(NOAA_BASE + "/search", params = params) | ||||
| 
 | ||||
|     if res.ok: | ||||
|         return res.json()["stationCollection"]["stations"] | ||||
|     else: | ||||
|         print("NETWORK ERROR: Could not get resources from NOAA HOMR") | ||||
|         print(res.text) | ||||
|         exit(5) | ||||
| 
 | ||||
| # dictionary to convert NOAA types to Supercell_wx types | ||||
| NOAA_TYPE_DICT = { | ||||
|     "TDWR":     "tdwr", | ||||
|     "NEXRAD":  "wsr88d" | ||||
| } | ||||
| 
 | ||||
| # Given an list of objects, find the object with the best value for key. | ||||
| # The values that appear earlier in values are better. | ||||
| # subKey will take return the value under that key, not the full object. | ||||
| # parser (needs subKey) is a function that will have the value found by | ||||
| #   subKey and return a parsed version of it (often 'float' because HOMR | ||||
| #   data uses strings for floats) | ||||
| def extract_best(items, key, values, subKey = None, parser = None): | ||||
|     valuesPart = enumerate(reversed(values)) | ||||
|     valueDict = dict([(k,v) for v,k in valuesPart]) | ||||
|     best = None | ||||
|     bestInd = -1 | ||||
| 
 | ||||
|     for item in items: | ||||
|         index = valueDict.get(item[key], -1) | ||||
|         if bestInd is None or bestInd < index: | ||||
|             bestInd = index | ||||
|             best = item | ||||
| 
 | ||||
| 
 | ||||
|     if subKey is None or best is None: | ||||
|         return best | ||||
| 
 | ||||
|     if parser is not None: | ||||
|         return parser(best[subKey]) | ||||
| 
 | ||||
|     return best[subKey] | ||||
| 
 | ||||
| def make_noaa_stations_dict(noaaStations): | ||||
|     output = {} | ||||
| 
 | ||||
|     for station in noaaStations: | ||||
|         stationId = extract_best(station["identifiers"], "idType", ["NEXRAD", "ICAO"], "id") | ||||
| 
 | ||||
|         if stationId in output: # some stations are repeaded in non NEXRAD/TDWR locations. | ||||
|             continue | ||||
| 
 | ||||
|         stationDict = {} | ||||
|         stationDict["lat"]   = float(station["header"]["latitude_dec"]) | ||||
|         stationDict["lon"]   = float(station["header"]["longitude_dec"]) | ||||
|         stationDict["elevation"] = extract_best(station["location"].get("elevations", []), | ||||
|                                                 "elevationType", | ||||
|                                                 ["GROUND"], | ||||
|                                                 "elevationFeet", | ||||
|                                                 float) | ||||
|         # These are some things that could be updated from the NOAA HOMR data, | ||||
|         # but are not necessary in the same format, so they are disabled. | ||||
|         """ | ||||
|         stationDict["id"] = stationId | ||||
|         stationDict["country"] = station["location"]["geoInfo"]["countries"][0]["country"] | ||||
|         if "stateProvinces" in station["location"]["geoInfo"]: | ||||
|             stationDict["state"]   = station["location"]["geoInfo"]["stateProvinces"][0]["stateProvince"] | ||||
|         else: | ||||
|             stationDict["state"] = None | ||||
|         stationDict["place"]   = extract_best(station["names"], "nameType", ["PRINCIPAL"], "name") | ||||
|         stationDict["type"]    = NOAA_TYPE_DICT[station["platforms"][0]["platform"]] | ||||
|         #stationDict["tz"]      = station["location"]["geoInfo"]["utcOffsets"][0]["utcOffset"] # This is UTC offset, not timezone | ||||
|         """ | ||||
| 
 | ||||
|         output[stationId] = stationDict | ||||
| 
 | ||||
|     return output | ||||
| 
 | ||||
| # Get the list of updated stations (not in place), using the noaaStationsDict | ||||
| # from make_noaa_stations_dict | ||||
| def update_stations(noaaStationsDict, previousStations): | ||||
|     newStations = [] | ||||
|     for station in previousStations: | ||||
|         newStation = station.copy() | ||||
| 
 | ||||
|         if not station["id"] in noaaStationsDict: | ||||
|             # may be good idea to add fallback to a ICAO search for non active | ||||
|             # stations. | ||||
|             print(f"WARNING: Station '{station['id']}' not found in noaa data") | ||||
| 
 | ||||
|             if "elevation" not in station: | ||||
|                 newStation["elevation"] = None | ||||
|         else: | ||||
|             newStation.update(noaaStationsDict[station["id"]]) | ||||
| 
 | ||||
|         newStations.append(newStation) | ||||
| 
 | ||||
|     return newStations | ||||
| 
 | ||||
| # Customized dump routine. Formats it as one station per row, aligning items. | ||||
| def custom_dump(stations, file): | ||||
|     file.write("[\n") | ||||
|     lengths = {} | ||||
|     lastKey = None | ||||
|     keys = None | ||||
| 
 | ||||
|     # Find length for each value, and ensure all stations have the same keys. | ||||
|     for station in stations: | ||||
|         for key, value in station.items(): | ||||
|             length = len(json.dumps(value)) | ||||
|             if key in lengths: | ||||
|                 lengths[key] = max(length, lengths[key]) | ||||
|             else: | ||||
|                 lengths[key] = length | ||||
|             lastKey = key | ||||
| 
 | ||||
|         newKeys = list(station.keys()) | ||||
|         if keys is None: | ||||
|             keys = newKeys | ||||
|         elif keys != newKeys: | ||||
|             print("DUMP ERROR: Stations did not have the same keys.") | ||||
|             exit(3) | ||||
| 
 | ||||
|     # Write out each station with the correct format. | ||||
|     lastType = None | ||||
|     for station in stations: | ||||
|         # put an empty line between NEXRAD and TDWR. | ||||
|         if lastType is not None and lastType != station["type"]: | ||||
|             file.write("\n") | ||||
| 
 | ||||
|         file.write("\t{ ") | ||||
| 
 | ||||
|         for key, value in station.items(): | ||||
|             value = json.dumps(value) | ||||
|             file.write(f'"{key}": {value}') | ||||
| 
 | ||||
|             if key != lastKey: | ||||
|                 file.write(", ") | ||||
|                 file.write(" " * (lengths[key] - len(value))) | ||||
| 
 | ||||
|         if station == stations[-1]: | ||||
|             file.write(" }\n") | ||||
|         else: | ||||
|             file.write(" },\n") | ||||
| 
 | ||||
|         lastType = station["type"] | ||||
| 
 | ||||
|     file.write("]\n") | ||||
| 
 | ||||
| # Write coordinates out to a file. Useful for checking against map program. | ||||
| def make_coords(stations, file): | ||||
|     for station in stations: | ||||
|         lat = str(abs(station["lat"])) | ||||
|         lat += "N" if station["lat"] > 0 else "S" | ||||
| 
 | ||||
|         lon = str(abs(station["lon"])) | ||||
|         lon += "E" if station["lon"] > 0 else "W" | ||||
| 
 | ||||
|         file.write(f"{lat} {lon}\n") | ||||
| 
 | ||||
| def main(): | ||||
|     parser = argparse.ArgumentParser( | ||||
|             description="""Update supercell-wx's location data for towers form NOAA's HOMR database.\n | ||||
|             Recommended Arguments: -u ../res/config/radar_sites.json -t""") | ||||
|     parser.add_argument("--current_file", "-u", type = str, default = None, required = False, | ||||
|                         help = "The 'radar_sites.json' file to update. Without this option, this will generate a new file") | ||||
|     parser.add_argument("--test_updated", "-t", default = False, action = "store_true", | ||||
|                         help = "Read in the updated file to ensure it is valid JSON. Should be used.") | ||||
|     parser.add_argument("--updated_file", "-o", type = str, default = None, required = False, | ||||
|                         help = "The updated 'radar_sites.json' file. The default is to overwrite the current one.") | ||||
|     parser.add_argument("--coord_file", "-c", type = str, default = None, required = False, | ||||
|                         help = "Output an additional file with the coordinates of each site.") | ||||
|     parser.add_argument("--resp_file", "-r", type = str, default = None, required = False, | ||||
|                         help = "Output most of the JSON from the responses.") | ||||
|     parser.add_argument("--input_json", "-i", type = str, default = None, required = False, | ||||
|                         help = "Instead of querying NOAA, just read in a JSON file made by \"-r\".") | ||||
|     parser.add_argument("--json_dump", "-j", default = False, action = "store_true", | ||||
|                         help = "Uses 'json.dump' instead of the custom dump function. Has worse formatting.") | ||||
|     parser.add_argument("--more_radars", "-m", default = False, action = "store_true", | ||||
|                         help = "Get AWOS and UPPERAIR stations as well. Should NOT be used.") | ||||
|     parser.add_argument("--current_only", "-C", default = False, action = "store_true", | ||||
|                         help = "Get only currently active stations. Does not seem to change anything.") | ||||
| 
 | ||||
|     args = parser.parse_args() | ||||
|     # default to updating the same file as input | ||||
|     if args.updated_file is None: | ||||
|         if args.current_file is None: | ||||
|             parser.error("Needs 'current_file' or 'updated_file'") | ||||
|         args.updated_file = args.current_file | ||||
| 
 | ||||
|     previousStations = None | ||||
|     if args.current_file is not None: | ||||
|         print(f"Reading Current Sites from '{args.current_file}'") | ||||
|         with open(args.current_file, "r") as file: | ||||
|             previousStations = json.load(file) | ||||
| 
 | ||||
|     if args.input_json is None: | ||||
|         print("Getting NEXRAD stations") | ||||
|         noaaStations = get_noaa_stations("NEXRAD", args.current_only) | ||||
| 
 | ||||
|         print("Getting TDWR stations") | ||||
|         noaaStations += get_noaa_stations("TDWR", args.current_only) | ||||
| 
 | ||||
|         if args.more_radars: # Should not be used | ||||
|             print("Getting AWOS stations") | ||||
|             noaaStations += get_noaa_stations("AWOS", args.current_only) | ||||
| 
 | ||||
|             print("Getting UPPERAIR stations") | ||||
|             noaaStations += get_noaa_stations("UPPERAIR", args.current_only) | ||||
|     else: | ||||
|         with open(args.input_json, "r") as file: | ||||
|             noaaStations = json.load(file) | ||||
| 
 | ||||
|     if args.resp_file is not None: | ||||
|         with open(args.resp_file, "w") as file: | ||||
|             json.dump(noaaStations, file, indent=4) | ||||
| 
 | ||||
|     print("Processing Data") | ||||
|     noaaStationsDict = make_noaa_stations_dict(noaaStations) | ||||
| 
 | ||||
|     if args.current_file is None: | ||||
|         newStations = list(noaaStationsDict.values()) | ||||
|     else: | ||||
|         newStations = update_stations(noaaStationsDict, previousStations) | ||||
| 
 | ||||
|     print(f"Saving Updated Sites to '{args.updated_file}'") | ||||
|     with open(args.updated_file, "w") as file: | ||||
|         if args.json_dump: | ||||
|             json.dump(newStation, file) | ||||
|         else: | ||||
|             custom_dump(newStations, file) | ||||
| 
 | ||||
|     if args.coord_file is not None: | ||||
|         print(f"Saving Coordinates to '{args.coord_file}'") | ||||
|         with open(args.coord_file, "w") as file: | ||||
|             make_coords(newStations, file) | ||||
| 
 | ||||
|     if args.test_updated: | ||||
|         failed = False | ||||
|         with open(args.updated_file, "r") as file: | ||||
|             try: | ||||
|                 data = json.load(file) | ||||
|                 if len(data) < len(newStations): | ||||
|                     print(f"TEST ERROR: Only read in {len(data)} out of {len(newStations)} items.") | ||||
|                     failed = True | ||||
|                 if json.dumps(data) != json.dumps(newStations): | ||||
|                     print(f"TEST ERROR: Dumps are not equal") | ||||
|                     failed = True | ||||
|             except Exception as e: | ||||
|                 print(e) | ||||
|                 failed = True | ||||
| 
 | ||||
|             if failed: | ||||
|                 exit(4) | ||||
| 
 | ||||
| if __name__ == "__main__": | ||||
|     main() | ||||
| 
 | ||||
| 
 | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 AdenKoperczak
						AdenKoperczak