py2ME/radar/TWCRadarProcessor.py
2025-01-28 00:58:23 -06:00

331 lines
12 KiB
Python

import asyncio
import collections
from genericpath import exists
import gzip
from multiprocessing import Pool
import aiohttp
import json
import time as epochTime
import requests
import logging,coloredlogs
from os import path, mkdir, listdir, remove, cpu_count
from shutil import rmtree
from PIL import Image as PILImage
from wand.image import Image as wandImage
from wand.color import Color
radarType = "Radar-US"
l = logging.getLogger(__name__)
coloredlogs.install()
upperLeftX,upperLeftY,lowerRightX,lowerRightY = 0,0,0,0
xStart,xEnd,yStart,yEnd = 0,0,0,0
imgW = 0
imgH = 0
import sys
sys.path.append("./py2lib")
sys.path.append("./radar")
from RadarProcessor import *
import bit
async def getValidTimestamps(boundaries:ImageBoundaries) -> list:
"""Gets all valid UNIX timestamps for the TWCRadarMosaic product """
l.info("Getting timestamps for the radar..")
times = []
async with aiohttp.ClientSession() as session:
url = "https://api.weather.com/v3/TileServer/series/productSet?apiKey=" + cfg[twcApiKey] + "&filter=twcRadarMosaic"
async with session.get(url) as r:
response = await r.json()
for t in range(0, len(response['seriesInfo']['twcRadarMosaic']['series'])):
if (t <= 35):
time = response['seriesInfo']['twcRadarMosaic']['series'][t]['ts']
# Don't add frames that aren't at the correct interval
if (time % boundaries.ImageInterval != 0):
l.debug(f"Ignoring {time} -- Not at the correct frame interval.")
continue
# Don't add frames that are expired
if (time < (datetime.utcnow().timestamp() - epochTime.time()) / 1000 - boundaries.Expiration):
l.debug(f"Ignoring {time} -- Expired.")
continue
times.append(time)
return times
def downloadRadarTile(url, p, fn):
img = requests.get(url, stream=True)
ts = fn.split("_")[0]
download = True
# Make the path if it doesn't exist
if exists(f"./.temp/tiles/output/{ts}.tiff"):
l.debug("Not downloading tiles for timestamp " + str(ts) + " since a frame for it already exists." )
download = False
if not path.exists(p):
mkdir(p)
l.debug(f"Download {ts}")
if exists(f"{p}/{fn}"):
l.debug(f"Not downloading new tiles for {ts} as they already exist.")
download = False
if (img.status_code == 200 and download):
with open(f'{p}/{fn}', 'wb') as tile:
for data in img:
tile.write(data)
elif (img.status_code != 200):
l.error("ERROR DOWNLOADING " + p + "\nSTATUS CODE " + str(img.status_code))
elif (download == False):
pass
def getImageBoundaries() -> ImageBoundaries:
""" Gets the image boundaries for the specified radar definition """
with open('radar/ImageSequenceDefs.json', 'r') as f:
ImageSequenceDefs = json.loads(f.read())
seqDef = ImageSequenceDefs['ImageSequenceDefs'][radarType]
return ImageBoundaries(
LowerLeftLong = seqDef['LowerLeftLong'],
LowerLeftLat= seqDef['LowerLeftLat'],
UpperRightLong= seqDef['UpperRightLong'],
UpperRightLat= seqDef['UpperRightLat'],
VerticalAdjustment= seqDef['VerticalAdjustment'],
OGImgW= seqDef['OriginalImageWidth'],
OGImgH= seqDef['OriginalImageHeight'],
ImagesInterval= seqDef['ImagesInterval'],
Expiration= seqDef['Expiration']
)
def CalculateBounds(upperRight:LatLong, lowerLeft:LatLong, upperLeft:LatLong, lowerRight: LatLong):
""" Calculates the image bounds for radar stitching & tile downloading """
upperRightTile:Point = WorldCoordinateToTile(LatLongProject(upperRight.x, upperRight.y))
lowerLeftTile:Point = WorldCoordinateToTile(LatLongProject(lowerLeft.x, lowerLeft.y))
upperLeftTile:Point = WorldCoordinateToTile(LatLongProject(upperLeft.x, upperLeft.y))
lowerRightTile:Point = WorldCoordinateToTile(LatLongProject(lowerRight.x,lowerRight.y))
upperLeftPx:Point = WorldCoordinateToPixel(LatLongProject(upperLeft.x, upperLeft.y))
lowerRightPx:Point = WorldCoordinateToPixel(LatLongProject(lowerRight.x,lowerRight.y))
global upperLeftX,upperLeftY,lowerRightX,lowerRightY
global xStart,xEnd,yStart,yEnd
global imgW,imgH
upperLeftX = upperLeftPx.x - upperLeftTile.x * 256
upperLeftY = upperLeftPx.y - upperLeftTile.y * 256
lowerRightX = lowerRightPx.x - upperLeftTile.x * 256
lowerRightY = lowerRightPx.y - upperLeftTile.y * 256
# Set the xStart, xEnd, yStart, and yEnd positions so we can download tiles that are within the tile coordinate regions
xStart = int(upperLeftTile.x)
xEnd = int(upperRightTile.x)
yStart = int(upperLeftTile.y)
yEnd = int(lowerLeftTile.y)
# Set the image width & height based off the x and y tile amounts
# These should amount to the amount of tiles needed to be downloaded
# for both the x and y coordinates.
xTiles:int = xEnd - xStart
yTiles:int = yEnd - yStart
imgW = 256 * (xTiles + 1)
imgH = 256 * (yTiles + 1)
print(f"{imgW} x {imgH}")
def convertPaletteToWXPro(filepath:str):
""" Converts the color palette of a radar frame to one acceptable to the i2 """
img = wandImage(filename = filepath)
rainColors = [
Color('rgb(64,204,85'), # lightest green
Color('rgb(0,153,0'), # med green
Color('rgb(0,102,0)'), # darkest green
Color('rgb(191,204,85)'), # yellow
Color('rgb(191,153,0)'), # orange
Color('rgb(255,51,0)'), # ...
Color('rgb(191,51,0)'), # red
Color('rgb(64,0,0)') # dark red
]
mixColors = [
Color('rgb(253,130,215)'), # light purple
Color('rgb(208,94,176)'), # ...
Color('rgb(190,70,150)'), # ...
Color('rgb(170,50,130)') # dark purple
]
snowColors = [
Color('rgb(150,150,150)'), # dark grey
Color('rgb(180,180,180)'), # light grey
Color('rgb(210,210,210)'), # grey
Color('rgb(230,230,230)') # white
]
# Replace rain colors
img.opaque_paint(Color('rgb(99, 235, 99)'), rainColors[0], 7000.0)
img.opaque_paint(Color('rgb(28,158,52)'), rainColors[1], 7000.0)
img.opaque_paint(Color('rgb(0, 63, 0)'), rainColors[2], 7000.0)
img.opaque_paint(Color('rgb(251,235,2)'), rainColors[3], 7000.0)
img.opaque_paint(Color('rgb(238, 109, 2)'), rainColors[4], 7000.0)
img.opaque_paint(Color('rgb(210,11,6)'), rainColors[5], 7000.0)
img.opaque_paint(Color('rgb(169,5,3)'), rainColors[6], 7000.0)
img.opaque_paint(Color('rgb(128,0,0)'), rainColors[7], 7000.0)
# Replace mix colors
img.opaque_paint(Color('rgb(255,160,207)'), mixColors[0], 7000.0)
img.opaque_paint(Color('rgb(217,110,163)'), mixColors[1], 7000.0)
img.opaque_paint(Color('rgb(192,77,134)'), mixColors[2], 7000.0)
img.opaque_paint(Color('rgb(174,51,112)'), mixColors[3], 7000.0)
img.opaque_paint(Color('rgb(146,13,79)'), mixColors[3], 7000.0)
# Replace snow colors
img.opaque_paint(Color('rgb(138,248,255)'), snowColors[0], 7000.0)
img.opaque_paint(Color('rgb(110,203,212)'), snowColors[1], 7000.0)
img.opaque_paint(Color('rgb(82,159,170)'), snowColors[2], 7000.0)
img.opaque_paint(Color('rgb(40,93,106)'), snowColors[3], 7000.0)
img.opaque_paint(Color('rgb(13,49,64)'), snowColors[3]), 7000.0
img.compression = 'lzw'
img.save(filename=filepath)
def getTime(timestamp) -> str:
time:datetime = datetime.utcfromtimestamp(timestamp).strftime("%m/%d/%Y %H:%M:%S")
return str(time)
async def makeRadarImages():
""" Creates proper radar frames for the i2 """
l.info("Downloading frames for the Regional Radar...")
combinedCoordinates = []
boundaries = getImageBoundaries()
upperRight:LatLong = boundaries.GetUpperRight()
lowerLeft:LatLong = boundaries.GetLowerLeft()
upperLeft:LatLong = boundaries.GetUpperLeft()
lowerRight:LatLong = boundaries.GetLowerRight()
CalculateBounds(upperRight, lowerLeft, upperLeft, lowerRight)
times = await getValidTimestamps(boundaries)
# Get rid of invalid radar frames
for i in listdir('./.temp/tiles/output'):
if i.split('.')[0] not in [str(x) for x in times] and i != "Thumbs.db":
l.debug(f"Deleting {i} as it is no longer valid.")
remove("./.temp/tiles/output/" + i)
# Collect coordinates for the frame tiles
for y in range(yStart, yEnd):
if y <= yEnd:
for x in range(xStart, xEnd):
if x <= xEnd:
combinedCoordinates.append(Point(x,y))
# Create urls, paths, and filenames to download tiles for.
urls = []
paths = []
filenames = []
for i in range(0, len(times)):
for c in range(0, len(combinedCoordinates)):
if not exists(f'./.temp/tiles/output/{times[i]}.tiff'):
urls.append(f"https://api.weather.com/v3/TileServer/tile?product=twcRadarMosaic&ts={str(times[i])}&xyz={combinedCoordinates[c].x}:{combinedCoordinates[c].y}:6&apiKey={cfg[twcApiKey]}")
paths.append(f"./.temp/tiles/{times[i]}")
filenames.append(f"{times[i]}_{combinedCoordinates[c].x}_{combinedCoordinates[c].y}.png")
l.debug(len(urls))
if len(urls) != 0 and len(urls) >= 6:
with Pool(cpu_count() - 1) as p:
p.starmap(downloadRadarTile, zip(urls, paths, filenames))
p.close()
p.join()
elif len(urls) < 6 and len(urls) != 0: # We don't need to run more threads than we need to, that's how we get halted.
with Pool(len(urls)) as p:
p.starmap(downloadRadarTile, zip(urls, paths, filenames))
p.close()
p.join()
elif len(urls) == 0:
l.info("No new radar frames need to be downloaded.")
return
# Stitch them all together!
imgsToGenerate = []
framesToComposite = []
finished = []
files = []
for t in times:
imgsToGenerate.append(PILImage.new("RGB", (imgW, imgH)))
# Stitch the frames together
for i in range(0, len(imgsToGenerate)):
if not exists(F"./.temp/tiles/output/{times[i]}.tiff"):
l.debug(f"Generate frame for {times[i]}")
for c in combinedCoordinates:
path = f"./.temp/tiles/{times[i]}/{times[i]}_{c.x}_{c.y}.png"
xPlacement = (c.x - xStart) * 256
yPlacement = (c.y - yStart) * 256
placeTile = PILImage.open(path)
imgsToGenerate[i].paste(placeTile, (xPlacement, yPlacement))
# Don't render it with an alpha channel
imgsToGenerate[i].save(f"./.temp/tiles/output/{times[i]}.tiff", compression = 'tiff_lzw')
framesToComposite.append(f"./.temp/tiles/output/{times[i]}.tiff") # Store the path so we can composite it using WAND and PIL
# Remove the tileset as we don't need it anymore!
rmtree(f'./.temp/tiles/{times[i]}')
# Composite images for the i2
imgsProcessed = 0
for img in framesToComposite:
imgsProcessed += 1
l.debug("Attempting to composite " + img)
l.info(f"Processing radar frame {imgsProcessed} / 36")
# Crop the radar images something that the i2 will actually take
img_raw = wandImage(filename=img)
img_raw.crop(upperLeftX, upperLeftY, width = int(lowerRightX - upperLeftX), height = int(lowerRightY - upperLeftY))
img_raw.compression = 'lzw'
img_raw.save(filename=img)
# Resize using PIL
imgPIL = PILImage.open(img)
imgPIL = imgPIL.resize((boundaries.OGImgW, boundaries.OGImgH), 0)
imgPIL.save(img)
convertPaletteToWXPro(img)
finished.append(img)
commands = []
# Send them all to the i2!
for i in range(0, len(finished)):
commands.append( '<MSG><Exec workRequest="storePriorityImage(FileExtension=.tiff,File={0},Location=US,ImageType=Radar,IssueTime=' + getTime(times[i]) + ')"/></MSG>' )
# print(file + "\n" + command)
bit.sendFile([finished[i]], [commands[i]], 1, 0)
l.info("Downloaded and sent Regional Radar frames!")
if __name__ == "__main__":
asyncio.run(makeRadarImages())