You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

715 lines
21 KiB
Python

#!/usr/bin/env python
from __future__ import print_function, division
from argparse import ArgumentParser
from imagetile2 import tile_image
from PIL import Image
import os, json, sys, re, datetime, urlparse
from math import ceil, log
"""
Maybe a better name for this script is tiling or tiler as it's not particularly leaflet specific.
"""
def tiles_path_for (n):
return n + ".tiles"
def autolink (text):
def sub (m):
return u'<a href="{0}">LINK</a>'.format(m.group(0))
return re.sub(r'(?<!")https?://[\S]+(?!")', sub, text, re.I)
def parse8601 (t, fmt=None):
""" simple 8601 parser that doesn't care about more than YMDHMS"""
# 2016-11-16T14:13:40.379857
m = re.search(r"(?P<year>\d\d\d\d)-(?P<month>\d\d)-(?P<day>\d\d)T(?P<hour>\d\d):(?P<minute>\d\d):(?P<second>\d\d)", t)
if m:
d = m.groupdict()
ret = datetime.datetime(int(d['year']), int(d['month']), int(d['day']), int(d['hour']), int(d['minute']), int(d['second']))
if fmt:
return ret.strftime(fmt)
else:
return ret
class tiles_wrapper (object):
""" Image wrapper abstraction... include URL to original + caption
"""
def __init__(self, path, url=None, text=None, tilename="z{0[z]}y{0[y]}x{0[x]}.png"):
self.path = path
# self.item = item
self.url = url
self.text = text
self.tilename = tilename
def get_tile_path (self, z, y, x):
return os.path.join(self.path, self.tilename.format({'z':z,'y':y,'x':x}))
def zoom (self):
""" return serialized version of self """
node = {}
node['zoomable'] = True
if self.text:
node['text'] = self.text
else:
# autotext is a link to the url showing the basename
_, basename = os.path.split(self.url)
base, ext = os.path.splitext(basename)
ext = ext[1:]
node['text'] = u"<p class=\"caption\"><a class=\"url\" href=\"{0}\">{1}</a>".format(self.url, ext.upper())
node['url'] = self.url
node['image'] = self.get_tile_path(0, 0, 0)
return node
def zoom_recursive (self, caption, x=0, y=0, z=0, maxzoom=3):
""" old style zoom in place -- ie render self to child nodes """
node = {}
node['text'] = self.text
node['image'] = self.get_tile_path(z, y, x)
if z < maxzoom:
kids = []
for r in range(2):
for c in range(2):
kids.append(self.zoom_recursive(caption, (x*2)+c, (y*2)+r, z+1, maxzoom))
node['children'] = kids
return node
def cell_layout(items, w=2):
i = 0
for r in range(w):
for c in range(w):
if i<len(items):
yield items[i], c, r
i+=1
def fourup (imgs, w, h):
print ("fourup", imgs, w, h, file=sys.stderr)
oi = Image.new("RGBA", (w, h))
cw = w//2
ch = h//2
i = 0
for impath, c, r in cell_layout(imgs):
if impath:
im = Image.open(impath)
im.thumbnail((cw, ch))
oi.paste(im, (c*cw, r*ch))
return oi
def split4(items):
""" returns 4 lists where len(l) is a power of 4 """
l = len(items)
p = int(ceil(log(l, 4)))
# print ("{0} items {1} {2} {3}".format(l, p, 2**p, 4**p))
c = int((4**p)/ 4)
# c = int(ceil(len(items) / 4))
def el (x, c): # ensurelength
while len(x) < c:
x.append(None)
return x
ret = [items[0:c],items[c:c*2],items[c*2:c*3],items[c*3:]]
return tuple([el(x, c) for x in ret])
def gridrender (items, basename, tilewidth=320, tileheight=320, z=0, y=0, x=0):
""" items are now nodes proper """
""" Takes a list of nodes and returns a new node where items are arranged in a cascade of nodes such that
all items appear at the same (z) level -- side by side
Uses fourup to (recursively) produce a composite image of the underlying tiles.
"""
print ("gridrender {0} items".format(len(items)), file=sys.stderr)
if len(items) == 1:
x = items[0]
if x == None:
return None
return x # x.zoom()
else:
node = {}
node['text'] = ''
kids = []
for group, x2, y2 in cell_layout(split4(items)):
kids.append(gridrender(group, basename, tilewidth, tileheight, z+1, (y*2)+y2, (x*2)+x2))
node['children'] = [j for j in kids if j != None]
newim = fourup([j.get("image") for j in node['children'] if j != None and j.get("image")], tilewidth, tileheight)
node['image'] = newim
newimpath = "{0}.z{1}y{2}x{3}.png".format(basename, z, y, x)
newim.save(newimpath)
node['image'] = newimpath
print ("Created 4up image {0}".format(newimpath), file=sys.stderr)
return node
def recursiverender (items, basename, tilewidth=320, tileheight=320, direction=3, z=0):
node = {}
node['text'] = ''
# if len(items) >=1 and 'date' in items[0].item:
# node['text'] = items[0].item['date']
# else:
# node['text'] = ''
# node['image'] = ''
node['children'] = cc = [None, None, None, None]
ai = 0
for x in items[:3]:
# cap = os.path.splitext(os.path.basename(x.path))[0]
# cc.append(x) # x.zoom()
if (ai == direction):
ai += 1
cc[ai] = x
ai += 1;
rest = items[3:]
if rest:
# recurse
# cc.append(recursiverender(rest, basename, tilewidth, tileheight, z+1))
cc[direction] = recursiverender(rest, basename, tilewidth, tileheight, direction, z+1)
newim = fourup([x.get("image") for x in node['children'] if x != None and x.get("image")], tilewidth, tileheight)
# simplified name works just because there's only one generated tile per level
newimpath = u"{0}.z{1}.png".format(basename, z)
newim.save(newimpath)
node['image'] = newimpath
return node
def layoutxyz (n, x=0, y=0, z=0, outnode={}):
# print ("layout", n, x, y, z, file=sys.stderr)
outnode["{0},{1},{2}".format(x,y,z)] = {
"text": n['text'],
"image": n['image']
}
if 'children' in n:
for child, cx, cy in cell_layout(n['children']):
layout(child, (x*2)+cx, (y*2)+cy, z+1, outnode)
return outnode
def html (node, title):
page = u"""<!DOCTYPE html>
<html>
<head>
<title>""" + title + u"""</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<script src="/lib/leaflet-1.0.1/leaflet.js"></script>
<link href="/lib/leaflet-1.0.1/leaflet.css" rel="stylesheet" type="text/css">
<link href="map.css" rel="stylesheet" type="text/css">
</head>
<body>
<div id="frame" style="position: absolute; left: 0px; top: 0px; right: 0px; bottom: 0px">
<div id="map" style="width: 100%; height: 100%; background: black"></div>
<div id="text" style="position: absolute; left: 50px; top: 10px; width: auto; color: white">
</div>
</div>
<script>
(function() {
// warning CHANGES TO THIS CODE NEED TO BE ROLLED BACK INTO leaflet.py
var cell_layout, expandzoom, fourup, layoutxyz, render, split4, tiler, tiles_wrapper, zoom;
window.tiler = tiler = {};
tiler.tiles_wrapper = tiles_wrapper = function(path, ext) {
if (ext == null) { ext = "jpg"; }
var ret = {};
ret.get_tile_path = function(z, y, x) {
return path + ("/z"+z+"y"+y+"x"+x+"."+ext);
};
return ret;
};
tiler.zoom = zoom = function(tiles, caption, url, x, y, z, maxzoom) {
var c, i, k, kids, len, len1, node, r, ref, ref1;
if (x == null) {
x = 0;
}
if (y == null) {
y = 0;
}
if (z == null) {
z = 0;
}
if (maxzoom == null) {
maxzoom = 3;
}
node = {};
if (caption && x === 0 && y === 0) {
node['text'] = caption;
}
var lastc = Math.pow(2, z) - 1;
if (url && x === 0 && y === lastc) {
node['url'] = url
}
node['image'] = tiles.get_tile_path(z, y, x);
if (z < maxzoom) {
kids = [];
ref = [0, 1];
for (i = 0, len = ref.length; i < len; i++) {
r = ref[i];
ref1 = [0, 1];
for (k = 0, len1 = ref1.length; k < len1; k++) {
c = ref1[k];
kids.push(zoom(tiles, caption, url, (x * 2) + c, (y * 2) + r, z + 1, maxzoom));
}
}
node['children'] = kids;
}
return node;
};
split4 = function(items) {
var c, el, i, l, len, p, ref, results, x;
l = items.length;
p = Math.ceil(Math.log(l) / Math.log(4));
c = Math.max(1, Math.pow(4, p) / 4);
el = function(x, c) {
while (x.length < c) {
x.push(null);
}
return x;
};
ref = [items.slice(0, c), items.slice(c, c * 2), items.slice(c * 2, c * 3), items.slice(c * 3)];
results = [];
for (i = 0, len = ref.length; i < len; i++) {
x = ref[i];
results.push(el(x, c));
}
return results;
};
cell_layout = function(items) {
return [
{
y: 0,
x: 0,
item: items[0]
}, {
y: 0,
x: 1,
item: items[1]
}, {
y: 1,
x: 0,
item: items[2]
}, {
y: 1,
x: 1,
item: items[3]
}
];
};
tiler.render = render = function(items, tilewidth, tileheight, z, y, x) {
var g, i, j, kids, len, node, ref;
if (tilewidth == null) {
tilewidth = 256;
}
if (tileheight == null) {
tileheight = 256;
}
if (z == null) {
z = 0;
}
if (y == null) {
y = 0;
}
if (x == null) {
x = 0;
}
if (items.length === 1) {
x = items[0];
if (x === null) {
return null;
}
return zoom(x, '');
} else {
node = {};
node['text'] = '';
kids = [];
ref = cell_layout(split4(items));
for (i = 0, len = ref.length; i < len; i++) {
g = ref[i];
kids.push(render(g.item, tilewidth, tileheight, z + 1, (y * 2) + g.y, (x * 2) + g.x));
}
node.children = (function() {
var k, len1, results;
results = [];
for (k = 0, len1 = kids.length; k < len1; k++) {
j = kids[k];
if (j !== null) {
results.push(j);
}
}
return results;
})();
node.image = fourup((function() {
var k, len1, ref1, results;
ref1 = node.children;
results = [];
for (k = 0, len1 = ref1.length; k < len1; k++) {
j = ref1[k];
if (j !== null) {
results.push(j.image);
}
}
return results;
})(), tilewidth, tileheight);
return node;
}
};
tiler.layoutxyz = layoutxyz = function(n, x, y, z, outnode) {
var g, i, len, ref;
if (x == null) {
x = 0;
}
if (y == null) {
y = 0;
}
if (z == null) {
z = 0;
}
if (outnode == null) {
outnode = {};
}
outnode[x + "," + y + "," + z] = n;
if (n.children) {
ref = cell_layout(n.children);
for (i = 0, len = ref.length; i < len; i++) {
g = ref[i];
if (g.item) {
layoutxyz(g.item, (x * 2) + g.x, (y * 2) + g.y, z + 1, outnode);
}
}
}
return outnode;
};
tiler.fourup = fourup = function(images, tilewidth, tileheight) {
if (tilewidth == null) {
tilewidth = 256;
}
if (tileheight == null) {
tileheight = 256;
}
return function(done) {
var i, img, imgelts, len, loadcount, results, src, x;
loadcount = 0;
images = (function() {
var i, len, results;
results = [];
for (i = 0, len = images.length; i < len; i++) {
x = images[i];
if (x !== null) {
results.push(x);
}
}
return results;
})();
imgelts = [];
results = [];
for (i = 0, len = images.length; i < len; i++) {
src = images[i];
img = new Image;
imgelts.push(img);
img.addEventListener("load", function() {
var canvas, ctx, g, hh, hw, k, len1, ref;
if (++loadcount >= images.length) {
canvas = document.createElement("canvas");
canvas.width = tilewidth;
canvas.height = tileheight;
ctx = canvas.getContext("2d");
hw = tilewidth / 2;
hh = tileheight / 2;
ref = cell_layout(imgelts);
for (k = 0, len1 = ref.length; k < len1; k++) {
g = ref[k];
if (g.item) {
ctx.drawImage(g.item, g.x * hw, g.y * hh, hw, hh);
}
}
return done(null, canvas.toDataURL());
}
}, false);
if (typeof src === "function") {
console.log("inside 4up, deferring");
results.push(src(function(err, data) {
console.log(" inside 4up, GOT DATA");
return img.src = data;
}));
} else {
results.push(img.src = src);
}
}
return results;
};
};
tiler.expandzoom = expandzoom = function(node) {
var c, ret, tilespath;
if (node.zoomable) {
tilespath = node.image.replace(/\/[^\/]+$/, "");
var ext = node.image.match(/\.([^\.]+)$/);
if (ext != null) { ext = ext[1] };
ret = zoom(tiles_wrapper(tilespath, ext), node.text, node.url);
return ret;
}
if (node.children) {
node.children = (function() {
var i, len, ref, results;
ref = node.children;
results = [];
for (i = 0, len = ref.length; i < len; i++) {
c = ref[i];
if (c != null) {
results.push(expandzoom(c));
}
}
return results;
})();
}
return node;
};
/* DynamicTiles */
/*
A simple GridLayer extension that takes an external "nodes" object as option,
Nodes are keyed [x,y,z]
and expected to be of the form:
{
text: "My text",
image" "imagepath.jpg"
}
*/
L.GridLayer.DynamicTiles = L.GridLayer.extend({
createTile: function (coords, done) { // done = (err, tile)
// console.log("createTile", coords, this.options, this.options.nodes);
var tile = document.createElement('div'),
node = this.options.nodes[coords.x+","+coords.y+","+coords.z],
defer = false;
tile.classList.add("tile");
if (node != undefined) {
// console.log("NODE", node);
if (node.image) {
var img = document.createElement("img");
defer = true;
img.addEventListener("load", function () {
done(null, tile);
})
img.src = node.image;
tile.appendChild(img);
img.classList.add("imagetile");
}
if (node.text) {
//console.log("text", node.text);
var textdiv = document.createElement("div");
textdiv.innerHTML = node.text;
tile.appendChild(textdiv);
textdiv.classList.add("text");
}
// if (node.url) {
// console.log("NODE HAS URL!", node.url);
// var urldiv = document.createElement("div"),
// urllink = document.createElement("a"),
// m = node.url.search(/\/([^\/]+)$/);
// urllink.innerHTML = (m != null) ? m[1] : "LINK";
// urldiv.appendChild(urllink);
// urldiv.classList.add("url");
// tile.appendChild(urldiv);
// }
if (node.background) {
tile.style.color = node.background;
}
if (node.class) {
tile.classList.add(node.class);
}
tile.classList.add("z"+coords.z);
} else {
tile.innerHTML = [coords.x, coords.y, coords.z].join(', ');
tile.classList.add("coords");
}
// tile.style.outline = '1px solid red';
if (!defer) {
window.setTimeout(function () {
done(null, tile);
}, 250);
}
return tile;
}
});""
L.gridLayer.dynamicTiles = function(opts) {
return new L.GridLayer.DynamicTiles(opts);
};
}).call(this);
(function () {
function getjson (url, callback) {
var request = new XMLHttpRequest();
request.open('GET', url, true);
request.onload = function() {
if (request.readyState == XMLHttpRequest.DONE && request.status >= 200 && request.status < 400) {
callback(null, JSON.parse(request.responseText));
} else {
callback("server error");
}
};
request.onerror = function() {
callback("connection error");
};
request.send();
}
var map = L.map('map', {
editable: true,
maxZoom: 100,
minZoom: 0,
zoom: 0,
crs: L.CRS.Simple,
center: new L.LatLng(0,0),
});
var data = """ + json.dumps(node) + """;
var nodes = (tiler.layoutxyz(tiler.expandzoom(data)));
map.addLayer( L.gridLayer.dynamicTiles({
minZoom: 0,
nodes: nodes
}) );
var yx = L.latLng,
xy = function(x, y) {
if (L.Util.isArray(x)) { // When doing xy([x, y]);
return yx(x[1], x[0]);
}
return yx(y, x); // When doing xy(x, y);
};
// map.setView(xy(0.5 * 256, -0.5 * 256), 0);
})();
</script>
</body>
</html>
"""
return page
def make_gallery(args):
"""
to do -- separate the actual tiling process...
make tiling a separate pass ON THE ACTUAL NODE jSON
NB: this command accepts two different kinds of input.
1. One or more images as (argv) arguments -or-
2. A JSON stream (one object per line) on stdin.
"""
bgcolor = None # (0, 0, 0)
items = []
if args.input:
for x in args.input:
i = {'url': x}
items.append(i)
else:
for line in sys.stdin:
line = line.rstrip()
if line and not line.startswith("#"):
item = json.loads(line)
items.append(item)
# Ensure / Generate tiles per image
items.sort(key=lambda x: x['url'])
tiles = []
for item in items:
n = item['url']
# print (n, file=sys.stderr)
path = os.path.join(args.tilespath, n)
# TODO date format...
caption = ''
if 'text' or 'date' in item:
caption += u'<p class="caption">';
if 'text' in item:
caption += u'<span class="text">{0}</span>'.format(autolink(item['text']))
if 'date' in item:
dt = parse8601(item['date'], "%d %b %Y")
caption += u'<span class="date">{0}</span>'.format(dt)
if 'url' in item:
ext = os.path.splitext(urlparse.urlparse(item['url']).path)[1]
if ext:
ext = ext[1:].upper()
caption += u'<a class="url" href="{0}">{1}</a>'.format(item['url'], ext)
if 'text' or 'date' in item:
caption += u'</p>';
t = tiles_wrapper(path, item['url'], text=caption)
tiles.append(t)
tile0 = t.get_tile_path(0, 0, 0) # os.path.join(path, args.tilename.format({'x': 0, 'y': 0, 'z': 0}))
if not os.path.exists(tile0) or args.force:
print ("Tiling {0}".format(n), file=sys.stderr)
try:
im = Image.open(n)
try:
os.makedirs(path)
except OSError:
pass
tile_image(im, args.zoom, args.tilewidth, args.tileheight, path+"/", args.tilename, bgcolor)
# tiles.append(t)
except IOError as e:
print ("Missing {0}, skipping".format(n), file=sys.stderr)
tiles = tiles[:-1]
# DO THE LAYOUT, generating intermediate tiles (zoom outs)
if args.reverse:
tiles.reverse()
tiles = [t.zoom() for t in tiles]
basename = os.path.join(args.tilespath, args.name)
if args.recursive:
root_node = recursiverender(tiles, basename, args.tilewidth, args.tileheight, args.direction)
else:
root_node = gridrender(tiles, basename, args.tilewidth, args.tileheight)
# OUTPUT ROOT NODE
if args.html:
print (html(root_node, args.name))
else:
print (json.dumps(root_node, indent=args.indent))
if __name__ == "__main__":
ap = ArgumentParser("")
ap.add_argument("--basepath", default=".")
ap.add_argument("--baseuri", default="")
ap.add_argument("--tilespath", default="tiles")
ap.add_argument("--tilewidth", type=int, default=256)
ap.add_argument("--tileheight", type=int, default=256)
ap.add_argument("--zoom", type=int, default=3)
ap.add_argument("--tilename", default="z{0[z]}y{0[y]}x{0[x]}.png")
ap.add_argument("--reverse", default=False, action="store_true")
ap.add_argument("--indent", default=2, type=int)
ap.add_argument("--recursive", default=False, action="store_true")
ap.add_argument("--force", default=False, action="store_true")
subparsers = ap.add_subparsers(help='sub-command help')
ap_gallery = subparsers.add_parser('gallery', help='Create a grid gallery of images')
ap_gallery.add_argument("input", nargs="*")
ap_gallery.add_argument("--html", default=False, action="store_true")
ap_gallery.add_argument("--recursive", default=False, action="store_true")
ap_gallery.add_argument("--reverse", default=False, action="store_true")
ap_gallery.add_argument("--direction", type=int, default=3, help="cell to recursively expand into, 0-3, default: 3 (bottom-right)")
ap_gallery.add_argument("--name", default="gallery")
ap_gallery.set_defaults(func=make_gallery)
args = ap.parse_args()
args.func(args)