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