Geomatics(GIS,GPS,RS,Surveying)GIS后端

How it works(6) TileStache源码阅读(B

2019-02-13  本文已影响14人  默而识之者

引入

TileStache的核心阅读完了,就可以看看具体的功能部分是如何运行的.
功能部分有4大类:

模块

Caches.py

缓存的使用在上一篇已经看过了:

所有的缓存都必须实现如下的方法:

下面以文件缓存为例:

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为例,推广的难度在于两点:

  1. 使用
    geoserver有一个web页面的控制台,先不说美观与否,它确实可以进行全部的操作,真正做到了开箱即用,而不是执行控制台命令,编写代码或配置文件,这足以吓退全部用户了.
  2. 环境
    geoserver的开箱即用包含一个很大的前提:它足够容易"开箱",配好java环境,就能运行.尽管如Tilestrata或TileStache因为调用C++库等拥有更高的效率,更低的内存占用,但安装还是太"硬核",还包含相当多不确定性甚至拒绝windows平台的用户.这也足以吓退全部用户了.

docker+一个简单H5界面+一个有基本功能的地图服务器或许就是一个更令人能接受的产品了.

上一篇下一篇

猜你喜欢

热点阅读