-
Notifications
You must be signed in to change notification settings - Fork 0
/
radar
executable file
·165 lines (140 loc) · 8.49 KB
/
radar
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
#!/usr/bin/python3
# rain radar w/o 2GB of GUI browser - TWB (Trent W. Buck @ Linux Users Victoria ~ September 2020)
# PROBLEM:
# * http://bom.gov.au is 90s style and very hard to use.
# It also requires js, and is not SSL so COMPLETELY UNSAFE.
# * http://m.bom.gov.au provided a GOOD option with two simple pages:
# 1. a full-screen rain radar for the last 15 minutes.
# This tells me the heavy rainstorm is just starting, or just finishing.
# This in turn tells me whether to leave now or wait 15 minutes.
# 2. For my station only (Melbourne CBD),
# * min/max temp, rain chance forecasts for the rest of today
# * severe weather warnings, in big letters, with a solid yellow background.
# * m.bom.gov.au was shut down and replaced by an "app" that requires ios or dalvik.
# * the backend data is now commercialized, so open source alternative interfaces to this data CANNOT EXIST ANYMORE.
# i.e. bom.gov.au is trying to buck the govhack trend and make Australian weather MORE like American weather -- expensive and exclusive.
#
# So let's try to solve the most immediate problem: merge the rain GIF overlay onto the static map PNGs, as the javascript does.
# Then render that into a static png or webm, and view it.
# usage: radar [tmpdir1] [tmpdir2] [movie player and arguments]
import pwd
import os
import tempfile
import pathlib
import datetime
import subprocess
import shutil
import sys
import re
import time, stat
underlays = ('background', 'locations', 'range', 'topography')
primaries = ('IDR021', 'IDR023')
secondaries = ('IDR011', 'IDR013')
current = primaries
def age(pathname):
return time.time() - os.stat(pathname)[stat.ST_MTIME]
def radar(prefix,minute_offset_file,tempdir_path):
now = datetime.datetime.utcnow() # NOTE: TZ=UTC
try:
with open(minute_offset_file, 'r') as offsetfile:
minute_offset = int(offsetfile.read())
except:
minute_offset = 0
new_offset=0
while new_offset < 5:
print (datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
proc = subprocess.run(args = [
# if reg becomes blocked, could fallback to `lynx -source`, but then we have to manage the forks ourselves
'puf', '-vvv', '-nc', '-U', 'Yes, keep trying to block those user agents BoM. It''s not like you''re the public service', '-P', tempdir_path,
# Try to get a rain image for every minute in the last hour.
# 1 in every 5 or 6 will work. For now, just ignore that some fail.
*[then.strftime('http://reg.bom.gov.au/radar/'+prefix+'.T.%Y%m%d%H%M.png')
for i in range(60)
for then in [(now - datetime.timedelta(minutes=i))]
if (then.minute - minute_offset - new_offset) % 5 == 0], # was 6, should now be 5
# The underlay images (NOTE: hardcode station to MEL (IDR021) for now).
*[f'http://reg.bom.gov.au/products/radar_transparencies/{prefix}.{u}.png'
for u in underlays]], universal_newlines = True, stderr = subprocess.PIPE)
# for a complete non-match, you'd expect 12/12
# failures for 5 minute intervals in an hour. Set
# threshold to require 4 successes?
print ("proc.stderr = ", proc.stderr, datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), ": \"not found\" len = ", len(re.findall('not found', proc.stderr)))
if len(re.findall('not found', proc.stderr)) < 8:
# successfully downloaded a set of images at offset minute_offset + new_offset
with open(minute_offset_file, 'w') as offsetfile:
offsetfile.write(str(minute_offset+new_offset))
break
new_offset += 1
# remove all but the last 12 images from each category
subprocess.check_call(f"ls -tr {tempdir_path}/*.T.????????????.png 2>/dev/null | head -n -12 | xargs --no-run-if-empty rm --verbose", shell=True)
subprocess.check_call(f"ls -tr {tempdir_path}/*.T.????????????.1.png 2>/dev/null | head -n -12 | xargs --no-run-if-empty rm --verbose", shell=True)
subprocess.check_call(f"ls -tr {tempdir_path}/*.T.????????????.2.png 2>/dev/null | head -n -12 | xargs --no-run-if-empty rm --verbose", shell=True)
subprocess.check_call(f"ls -tr {tempdir_path}/*.T.????????????.3.png 2>/dev/null | head -n -12 | xargs --no-run-if-empty rm --verbose", shell=True)
subprocess.check_call(f"ls -tr {tempdir_path}/*.T.????????????.4.png 2>/dev/null | head -n -12 | xargs --no-run-if-empty rm --verbose", shell=True)
regenerate = False
# Merge the underlays and overlays into combined images.
# We have to do this a bit at a time, because reasons.
# For now just to the map overlay and skip land/sea &c
for overlay_path in tempdir_path.glob(prefix+'*.T.????????????.png'):
if not pathlib.Path(overlay_path.with_suffix(f'.4.png')).exists():
subprocess.check_call(['gm', 'composite', overlay_path, tempdir_path / f'{prefix}.locations.png', overlay_path.with_suffix(f'.1.png')])
subprocess.check_call(['gm', 'composite', overlay_path.with_suffix(f'.1.png'), tempdir_path / f'{prefix}.range.png', overlay_path.with_suffix(f'.2.png')])
subprocess.check_call(['gm', 'composite', overlay_path.with_suffix(f'.2.png'), tempdir_path / f'{prefix}.topography.png', overlay_path.with_suffix(f'.3.png')])
subprocess.check_call(['gm', 'composite', overlay_path.with_suffix(f'.3.png'), tempdir_path / f'{prefix}.background.png', overlay_path.with_suffix(f'.4.png')])
print("Regenerated:", overlay_path.with_suffix(f'.4.png'))
regenerate = True
# Convert to an animated gif.
# FIXME: currently bugged -- it's not wiping the transparent cells between frames.
# When I add in the all-opaque underlay that should fix it.
if regenerate:
subprocess.check_call(['gm', 'convert',
'-delay', '15', # 1/6s per frame
'-loop', '0', # loop forever
*sorted(tempdir_path.glob(prefix+'*.T.????????????.4.png')),
tempdir_path / f'{prefix}.final.gif'])
print("Regenerated:", tempdir_path / f'{prefix}.final.gif')
else:
print("No need to regenerate",overlay_path.with_suffix(f'.4.png'), "or", tempdir_path / f'{prefix}.final.gif')
args=()
if len(sys.argv) != 1:
args=sys.argv[1:] # shift
if ( args[0] == "--failover" ):
# failover=true
current = secondaries
args = args[1:] # shift
provided_paths = False
if len(args) > 1:
tempdir1_path = pathlib.Path(args.pop(0))
tempdir2_path = pathlib.Path(args.pop(0))
pathlib.Path(tempdir1_path).mkdir(parents=True, exist_ok=True)
pathlib.Path(tempdir2_path).mkdir(parents=True, exist_ok=True)
provided_paths = True
else:
tempdir1_path = pathlib.Path(tempfile.mkdtemp(prefix="radar1."))
tempdir2_path = pathlib.Path(tempfile.mkdtemp(prefix="radar2."))
print(f"path1={tempdir1_path},path2={tempdir2_path}")
if 0 == os.fork():
radar(current[0], '/tmp/radar1.offset', tempdir1_path)
os._exit(0)
else:
radar(current[1], '/tmp/radar2.offset', tempdir2_path)
os.wait()
try:
if ((age(tempdir1_path / (current[0]+'.final.gif')) < 1800) and
(age(tempdir2_path / (current[1]+'.final.gif')) < 1800)):
subprocess.check_call(['ffmpeg', '-i', tempdir1_path / (current[0]+'.final.gif'), '-i', tempdir2_path / (current[1]+'.final.gif'), '-filter_complex', 'hstack', '-y', tempdir2_path / '.tmp.output.mp4'])
os.rename(tempdir2_path / '.tmp.output.mp4', tempdir2_path / 'output.mp4')
# if the user didn't supply any args, run `mpv -loop $filename`. If
# they did supply extra args, we invoke `<those args> $filename`
if len(args) == 0:
args=['mpv', '-loop']
args+=[tempdir2_path / 'output.mp4']
print("Argument List:", str(args))
subprocess.check_call(args)
else:
raise RuntimeError("No output was generated probably because not enough input files")
except Exception as e:
if not provided_paths:
shutil.rmtree(tempdir1_path)
shutil.rmtree(tempdir2_path)
raise