source: src/django_gheat/gheat/base.py@ 9819

Last change on this file since 9819 was 9819, checked in by rick, 13 years ago

Correct annoying typo in naming of the acces*s* point.

File size: 7.7 KB
Line 
1import datetime
2import os
3import stat
4
5from django.core.exceptions import ImproperlyConfigured
6
7import gheat
8import gheat.opacity
9from gheat.models import Accesspoint, Gebruiker, Meting
10from gheat import gheatsettings as settings
11from gheat import gmerc
12from gheat import BUILD_EMPTIES, DIRMODE, SIZE, log
13
14
15class ColorScheme(object):
16 """Base class for color scheme representations.
17 """
18
19 def __init__(self, name, fspath):
20 """Takes the name and filesystem path of the defining PNG.
21 """
22# if aspen.mode.DEVDEB:
23# aspen.restarter.track(fspath)
24 self.hook_set(fspath)
25 self.empties_dir = os.path.join(settings.GHEAT_MEDIA_ROOT, name, 'empties')
26 self.build_empties()
27
28
29 def build_empties(self):
30 """Build empty tiles for this color scheme.
31 """
32 empties_dir = self.empties_dir
33
34 if not BUILD_EMPTIES:
35 log.info("not building empty tiles for %s " % self)
36 else:
37 if not os.path.isdir(empties_dir):
38 os.makedirs(empties_dir, DIRMODE)
39 if not os.access(empties_dir, os.R_OK|os.W_OK|os.X_OK):
40 raise ImproperlyConfigured( "Permissions too restrictive on "
41 + "empties directory "
42 + "(%s)." % empties_dir
43 )
44 for fname in os.listdir(empties_dir):
45 if fname.endswith('.png'):
46 os.remove(os.path.join(empties_dir, fname))
47 for zoom, opacity in gheat.opacity.zoom_to_opacity.items():
48 fspath = os.path.join(empties_dir, str(zoom)+'.png')
49 self.hook_build_empty(opacity, fspath)
50
51 log.info("building empty tiles in %s" % empties_dir)
52
53
54 def get_empty_fspath(self, zoom):
55 fspath = os.path.join(self.empties_dir, str(zoom)+'.png')
56 if not os.path.isfile(fspath):
57 self.build_empties() # so we can rebuild empties on the fly
58 return fspath
59
60
61 def hook_set(self):
62 """Set things that your backend will want later.
63 """
64 raise NotImplementedError
65
66
67 def hook_build_empty(self, opacity, fspath):
68 """Given an opacity and a path, save an empty tile.
69 """
70 raise NotImplementedError
71
72
73class Dot(object):
74 """Base class for dot representations.
75
76 Unlike color scheme, the same basic external API works for both backends.
77 How we compute that API is different, though.
78
79 """
80
81 def __init__(self, zoom):
82 """Takes a zoom level.
83 """
84 name = 'dot%d.png' % zoom
85 fspath = os.path.join(settings.GHEAT_CONF_DIR, 'dots', name)
86 self.img, self.half_size = self.hook_get(fspath)
87
88 def hook_get(self, fspath):
89 """Given a filesystem path, return two items.
90 """
91 raise NotImplementedError
92
93
94class Tile(object):
95 """Base class for tile representations.
96 """
97
98 img = None
99
100 def __init__(self, color_scheme, dots, zoom, x, y, fspath):
101 """x and y are tile coords per Google Maps.
102 """
103
104 # Calculate some things.
105 # ======================
106
107 dot = dots[zoom]
108
109
110 # Translate tile to pixel coords.
111 # -------------------------------
112
113 x1 = x * SIZE
114 x2 = x1 + 255
115 y1 = y * SIZE
116 y2 = y1 + 255
117
118
119 # Expand bounds by one-half dot width.
120 # ------------------------------------
121
122 x1 = x1 - dot.half_size
123 x2 = x2 + dot.half_size
124 y1 = y1 - dot.half_size
125 y2 = y2 + dot.half_size
126 expanded_size = (x2-x1, y2-y1)
127
128
129 # Translate new pixel bounds to lat/lng.
130 # --------------------------------------
131
132 n, w = gmerc.px2ll(x1, y1, zoom)
133 s, e = gmerc.px2ll(x2, y2, zoom)
134
135
136 # Save
137 # ====
138
139 self.dot = dot.img
140 self.pad = dot.half_size
141
142 self.x = x
143 self.y = y
144
145 self.x1 = x1
146 self.y1 = y1
147
148 self.x2 = x2
149 self.y2 = y2
150
151 self.expanded_size = expanded_size
152 self.llbound = (n,s,e,w)
153 self.zoom = zoom
154 self.fspath = fspath
155 self.opacity = gheat.opacity.zoom_to_opacity[zoom]
156 self.color_scheme = color_scheme
157
158
159 def is_empty(self):
160 """With attributes set on self, return a boolean.
161
162 Calc lat/lng bounds of this tile (include half-dot-width of padding)
163 SELECT count(uid) FROM points
164 """
165 numpoints = Meting.objects.num_points(self)
166 return numpoints == 0
167
168
169 def is_stale(self):
170 """With attributes set on self, return a boolean.
171
172 Calc lat/lng bounds of this tile (include half-dot-width of padding)
173 SELECT count(uid) FROM points WHERE modtime < modtime_tile
174 """
175 if not os.path.isfile(self.fspath):
176 return True
177
178 timestamp = os.stat(self.fspath)[stat.ST_MTIME]
179 datum = datetime.datetime.fromtimestamp(timestamp)
180
181 numpoints = Meting.objects.num_points(self, datum)
182
183 return numpoints > 0
184
185
186 def rebuild(self):
187 """Rebuild the image at self.img. Real work delegated to subclasses.
188 """
189
190 # Calculate points.
191 # =================
192 # Build a closure that gives us the x,y pixel coords of the points
193 # to be included on this tile, relative to the top-left of the tile.
194
195 _points = Meting.objects.points_inside(self)
196
197 def points():
198 """Yield x,y pixel coords within this tile, top-left of dot.
199 """
200 result = []
201 for point in _points:
202 x, y = gmerc.ll2px(point.latitude, point.longitude, self.zoom)
203 x = x - self.x1 # account for tile offset relative to
204 y = y - self.y1 # overall map
205 point_signaal = point.signaal
206 while point_signaal > 0:
207 result.append((x-self.pad,y-self.pad))
208 point_signaal = point_signaal - 1
209 return result
210
211
212 # Main logic
213 # ==========
214 # Hand off to the subclass to actually build the image, then come back
215 # here to maybe create a directory before handing back to the backend
216 # to actually write to disk.
217
218 self.img = self.hook_rebuild(points())
219
220 dirpath = os.path.dirname(self.fspath)
221 if dirpath and not os.path.isdir(dirpath):
222 os.makedirs(dirpath, DIRMODE)
223
224
225 def hook_rebuild(self, points, opacity):
226 """Rebuild and save the file using the current library.
227
228 The algorithm runs something like this:
229
230 o start a tile canvas/image that is a dots-worth oversized
231 o loop through points and multiply dots on the tile
232 o trim back down to straight tile size
233 o invert/colorize the image
234 o make it transparent
235
236 Return the img object; it will be sent back to hook_save after a
237 directory is made if needed.
238
239 Trim after looping because we multiply is the only step that needs the
240 extra information.
241
242 The coloring and inverting can happen in the same pixel manipulation
243 because you can invert colors.png. That is a 1px by 256px PNG that maps
244 grayscale values to color values. You can customize that file to change
245 the coloration.
246
247 """
248 raise NotImplementedError
249
250
251 def save(self):
252 """Write the image at self.img to disk.
253 """
254 raise NotImplementedError
255
256
Note: See TracBrowser for help on using the repository browser.