author | Tero Marttila <terom@fixme.fi> |
Mon, 25 Jan 2010 04:59:38 +0200 | |
changeset 89 | 02e5b9b08881 |
parent 47 | 201257cbd887 |
child 90 | 1c317e0628a7 |
permissions | -rw-r--r-- |
30 | 1 |
""" |
2 |
Our WSGI web interface, which can serve the JS UI and any .png tiles via HTTP. |
|
3 |
""" |
|
4 |
||
5 |
from werkzeug import Request, Response, responder |
|
6 |
from werkzeug import exceptions |
|
42
a5bca7b0cd8a
get DATA_ROOT from os.environ, fix use of prefix for dir view
Tero Marttila <terom@fixme.fi>
parents:
40
diff
changeset
|
7 |
import os.path, os |
30 | 8 |
|
9 |
import pypngtile as pt |
|
10 |
||
89 | 11 |
## Settings |
12 |
# path to images |
|
42
a5bca7b0cd8a
get DATA_ROOT from os.environ, fix use of prefix for dir view
Tero Marttila <terom@fixme.fi>
parents:
40
diff
changeset
|
13 |
DATA_ROOT = os.environ.get("PNGTILE_DATA_PATH") or os.path.abspath('data/') |
30 | 14 |
|
89 | 15 |
# only open each image once |
30 | 16 |
IMAGE_CACHE = {} |
17 |
||
89 | 18 |
# width of a tile |
30 | 19 |
TILE_WIDTH = 256 |
20 |
TILE_HEIGHT = 256 |
|
21 |
||
43 | 22 |
# max. output resolution to allow |
23 |
MAX_PIXELS = 1920 * 1200 |
|
24 |
||
89 | 25 |
def dir_url (prefix, name, item) : |
26 |
""" |
|
27 |
Join together an absolute URL prefix, an optional directory name, and a directory item |
|
28 |
""" |
|
29 |
||
30 |
url = prefix |
|
31 |
||
32 |
if name : |
|
33 |
url = os.path.join(url, name) |
|
34 |
||
35 |
url = os.path.join(url, item) |
|
36 |
||
37 |
return url |
|
38 |
||
39 |
def dir_list (dir_path) : |
|
40 |
""" |
|
41 |
Yield a series of directory items to show for the given dir |
|
42 |
""" |
|
43 |
||
44 |
# link to parent |
|
45 |
yield '..' |
|
46 |
||
47 |
for item in os.listdir(dir_path) : |
|
48 |
path = os.path.join(dir_path, item) |
|
49 |
||
50 |
# skip dotfiles |
|
51 |
if item.startswith('.') : |
|
52 |
continue |
|
53 |
||
54 |
# show dirs |
|
55 |
if os.path.isdir(path) : |
|
56 |
yield item |
|
57 |
||
58 |
# examine ext |
|
59 |
base, ext = os.path.splitext(path) |
|
60 |
||
61 |
# show .png files with a .cache file |
|
62 |
if ext == '.png' and os.path.exists(base + '.cache') : |
|
63 |
yield item |
|
64 |
||
65 |
### Render HTML data |
|
66 |
def render_dir (req, name, path) : |
|
67 |
""" |
|
68 |
Directory index |
|
69 |
""" |
|
70 |
||
31
7eec7486a0af
dir index view, report if image not cached
Tero Marttila <terom@fixme.fi>
parents:
30
diff
changeset
|
71 |
prefix = os.path.dirname(req.script_root).rstrip('/') |
42
a5bca7b0cd8a
get DATA_ROOT from os.environ, fix use of prefix for dir view
Tero Marttila <terom@fixme.fi>
parents:
40
diff
changeset
|
72 |
script_prefix = req.script_root |
31
7eec7486a0af
dir index view, report if image not cached
Tero Marttila <terom@fixme.fi>
parents:
30
diff
changeset
|
73 |
name = name.rstrip('/') |
7eec7486a0af
dir index view, report if image not cached
Tero Marttila <terom@fixme.fi>
parents:
30
diff
changeset
|
74 |
|
7eec7486a0af
dir index view, report if image not cached
Tero Marttila <terom@fixme.fi>
parents:
30
diff
changeset
|
75 |
return """\ |
7eec7486a0af
dir index view, report if image not cached
Tero Marttila <terom@fixme.fi>
parents:
30
diff
changeset
|
76 |
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"> |
7eec7486a0af
dir index view, report if image not cached
Tero Marttila <terom@fixme.fi>
parents:
30
diff
changeset
|
77 |
<head> |
7eec7486a0af
dir index view, report if image not cached
Tero Marttila <terom@fixme.fi>
parents:
30
diff
changeset
|
78 |
<title>Index of %(dir)s</title> |
7eec7486a0af
dir index view, report if image not cached
Tero Marttila <terom@fixme.fi>
parents:
30
diff
changeset
|
79 |
<link rel="Stylesheet" type="text/css" href="%(prefix)s/static/style.css"> |
7eec7486a0af
dir index view, report if image not cached
Tero Marttila <terom@fixme.fi>
parents:
30
diff
changeset
|
80 |
</head> |
7eec7486a0af
dir index view, report if image not cached
Tero Marttila <terom@fixme.fi>
parents:
30
diff
changeset
|
81 |
<body> |
7eec7486a0af
dir index view, report if image not cached
Tero Marttila <terom@fixme.fi>
parents:
30
diff
changeset
|
82 |
<h1>Index of %(dir)s</h1> |
7eec7486a0af
dir index view, report if image not cached
Tero Marttila <terom@fixme.fi>
parents:
30
diff
changeset
|
83 |
|
7eec7486a0af
dir index view, report if image not cached
Tero Marttila <terom@fixme.fi>
parents:
30
diff
changeset
|
84 |
<ul> |
7eec7486a0af
dir index view, report if image not cached
Tero Marttila <terom@fixme.fi>
parents:
30
diff
changeset
|
85 |
%(listing)s |
7eec7486a0af
dir index view, report if image not cached
Tero Marttila <terom@fixme.fi>
parents:
30
diff
changeset
|
86 |
</ul> |
7eec7486a0af
dir index view, report if image not cached
Tero Marttila <terom@fixme.fi>
parents:
30
diff
changeset
|
87 |
</body> |
7eec7486a0af
dir index view, report if image not cached
Tero Marttila <terom@fixme.fi>
parents:
30
diff
changeset
|
88 |
</html>""" % dict( |
7eec7486a0af
dir index view, report if image not cached
Tero Marttila <terom@fixme.fi>
parents:
30
diff
changeset
|
89 |
prefix = prefix, |
89 | 90 |
dir = '/' + name, |
31
7eec7486a0af
dir index view, report if image not cached
Tero Marttila <terom@fixme.fi>
parents:
30
diff
changeset
|
91 |
|
7eec7486a0af
dir index view, report if image not cached
Tero Marttila <terom@fixme.fi>
parents:
30
diff
changeset
|
92 |
listing = "\n".join( |
89 | 93 |
# <li> link |
31
7eec7486a0af
dir index view, report if image not cached
Tero Marttila <terom@fixme.fi>
parents:
30
diff
changeset
|
94 |
"""<li><a href="%(url)s">%(name)s</a></li>""" % dict( |
89 | 95 |
# URL to dir |
96 |
url = dir_url(script_prefix, name, item), |
|
97 |
||
98 |
# item name |
|
31
7eec7486a0af
dir index view, report if image not cached
Tero Marttila <terom@fixme.fi>
parents:
30
diff
changeset
|
99 |
name = item, |
89 | 100 |
) for item in dir_list(path) |
31
7eec7486a0af
dir index view, report if image not cached
Tero Marttila <terom@fixme.fi>
parents:
30
diff
changeset
|
101 |
), |
7eec7486a0af
dir index view, report if image not cached
Tero Marttila <terom@fixme.fi>
parents:
30
diff
changeset
|
102 |
) |
7eec7486a0af
dir index view, report if image not cached
Tero Marttila <terom@fixme.fi>
parents:
30
diff
changeset
|
103 |
|
89 | 104 |
def render_img_viewport (req, name, image) : |
105 |
""" |
|
106 |
HTML for image |
|
107 |
""" |
|
108 |
||
109 |
# a little slow, but not so bad - two stats(), heh |
|
110 |
info = image.info() |
|
111 |
img_width, img_height = info['img_width'], info['img_height'] |
|
43 | 112 |
|
30 | 113 |
return """\ |
114 |
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"> |
|
115 |
<head> |
|
116 |
<title>%(title)s</title> |
|
117 |
<script src="%(prefix)s/static/prototype.js" type="text/javascript"></script> |
|
118 |
<script src="%(prefix)s/static/dragdrop.js" type="text/javascript"></script> |
|
119 |
<script src="%(prefix)s/static/builder.js" type="text/javascript"></script> |
|
120 |
<script src="%(prefix)s/static/tiles2.js" type="text/javascript"></script> |
|
121 |
<link rel="Stylesheet" type="text/css" href="%(prefix)s/static/style.css"> |
|
122 |
</head> |
|
123 |
<body> |
|
124 |
<div id="wrapper"> |
|
125 |
<div id="viewport" style="width: 100%%; height: 100%%"> |
|
39 | 126 |
<div class="overlay"> |
127 |
<input type="button" id="btn-zoom-in" value="Zoom In" /> |
|
128 |
<input type="button" id="btn-zoom-out" value="Zoom Out" /> |
|
40 | 129 |
<a class="link" id="lnk-image" href="#"></a> |
39 | 130 |
</div> |
131 |
||
30 | 132 |
<div class="substrate"></div> |
47
201257cbd887
add a simple Loading... background
Tero Marttila <terom@fixme.fi>
parents:
44
diff
changeset
|
133 |
|
201257cbd887
add a simple Loading... background
Tero Marttila <terom@fixme.fi>
parents:
44
diff
changeset
|
134 |
<div class="background"> |
201257cbd887
add a simple Loading... background
Tero Marttila <terom@fixme.fi>
parents:
44
diff
changeset
|
135 |
Loading... |
201257cbd887
add a simple Loading... background
Tero Marttila <terom@fixme.fi>
parents:
44
diff
changeset
|
136 |
</div> |
30 | 137 |
</div> |
138 |
</div> |
|
139 |
||
140 |
<script type="text/javascript"> |
|
43 | 141 |
var tile_source = new Source("%(tile_url)s", %(tile_width)d, %(tile_height)d, -4, 0, %(img_width)d, %(img_height)d); |
30 | 142 |
var main = new Viewport(tile_source, "viewport"); |
143 |
</script> |
|
144 |
</body> |
|
145 |
</html>""" % dict( |
|
89 | 146 |
title = name, |
30 | 147 |
prefix = os.path.dirname(req.script_root).rstrip('/'), |
148 |
tile_url = req.url, |
|
149 |
||
150 |
tile_width = TILE_WIDTH, |
|
151 |
tile_height = TILE_HEIGHT, |
|
43 | 152 |
|
153 |
img_width = img_width, |
|
154 |
img_height = img_height, |
|
30 | 155 |
) |
156 |
||
34 | 157 |
def scale_by_zoom (val, zoom) : |
89 | 158 |
""" |
159 |
Scale coordinates by zoom factor |
|
160 |
""" |
|
161 |
||
34 | 162 |
if zoom > 0 : |
163 |
return val << zoom |
|
164 |
||
165 |
elif zoom > 0 : |
|
166 |
return val >> -zoom |
|
167 |
||
168 |
else : |
|
169 |
return val |
|
170 |
||
89 | 171 |
### Render PNG Data |
172 |
def render_img_tile (image, x, y, zoom, width=TILE_WIDTH, height=TILE_HEIGHT) : |
|
173 |
""" |
|
174 |
Render given tile, returning PNG data |
|
175 |
""" |
|
176 |
||
34 | 177 |
return image.tile_mem( |
40 | 178 |
width, height, |
34 | 179 |
scale_by_zoom(x, -zoom), scale_by_zoom(y, -zoom), |
180 |
zoom |
|
181 |
) |
|
30 | 182 |
|
89 | 183 |
def render_img_region (image, cx, cy, zoom, width, height) : |
184 |
""" |
|
185 |
Render arbitrary tile, returning PNG data |
|
186 |
""" |
|
187 |
||
40 | 188 |
x = scale_by_zoom(cx - width / 2, -zoom) |
189 |
y = scale_by_zoom(cy - height / 2, -zoom) |
|
190 |
||
43 | 191 |
# safely limit |
192 |
if width * height > MAX_PIXELS : |
|
193 |
raise exceptions.Forbidden("Image too large: %d * %d > %d" % (width, height, MAX_PIXELS)) |
|
194 |
||
40 | 195 |
return image.tile_mem( |
196 |
width, height, |
|
197 |
x, y, |
|
198 |
zoom |
|
43 | 199 |
) |
40 | 200 |
|
89 | 201 |
|
202 |
### Manipulate request data |
|
203 |
def get_req_path (req) : |
|
204 |
""" |
|
205 |
Returns the name and path requested |
|
206 |
""" |
|
207 |
||
30 | 208 |
# path to image |
31
7eec7486a0af
dir index view, report if image not cached
Tero Marttila <terom@fixme.fi>
parents:
30
diff
changeset
|
209 |
image_name = req.path.lstrip('/') |
30 | 210 |
|
211 |
# build absolute path |
|
31
7eec7486a0af
dir index view, report if image not cached
Tero Marttila <terom@fixme.fi>
parents:
30
diff
changeset
|
212 |
image_path = os.path.abspath(os.path.join(DATA_ROOT, image_name)) |
42
a5bca7b0cd8a
get DATA_ROOT from os.environ, fix use of prefix for dir view
Tero Marttila <terom@fixme.fi>
parents:
40
diff
changeset
|
213 |
|
30 | 214 |
# ensure the path points inside the data root |
215 |
if not image_path.startswith(DATA_ROOT) : |
|
31
7eec7486a0af
dir index view, report if image not cached
Tero Marttila <terom@fixme.fi>
parents:
30
diff
changeset
|
216 |
raise exceptions.NotFound(image_name) |
7eec7486a0af
dir index view, report if image not cached
Tero Marttila <terom@fixme.fi>
parents:
30
diff
changeset
|
217 |
|
89 | 218 |
return image_name, image_path |
31
7eec7486a0af
dir index view, report if image not cached
Tero Marttila <terom@fixme.fi>
parents:
30
diff
changeset
|
219 |
|
89 | 220 |
def get_image (name, path) : |
221 |
""" |
|
222 |
Gets an Image object from the cache, ensuring that it is cached |
|
223 |
""" |
|
31
7eec7486a0af
dir index view, report if image not cached
Tero Marttila <terom@fixme.fi>
parents:
30
diff
changeset
|
224 |
|
30 | 225 |
# get Image object |
89 | 226 |
if path in IMAGE_CACHE : |
30 | 227 |
# get from cache |
89 | 228 |
image = IMAGE_CACHE[path] |
30 | 229 |
|
230 |
else : |
|
89 | 231 |
# open |
232 |
image = pt.Image(path) |
|
233 |
||
234 |
# check |
|
235 |
if image.status() not in (pt.CACHE_FRESH, pt.CACHE_STALE) : |
|
236 |
raise exceptions.InternalServerError("Image cache not available: %s" % name) |
|
237 |
||
238 |
# load |
|
239 |
image.open() |
|
30 | 240 |
|
241 |
# cache |
|
89 | 242 |
IMAGE_CACHE[path] = image |
31
7eec7486a0af
dir index view, report if image not cached
Tero Marttila <terom@fixme.fi>
parents:
30
diff
changeset
|
243 |
|
89 | 244 |
return image |
245 |
||
246 |
||
247 |
||
248 |
### Handle request |
|
249 |
def handle_dir (req, name, path) : |
|
250 |
""" |
|
251 |
Handle request for a directory |
|
252 |
""" |
|
253 |
||
254 |
return Response(render_dir(req, name, path), content_type="text/html") |
|
255 |
||
256 |
||
257 |
||
258 |
def handle_img_viewport (req, image, name) : |
|
259 |
""" |
|
260 |
Handle request for image viewport |
|
261 |
""" |
|
262 |
||
263 |
# viewport |
|
264 |
return Response(render_img_viewport(req, name, image), content_type="text/html") |
|
265 |
||
266 |
||
267 |
def handle_img_region (req, image) : |
|
268 |
""" |
|
269 |
Handle request for an image region |
|
270 |
""" |
|
271 |
||
272 |
# specific image |
|
273 |
width = int(req.args['w']) |
|
274 |
height = int(req.args['h']) |
|
275 |
cx = int(req.args['cx']) |
|
276 |
cy = int(req.args['cy']) |
|
277 |
zoom = int(req.args.get('zl', "0")) |
|
278 |
||
279 |
# yay full render |
|
280 |
return Response(render_img_region(image, cx, cy, zoom, width, height), content_type="image/png") |
|
281 |
||
282 |
||
283 |
def handle_img_tile (req, image) : |
|
284 |
""" |
|
285 |
Handle request for image tile |
|
286 |
""" |
|
287 |
||
288 |
# tile |
|
289 |
x = int(req.args['x']) |
|
290 |
y = int(req.args['y']) |
|
291 |
zoom = int(req.args.get('zl', "0")) |
|
292 |
||
293 |
# yay render |
|
294 |
return Response(render_img_tile(image, x, y, zoom), content_type="image/png") |
|
295 |
||
296 |
## Dispatch req to handle_img_* |
|
297 |
def handle_img (req, name, path) : |
|
298 |
""" |
|
299 |
Handle request for an image |
|
300 |
""" |
|
301 |
||
302 |
# get image object |
|
303 |
image = get_image(name, path) |
|
30 | 304 |
|
305 |
# what view? |
|
306 |
if not req.args : |
|
89 | 307 |
return handle_img_viewport(req, image, name) |
30 | 308 |
|
40 | 309 |
elif 'w' in req.args and 'h' in req.args and 'cx' in req.args and 'cy' in req.args : |
89 | 310 |
return handle_img_region(req, image) |
40 | 311 |
|
30 | 312 |
elif 'x' in req.args and 'y' in req.args : |
89 | 313 |
return handle_img_tile(req, image) |
314 |
||
30 | 315 |
else : |
316 |
raise exceptions.BadRequest("Unknown args") |
|
89 | 317 |
|
318 |
||
319 |
||
320 |
## Dispatch request to handle_* |
|
321 |
def handle_req (req) : |
|
322 |
""" |
|
323 |
Main request handler |
|
324 |
""" |
|
325 |
||
326 |
# decode req |
|
327 |
name, path = get_req_path(req) |
|
328 |
||
329 |
# determine dir/image |
|
330 |
if os.path.isdir(path) : |
|
331 |
# directory |
|
332 |
return handle_dir(req, name, path) |
|
333 |
||
334 |
elif not os.path.exists(path) : |
|
335 |
# no such file |
|
336 |
raise exceptions.NotFound(name) |
|
337 |
||
338 |
elif not name or not name.endswith('.png') : |
|
339 |
# invalid file |
|
340 |
raise exceptions.BadRequest("Not a PNG file") |
|
341 |
||
342 |
else : |
|
343 |
# image |
|
344 |
return handle_img(req, name, path) |
|
345 |
||
346 |
||
347 |
||
30 | 348 |
|
349 |
@responder |
|
350 |
def application (env, start_response) : |
|
89 | 351 |
""" |
352 |
Main WSGI entry point |
|
353 |
""" |
|
354 |
||
30 | 355 |
req = Request(env, start_response) |
356 |
||
357 |
try : |
|
89 | 358 |
return handle_req(req) |
30 | 359 |
|
360 |
except exceptions.HTTPException, e : |
|
361 |
return e |
|
362 |