mirror of
https://github.com/weewx/weewx.git
synced 2026-06-13 11:35:19 -04:00
246 lines
12 KiB
Python
246 lines
12 KiB
Python
#
|
|
# Copyright (c) 2009-2015 Tom Keffer <tkeffer@gmail.com>
|
|
#
|
|
# See the file LICENSE.txt for your full rights.
|
|
#
|
|
"""Generate images for up to an effective date.
|
|
Needs to be refactored into smaller functions."""
|
|
|
|
from __future__ import with_statement
|
|
import time
|
|
import datetime
|
|
import syslog
|
|
import os.path
|
|
|
|
import weeplot.genplot
|
|
import weeplot.utilities
|
|
import weeutil.weeutil
|
|
import weewx.reportengine
|
|
import weewx.units
|
|
from weeutil.weeutil import to_bool, to_int, to_float
|
|
|
|
#===============================================================================
|
|
# Class ImageGenerator
|
|
#===============================================================================
|
|
|
|
class ImageGenerator(weewx.reportengine.ReportGenerator):
|
|
"""Class for managing the image generator."""
|
|
|
|
def run(self):
|
|
|
|
self.setup()
|
|
|
|
# Generate any images
|
|
self.genImages(self.gen_ts)
|
|
|
|
def setup(self):
|
|
|
|
self.image_dict = self.skin_dict['ImageGenerator']
|
|
self.title_dict = self.skin_dict.get('Labels', {}).get('Generic', {})
|
|
self.formatter = weewx.units.Formatter.fromSkinDict(self.skin_dict)
|
|
self.converter = weewx.units.Converter.fromSkinDict(self.skin_dict)
|
|
|
|
def genImages(self, gen_ts):
|
|
"""Generate the images.
|
|
|
|
The time scales will be chosen to include the given timestamp, with nice beginning
|
|
and ending times.
|
|
|
|
gen_ts: The time around which plots are to be generated. This will also be used as
|
|
the bottom label in the plots. [optional. Default is to use the time of the last record
|
|
in the database.]
|
|
"""
|
|
t1 = time.time()
|
|
ngen = 0
|
|
|
|
# Loop over each time span class (day, week, month, etc.):
|
|
for timespan in self.image_dict.sections :
|
|
|
|
# Now, loop over all plot names in this time span class:
|
|
for plotname in self.image_dict[timespan].sections :
|
|
|
|
# Accumulate all options from parent nodes:
|
|
plot_options = weeutil.weeutil.accumulateLeaves(self.image_dict[timespan][plotname])
|
|
|
|
plotgen_ts = gen_ts
|
|
if not plotgen_ts:
|
|
binding = plot_options['data_binding']
|
|
archive = self.db_binder.get_manager(binding)
|
|
plotgen_ts = archive.lastGoodStamp()
|
|
if not plotgen_ts:
|
|
plotgen_ts = time.time()
|
|
|
|
image_root = os.path.join(self.config_dict['WEEWX_ROOT'], plot_options['HTML_ROOT'])
|
|
# Get the path of the file that the image is going to be saved to:
|
|
img_file = os.path.join(image_root, '%s.png' % plotname)
|
|
|
|
# Check whether this plot needs to be done at all:
|
|
ai = plot_options.as_int('aggregate_interval') if plot_options.has_key('aggregate_interval') else None
|
|
if skipThisPlot(plotgen_ts, ai, img_file) :
|
|
continue
|
|
|
|
# Create the subdirectory that the image is to be put in.
|
|
# Wrap in a try block in case it already exists.
|
|
try:
|
|
os.makedirs(os.path.dirname(img_file))
|
|
except OSError:
|
|
pass
|
|
|
|
# Create a new instance of a time plot and start adding to it
|
|
plot = weeplot.genplot.TimePlot(plot_options)
|
|
|
|
# Calculate a suitable min, max time for the requested time span and set it
|
|
(minstamp, maxstamp, timeinc) = weeplot.utilities.scaletime(plotgen_ts - int(plot_options.get('time_length', 86400)), plotgen_ts)
|
|
plot.setXScaling((minstamp, maxstamp, timeinc))
|
|
|
|
# Set the y-scaling, using any user-supplied hints:
|
|
plot.setYScaling(weeutil.weeutil.convertToFloat(plot_options.get('yscale', ['None', 'None', 'None'])))
|
|
|
|
# Get a suitable bottom label:
|
|
bottom_label_format = plot_options.get('bottom_label_format', '%m/%d/%y %H:%M')
|
|
bottom_label = time.strftime(bottom_label_format, time.localtime(plotgen_ts))
|
|
plot.setBottomLabel(bottom_label)
|
|
|
|
# Set day/night display
|
|
plot.setLocation(self.stn_info.latitude_f, self.stn_info.longitude_f)
|
|
plot.setDayNight(to_bool(plot_options.get('show_daynight', False)),
|
|
weeplot.utilities.tobgr(plot_options.get('daynight_day_color', '0xffffff')),
|
|
weeplot.utilities.tobgr(plot_options.get('daynight_night_color', '0xf0f0f0')),
|
|
weeplot.utilities.tobgr(plot_options.get('daynight_edge_color', '0xefefef')))
|
|
|
|
# Loop over each line to be added to the plot.
|
|
for line_name in self.image_dict[timespan][plotname].sections:
|
|
|
|
# Accumulate options from parent nodes.
|
|
line_options = weeutil.weeutil.accumulateLeaves(self.image_dict[timespan][plotname][line_name])
|
|
|
|
# See what SQL variable type to use for this line. By default,
|
|
# use the section name.
|
|
var_type = line_options.get('data_type', line_name)
|
|
|
|
# Look for aggregation type:
|
|
aggregate_type = line_options.get('aggregate_type')
|
|
if aggregate_type in (None, '', 'None', 'none'):
|
|
# No aggregation specified.
|
|
aggregate_type = aggregate_interval = None
|
|
else :
|
|
try:
|
|
# Aggregation specified. Get the interval.
|
|
aggregate_interval = line_options.as_int('aggregate_interval')
|
|
except KeyError:
|
|
syslog.syslog(syslog.LOG_ERR, "genimages: aggregate interval required for aggregate type %s" % aggregate_type)
|
|
syslog.syslog(syslog.LOG_ERR, "genimages: line type %s skipped" % var_type)
|
|
continue
|
|
|
|
# Now we have everything we need to find and hit the database:
|
|
binding = line_options['data_binding']
|
|
archive = self.db_binder.get_manager(binding)
|
|
(start_vec_t, stop_vec_t, data_vec_t) = \
|
|
archive.getSqlVectors((minstamp, maxstamp), var_type, aggregate_type=aggregate_type,
|
|
aggregate_interval=aggregate_interval)
|
|
|
|
if weewx.debug:
|
|
assert(len(start_vec_t) == len(stop_vec_t))
|
|
|
|
# Do any necessary unit conversions:
|
|
new_start_vec_t = self.converter.convert(start_vec_t)
|
|
new_stop_vec_t = self.converter.convert(stop_vec_t)
|
|
new_data_vec_t = self.converter.convert(data_vec_t)
|
|
|
|
# Add a unit label. NB: all will get overwritten except the last.
|
|
# Get the label from the configuration dictionary.
|
|
# TODO: Allow multiple unit labels, one for each plot line?
|
|
unit_label = line_options.get('y_label', weewx.units.get_label_string(self.formatter, self.converter, var_type))
|
|
# Strip off any leading and trailing whitespace so it's easy to center
|
|
plot.setUnitLabel(unit_label.strip())
|
|
|
|
# See if a line label has been explicitly requested:
|
|
label = line_options.get('label')
|
|
if not label:
|
|
# No explicit label. Is there a generic one?
|
|
# If not, then the SQL type will be used instead
|
|
label = self.title_dict.get(var_type, var_type)
|
|
|
|
# See if a color has been explicitly requested.
|
|
color = line_options.get('color')
|
|
if color is not None: color = weeplot.utilities.tobgr(color)
|
|
|
|
# Get the line width, if explicitly requested.
|
|
width = to_int(line_options.get('width'))
|
|
|
|
# Get the type of plot ("bar', 'line', or 'vector')
|
|
plot_type = line_options.get('plot_type', 'line')
|
|
|
|
interval_vec = None
|
|
|
|
# Some plot types require special treatments:
|
|
if plot_type == 'vector':
|
|
vector_rotate_str = line_options.get('vector_rotate')
|
|
vector_rotate = -float(vector_rotate_str) if vector_rotate_str is not None else None
|
|
else:
|
|
vector_rotate = None
|
|
|
|
gap_fraction = None
|
|
if plot_type == 'bar':
|
|
interval_vec = [x[1] - x[0]for x in zip(new_start_vec_t.value, new_stop_vec_t.value)]
|
|
elif plot_type == 'line':
|
|
gap_fraction = to_float(line_options.get('line_gap_fraction'))
|
|
if gap_fraction is not None:
|
|
if not 0 < gap_fraction < 1:
|
|
syslog.syslog(syslog.LOG_ERR, "genimages: Gap fraction %5.3f outside range 0 to 1. Ignored." % gap_fraction)
|
|
gap_fraction = None
|
|
|
|
# Get the type of line ('solid' or 'none' is all that's offered now)
|
|
line_type = line_options.get('line_type', 'solid')
|
|
if line_type.strip().lower() in ['', 'none']:
|
|
line_type = None
|
|
|
|
marker_type = line_options.get('marker_type')
|
|
marker_size = to_int(line_options.get('marker_size', 8))
|
|
|
|
# Add the line to the emerging plot:
|
|
plot.addLine(weeplot.genplot.PlotLine(new_stop_vec_t[0], new_data_vec_t[0],
|
|
label = label,
|
|
color = color,
|
|
width = width,
|
|
plot_type = plot_type,
|
|
line_type = line_type,
|
|
marker_type = marker_type,
|
|
marker_size = marker_size,
|
|
bar_width = interval_vec,
|
|
vector_rotate = vector_rotate,
|
|
gap_fraction = gap_fraction))
|
|
|
|
# OK, the plot is ready. Render it onto an image
|
|
image = plot.render()
|
|
|
|
try:
|
|
# Now save the image
|
|
image.save(img_file)
|
|
ngen += 1
|
|
except IOError, e:
|
|
syslog.syslog(syslog.LOG_CRIT, "genimages: Unable to save to file '%s' %s:" % (img_file, e))
|
|
t2 = time.time()
|
|
|
|
syslog.syslog(syslog.LOG_INFO, "genimages: Generated %d images for %s in %.2f seconds" % (ngen, self.skin_dict['REPORT_NAME'], t2 - t1))
|
|
|
|
def skipThisPlot(time_ts, aggregate_interval, img_file):
|
|
"""A plot can be skipped if it was generated recently and has not changed.
|
|
This happens if the time since the plot was generated is less than the
|
|
aggregation interval."""
|
|
|
|
# Images without an aggregation interval have to be plotted every time.
|
|
# Also, the image definitely has to be generated if it doesn't exist.
|
|
if aggregate_interval is None or not os.path.exists(img_file):
|
|
return False
|
|
|
|
# If its a very old image, then it has to be regenerated
|
|
if time_ts - os.stat(img_file).st_mtime >= aggregate_interval:
|
|
return False
|
|
|
|
# Finally, if we're on an aggregation boundary, regenerate.
|
|
time_dt = datetime.datetime.fromtimestamp(time_ts)
|
|
tdiff = time_dt - time_dt.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
return abs(tdiff.seconds % aggregate_interval) > 1
|
|
|