How it works(6) TileStache源码阅读(B
2019-02-13 本文已影响14人
默而识之者
引入
TileStache的核心阅读完了,就可以看看具体的功能部分是如何运行的.
功能部分有4大类:
- Caches.py
- PixelEffects.py
- Pixels.py
- Providers.py
模块
Caches.py
缓存的使用在上一篇已经看过了:
- 读取缓存不需要锁
- 写入缓存必须先加锁,再写入,再解锁
所有的缓存都必须实现如下的方法:
- save
- read
- lock/unlock
- remove
下面以文件缓存为例:
class Disk:
def __init__(self, path, umask=0o022, dirs='safe', gzip='txt text json xml'.split()):
self.cachepath = path
self.umask = int(umask)
self.dirs = dirs
self.gzip = [format.lower() for format in gzip]
def _is_compressed(self, format):
return format.lower() in self.gzip
def _filepath(self, layer, coord, format):
"""
获取缓存路径
"""
l = layer.name()
z = '%d' % coord.zoom
e = format.lower()
e += self._is_compressed(format) and '.gz' or ''
# safe模式下,文件名进行扩充,一个文件夹下将尽可能包含少的文件
# 如12/000/656/001/582.png
if self.dirs == 'safe':
x = '%06d' % coord.column
y = '%06d' % coord.row
# 将行列号扩展为6位
x1, x2 = x[:3], x[3:]
y1, y2 = y[:3], y[3:]
filepath = os.sep.join( (l, z, x1, x2, y1, y2 + '.' + e) )
# portable模式下不作处理
# 如12/656/1582.png
elif self.dirs == 'portable':
x = '%d' % coord.column
y = '%d' % coord.row
filepath = os.sep.join( (l, z, x, y + '.' + e) )
elif self.dirs == 'quadtile':
# 实现quadtile索引模式,详见https://wiki.openstreetmap.org/wiki/QuadTiles
pad, length = 1 << 31, 1 + coord.zoom
xs = bin(pad + int(coord.column))[-length:]
ys = bin(pad + int(coord.row))[-length:]
dirpath = ''.join([str(int(y+x, 2)) for (x, y) in zip(xs, ys)])
parts = [dirpath[i:i+3] for i in range(0, len(dirpath), 3)]
filepath = os.sep.join([l] + parts[:-1] + [parts[-1] + '.' + e])
return filepath
def _fullpath(self, layer, coord, format):
"""
获取完整缓存路径
"""
filepath = self._filepath(layer, coord, format)
fullpath = pathjoin(self.cachepath, filepath)
return fullpath
def _lockpath(self, layer, coord, format):
return self._fullpath(layer, coord, format) + '.lock'
def lock(self, layer, coord, format):
lockpath = self._lockpath(layer, coord, format)
due = time.time() + layer.stale_lock_timeout
while True:
# 如果出错就不断重复尝试上锁
try:
umask_old = os.umask(self.umask)
# 如果超时还未加上锁就尝试先删除这个锁
if time.time() > due:
try:
os.rmdir(lockpath)
except OSError:
pass
os.makedirs(lockpath, 0o777&~self.umask)
break
except OSError as e:
if e.errno != 17:
raise
time.sleep(.2)
finally:
# 恢复为原先的权限状态
os.umask(umask_old)
def unlock(self, layer, coord, format):
lockpath = self._lockpath(layer, coord, format)
# 解锁就是删掉锁文件
try:
os.rmdir(lockpath)
except OSError:
pass
def remove(self, layer, coord, format):
fullpath = self._fullpath(layer, coord, format)
try:
os.remove(fullpath)
except OSError as e:
if e.errno != 2:
raise
def read(self, layer, coord, format):
fullpath = self._fullpath(layer, coord, format)
if not exists(fullpath):
return None
# 当前缓存的存活周期
age = time.time() - os.stat(fullpath).st_mtime
# 超时则无效
if layer.cache_lifespan and age > layer.cache_lifespan:
return None
elif self._is_compressed(format):
return gzip.open(fullpath, 'r').read()
else:
body = open(fullpath, 'rb').read()
return body
def save(self, body, layer, coord, format):
fullpath = self._fullpath(layer, coord, format)
try:
umask_old = os.umask(self.umask)
os.makedirs(dirname(fullpath), 0o777&~self.umask)
except OSError as e:
if e.errno != 17:
raise
finally:
os.umask(umask_old)
suffix = '.' + format.lower()
suffix += self._is_compressed(format) and '.gz' or ''
# 先写入临时文件
fh, tmp_path = mkstemp(dir=self.cachepath, suffix=suffix)
# 处理压缩
if self._is_compressed(format):
os.close(fh)
tmp_file = gzip.open(tmp_path, 'w')
tmp_file.write(body)
tmp_file.close()
else:
os.write(fh, body)
os.close(fh)
# 再移动到正确位置
try:
os.rename(tmp_path, fullpath)
except OSError:
os.unlink(fullpath)
os.rename(tmp_path, fullpath)
os.chmod(fullpath, 0o666&~self.umask)
Provider.py
从前Core.py对Provider的调用可以看出,只要一个Provider拥有renderArea这一方法,就能完成瓦片的渲染:
if self.doMetatile() or hasattr(provider, 'renderArea'):
tile = provider.renderArea(width, height, srs, xmin, ymin, xmax, ymax, coord.zoom)
elif hasattr(provider, 'renderTile'):
width, height = self.dim, self.dim
tile = provider.renderTile(width, height, srs, coord)
就以mapnik的Provider为例:
class ImageProvider:
def __init__(self, layer, mapfile, fonts=None, scale_factor=None):
"""
初始化mapnik引擎
"""
maphref = urljoin(layer.config.dirpath, mapfile)
scheme, h, path, q, p, f = urlparse(maphref)
if scheme in ('file', ''):
self.mapfile = path
else:
self.mapfile = maphref
self.layer = layer
self.mapnik = None
try:
engine = mapnik.FontEngine.instance()
except AttributeError:
engine = mapnik.FontEngine
if fonts:
fontshref = urljoin(layer.config.dirpath, fonts)
scheme, h, path, q, p, f = urlparse(fontshref)
if scheme not in ('file', ''):
raise Exception('Fonts from "%s" can\'t be used by Mapnik' % fontshref)
for font in glob(path.rstrip('/') + '/*.ttf'):
engine.register_font(str(font))
self.scale_factor = scale_factor
@staticmethod
def prepareKeywordArgs(config_dict):
"""
静态函数,从配置文件发过来的参数,转换成自身所需的形式
"""
kwargs = {'mapfile': config_dict['mapfile']}
if 'fonts' in config_dict:
kwargs['fonts'] = config_dict['fonts']
if 'scale factor' in config_dict:
kwargs['scale_factor'] = int(config_dict['scale factor'])
return kwargs
def renderArea(self, width, height, srs, xmin, ymin, xmax, ymax, zoom):
"""
渲染给定区域
"""
start_time = time()
if global_mapnik_lock.acquire():
try:
if self.mapnik is None:
self.mapnik = get_mapnikMap(self.mapfile)
logging.debug('TileStache.Mapnik.ImageProvider.renderArea() %.3f to load %s', time() - start_time, self.mapfile)
self.mapnik.width = width
self.mapnik.height = height
self.mapnik.zoom_to_box(Box2d(xmin, ymin, xmax, ymax))
img = mapnik.Image(width, height)
if self.scale_factor is None:
mapnik.render(self.mapnik, img)
else:
mapnik.render(self.mapnik, img, self.scale_factor)
except:
self.mapnik = None
raise
finally:
# always release the lock
global_mapnik_lock.release()
if hasattr(Image, 'frombytes'):
img = Image.frombytes('RGBA', (width, height), img.tostring())
else:
img = Image.fromstring('RGBA', (width, height), img.tostring())
logging.debug('TileStache.Mapnik.ImageProvider.renderArea() %dx%d in %.3f from %s', width, height, time() - start_time, self.mapfile)
return img
可以看出,整体结构非常简单,因为所有的复杂操作都交给mapnik本身了,只需传参,静等返回.
Pixels.py/PixelEffects.py
同mapnik类似,基本上是通过简单调用已经封装完善的库,没有太多操作,在此不再赘述.
总结
阅读完TileStache的感觉就是,它把更多的精力放在扩充专业功能上,而非架构的精妙,尽可能的描述了一个地图服务器应该有哪些GIS功能.
一点额外的想法
无论是Tilestrata还是TileStache,都是很不错的地图服务器,或许不会像Geoserver或Arcgis Server那么优秀,但足以满足很多人的需求了,但他们却并不为大多数人所知.
以Geoserver为例,推广的难度在于两点:
- 使用
geoserver有一个web页面的控制台,先不说美观与否,它确实可以进行全部的操作,真正做到了开箱即用,而不是执行控制台命令,编写代码或配置文件,这足以吓退全部用户了. - 环境
geoserver的开箱即用包含一个很大的前提:它足够容易"开箱",配好java环境,就能运行.尽管如Tilestrata或TileStache因为调用C++库等拥有更高的效率,更低的内存占用,但安装还是太"硬核",还包含相当多不确定性甚至拒绝windows平台的用户.这也足以吓退全部用户了.
docker+一个简单H5界面+一个有基本功能的地图服务器或许就是一个更令人能接受的产品了.