import datetime import os import stat from django.core.exceptions import ImproperlyConfigured import gheat import gheat.opacity from gheat.models import Accespoint, Gebruiker, Meting from gheat import gheatsettings as settings from gheat import gmerc from gheat import BUILD_EMPTIES, DIRMODE, SIZE, log class ColorScheme(object): """Base class for color scheme representations. """ def __init__(self, name, fspath): """Takes the name and filesystem path of the defining PNG. """ # if aspen.mode.DEVDEB: # aspen.restarter.track(fspath) self.hook_set(fspath) self.empties_dir = os.path.join(settings.GHEAT_MEDIA_ROOT, name, 'empties') self.build_empties() def build_empties(self): """Build empty tiles for this color scheme. """ empties_dir = self.empties_dir if not BUILD_EMPTIES: log.info("not building empty tiles for %s " % self) else: if not os.path.isdir(empties_dir): os.makedirs(empties_dir, DIRMODE) if not os.access(empties_dir, os.R_OK|os.W_OK|os.X_OK): raise ImproperlyConfigured( "Permissions too restrictive on " + "empties directory " + "(%s)." % empties_dir ) for fname in os.listdir(empties_dir): if fname.endswith('.png'): os.remove(os.path.join(empties_dir, fname)) for zoom, opacity in gheat.opacity.zoom_to_opacity.items(): fspath = os.path.join(empties_dir, str(zoom)+'.png') self.hook_build_empty(opacity, fspath) log.info("building empty tiles in %s" % empties_dir) def get_empty_fspath(self, zoom): fspath = os.path.join(self.empties_dir, str(zoom)+'.png') if not os.path.isfile(fspath): self.build_empties() # so we can rebuild empties on the fly return fspath def hook_set(self): """Set things that your backend will want later. """ raise NotImplementedError def hook_build_empty(self, opacity, fspath): """Given an opacity and a path, save an empty tile. """ raise NotImplementedError class Dot(object): """Base class for dot representations. Unlike color scheme, the same basic external API works for both backends. How we compute that API is different, though. """ def __init__(self, zoom): """Takes a zoom level. """ name = 'dot%d.png' % zoom fspath = os.path.join(settings.GHEAT_CONF_DIR, 'dots', name) self.img, self.half_size = self.hook_get(fspath) def hook_get(self, fspath): """Given a filesystem path, return two items. """ raise NotImplementedError class Tile(object): """Base class for tile representations. """ img = None def __init__(self, color_scheme, dots, zoom, x, y, fspath): """x and y are tile coords per Google Maps. """ # Calculate some things. # ====================== dot = dots[zoom] # Translate tile to pixel coords. # ------------------------------- x1 = x * SIZE x2 = x1 + 255 y1 = y * SIZE y2 = y1 + 255 # Expand bounds by one-half dot width. # ------------------------------------ x1 = x1 - dot.half_size x2 = x2 + dot.half_size y1 = y1 - dot.half_size y2 = y2 + dot.half_size expanded_size = (x2-x1, y2-y1) # Translate new pixel bounds to lat/lng. # -------------------------------------- n, w = gmerc.px2ll(x1, y1, zoom) s, e = gmerc.px2ll(x2, y2, zoom) # Save # ==== self.dot = dot.img self.pad = dot.half_size self.x = x self.y = y self.x1 = x1 self.y1 = y1 self.x2 = x2 self.y2 = y2 self.expanded_size = expanded_size self.llbound = (n,s,e,w) self.zoom = zoom self.fspath = fspath self.opacity = gheat.opacity.zoom_to_opacity[zoom] self.color_scheme = color_scheme def is_empty(self): """With attributes set on self, return a boolean. Calc lat/lng bounds of this tile (include half-dot-width of padding) SELECT count(uid) FROM points """ numpoints = Meting.objects.num_points(self) return numpoints == 0 def is_stale(self): """With attributes set on self, return a boolean. Calc lat/lng bounds of this tile (include half-dot-width of padding) SELECT count(uid) FROM points WHERE modtime < modtime_tile """ if not os.path.isfile(self.fspath): return True timestamp = os.stat(self.fspath)[stat.ST_MTIME] datum = datetime.datetime.fromtimestamp(timestamp) numpoints = Meting.objects.num_points(self, datum) return numpoints > 0 def rebuild(self): """Rebuild the image at self.img. Real work delegated to subclasses. """ # Calculate points. # ================= # Build a closure that gives us the x,y pixel coords of the points # to be included on this tile, relative to the top-left of the tile. _points = Meting.objects.points_inside(self) def points(): """Yield x,y pixel coords within this tile, top-left of dot. """ result = [] for point in _points: x, y = gmerc.ll2px(point.latitude, point.longitude, self.zoom) x = x - self.x1 # account for tile offset relative to y = y - self.y1 # overall map point_signaal = point.signaal while point_signaal > 0: result.append((x-self.pad,y-self.pad)) point_signaal = point_signaal - 1 return result # Main logic # ========== # Hand off to the subclass to actually build the image, then come back # here to maybe create a directory before handing back to the backend # to actually write to disk. self.img = self.hook_rebuild(points()) dirpath = os.path.dirname(self.fspath) if dirpath and not os.path.isdir(dirpath): os.makedirs(dirpath, DIRMODE) def hook_rebuild(self, points, opacity): """Rebuild and save the file using the current library. The algorithm runs something like this: o start a tile canvas/image that is a dots-worth oversized o loop through points and multiply dots on the tile o trim back down to straight tile size o invert/colorize the image o make it transparent Return the img object; it will be sent back to hook_save after a directory is made if needed. Trim after looping because we multiply is the only step that needs the extra information. The coloring and inverting can happen in the same pixel manipulation because you can invert colors.png. That is a 1px by 256px PNG that maps grayscale values to color values. You can customize that file to change the coloration. """ raise NotImplementedError def save(self): """Write the image at self.img to disk. """ raise NotImplementedError