331 lines
12 KiB
Python
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()) |