PKirGcktilecloud_chain/expiretiles.py# -*- coding: utf-8 -*- from argparse import ArgumentParser import sys import psycopg2 from shapely.geometry import Polygon, MultiPolygon from shapely.ops import cascaded_union from tilecloud.grid.quad import QuadTileGrid from tilecloud_chain import parse_tilecoord def main(): parser = ArgumentParser( description='Used to import the osm2pgsql expire-tiles file to Postgres', prog=sys.argv[0] ) parser.add_argument( '--buffer', type=float, default=0.0, help='Extent buffer to the tiles [m], default is 0', ) parser.add_argument( '--simplify', type=float, default=0.0, help='Simplify the result geometry [m], default is 0', ) parser.add_argument( '--create', default=False, action="store_true", help='create the table if not exists', ) parser.add_argument( '--delete', default=False, action="store_true", help='empty the table', ) parser.add_argument( 'file', metavar='FILE', help='The osm2pgsql expire-tiles file', ) parser.add_argument( 'connection', metavar='CONNECTION', help='The PostgreSQL connection string e.g. "user=www-data password=www-data dbname=sig host=localhost"', ) parser.add_argument( 'table', metavar='TABLE', help='The PostgreSQL table to fill', ) parser.add_argument( '--schema', default='public', help='The PostgreSQL schema to use (should already exists), default is public', ) parser.add_argument( 'column', metavar='COLUMN', default='geom', nargs='?', help='The PostgreSQL column, default is "geom"', ) parser.add_argument( '--srid', type=int, default=3857, nargs='?', help='The stored geometry SRID, no conversion by default (3857)', ) options = parser.parse_args() connection = psycopg2.connect(options.connection) cursor = connection.cursor() if options.create: cursor.execute( "SELECT count(*) FROM pg_tables WHERE schemaname='%s' AND tablename='%s'" % ( options.schema, options.table, ) ) if cursor.fetchone()[0] == 0: cursor.execute('CREATE TABLE IF NOT EXISTS "%s"."%s" (id serial)' % ( options.schema, options.table, )) cursor.execute("SELECT AddGeometryColumn('%s', '%s', '%s', %s, 'MULTIPOLYGON', 2)" % ( options.schema, options.table, options.column, options.srid, )) if options.delete: cursor.execute('DELETE FROM "%s"' % (options.table)) geoms = [] grid = QuadTileGrid( max_extent=(-20037508.34, -20037508.34, 20037508.34, 20037508.34), ) with open(options.file, "r") as f: for coord in f: extent = grid.extent(parse_tilecoord(coord), options.buffer) geoms.append(Polygon(( (extent[0], extent[1]), (extent[0], extent[3]), (extent[2], extent[3]), (extent[2], extent[1]) ))) if len(geoms) == 0: print("No coords found") connection.commit() cursor.close() connection.close() exit(0) geom = cascaded_union(geoms) if geom.geom_type == 'Polygon': geom = MultiPolygon((geom,)) if options.simplify > 0: geom.simplify(options.simplify) sql_geom = "ST_GeomFromText('%s', 3857)" % geom.wkt if options.srid <= 0: sql_geom = "ST_GeomFromText('%s')" % geom.wkt # pragma: no cover elif options.srid != 3857: sql_geom = 'ST_Transform(%s, %i)' % (sql_geom, options.srid) cursor.execute('INSERT INTO "%s" ("%s") VALUES (%s)' % ( options.table, options.column, sql_geom )) connection.commit() cursor.close() connection.close() print('Import successful') PK9CeE1tilecloud_chain/wmts_get_capabilities_template.pywmts_get_capabilities_template = """ REST {% if server %} KVP {% endif %} {% for base_url in base_urls %} REST{% if server %} KVP{% endif %} {% endfor %} {% for layername, layer in layers.items() %} {{layername}} {{layername}} {{layer['mime_type']}} {% if layer['query_layers'] %}{% for info_format in layer['info_formats'] %} {{infoformat}}{% endfor %}{% endif %}{% for dimension in layer['dimensions'] %} {{dimension['name']}} {{dimension['default']}} {% for value in dimension['values'] %} {{value}} {% endfor %} {% endfor %}{% for base_url in base_urls %} {% endfor %} {{layer["grid"]}} {% endfor %} {% for gridname, grid in grids.items() %} {{gridname}} urn:ogc:def:crs:{{ grid['srs'].replace(':', '::') }}{% for i, resolution in enumerate(grid['resolutions']) %}{% set width = int(ceil( (grid['bbox'][2]-grid['bbox'][0]) / resolution / grid['tile_size'])) %}{% set height = int(ceil( (grid['bbox'][3]-grid['bbox'][1]) / resolution / grid['tile_size'])) %}{% set left = grid['bbox'][0] %}{% set top = grid['bbox'][3] %} {{ get_tile_matrix_identifier(grid, resolution=resolution, zoom=i) }} {{resolution / 0.00028}} {{left}} {{top}} {{grid['tile_size']}} {{grid['tile_size']}} {{width}} {{height}} {% endfor %} {% endfor %} """ PKvBߥ  "tilecloud_chain/openlayers_html.pyopenlayers_html = """ OpenLayers test page
""" PKwHR"^tilecloud_chain/__init__.py# -*- coding: utf-8 -*- import sys import os import re import logging import yaml import sqlite3 import tempfile import subprocess from six.moves import cStringIO, map, filter from math import ceil, sqrt from hashlib import sha1 from fractions import Fraction from datetime import datetime from tilecloud import consume try: import bsddb3 as bsddb except: # pragma: no cover import bsddb try: from PIL import Image Image # suppress pyflakes warning except: # pragma: no cover import Image import psycopg2 from shapely.wkb import loads as loads_wkb from shapely.geometry import Polygon from shapely.ops import cascaded_union import boto.sqs from boto.sqs.jsonmessage import JSONMessage from tilecloud import Tile, BoundingPyramid, TileCoord from tilecloud.grid.free import FreeTileGrid from tilecloud.store.metatile import MetaTileSplitterTileStore from tilecloud.store.s3 import S3TileStore from tilecloud.store.mbtiles import MBTilesTileStore from tilecloud.store.bsddb import BSDDBTileStore from tilecloud.store.filesystem import FilesystemTileStore from tilecloud.layout.wmts import WMTSTileLayout from tilecloud.filter.logger import Logger from tilecloud.filter.error import LogErrors, MaximumConsecutiveErrors logger = logging.getLogger('tilecloud_chain') def add_comon_options( parser, tile_pyramid=True, no_geom=True, near=True, time=True, dimensions=False, cache=True): parser.add_argument( '-c', '--config', default='tilegeneration/config.yaml', help='path to the configuration file', metavar="FILE" ) parser.add_argument( '-l', '--layer', metavar="NAME", help='the layer to generate' ) if tile_pyramid: parser.add_argument( '-b', '--bbox', nargs=4, type=float, metavar=('MINX', 'MINY', 'MAXX', 'MAXY'), help='restrict to specified bounding box' ) parser.add_argument( '-z', '--zoom', help='restrict to specified zoom level, or a zooms range (2-5), or a zooms list (2,4,5)' ) parser.add_argument( '-t', '--test', type=int, help='test with generating N tiles, and add log messages', metavar="N" ) if near: parser.add_argument( '--near', type=float, nargs=2, metavar=('X', 'Y'), help='This option is a good replacement of --bbox, to used with ' '--time or --test and --zoom, implies --no-geom. ' 'It automatically measure a bbox around the X Y position that corresponds to the metatiles.' ) if time: parser.add_argument( '--time', '--measure-generation-time', dest='time', metavar="N", type=int, help='Measure the generation time by creating N tiles to warm-up, ' 'N tile to do the measure and N tiles to slow-down' ) if no_geom: parser.add_argument( '--no-geom', default=True, action="store_false", dest="geom", help="Don't the geometry available in the SQL" ) if dimensions: parser.add_argument( '--dimensions', nargs='+', metavar='DIMENSION=VALUE', default=[], help='overwrite the dimensions values specified in the config file' ) if cache: parser.add_argument( '--cache', '--destination-cache', dest='cache', metavar="NAME", help='The cache name to use' ) parser.add_argument( '-q', '--quiet', default=False, action="store_true", help='Display only errors.' ) parser.add_argument( '-v', '--verbose', default=False, action="store_true", help='Display info message.' ) parser.add_argument( '-d', '--debug', default=False, action="store_true", help='Display debug message, and stop on first error.' ) def get_tile_matrix_identifier(grid, resolution=None, zoom=None): if grid is None or grid['matrix_identifier'] == 'zoom': return str(zoom) else: if resolution is None: resolution = grid['resolutions'][zoom] if int(resolution) == resolution: return str(int(resolution)) else: return str(resolution).replace('.', '_') class TileGeneration: def __init__(self, config_file, options=None, layer_name=None): self.close_actions = [] self.geom = None self.error = 0 if options is not None: if not hasattr(options, 'bbox'): options.bbox = None if not hasattr(options, 'zoom'): options.zoom = None if not hasattr(options, 'test'): options.test = None if not hasattr(options, 'near'): options.near = None if not hasattr(options, 'time'): options.time = None if not hasattr(options, 'geom'): options.geom = True level = logging.WARNING if options and options.quiet: level = logging.ERROR elif options and options.verbose: level = logging.INFO elif options and options.debug: level = logging.DEBUG logging.basicConfig( format='%(levelname)s:%(name)s:%(funcName)s:%(message)s', level=level) with open(config_file) as f: self.config = yaml.load(f) self.options = options self.validate_exists(self.config, 'config', 'grids') self.grids = self.config['grids'] error = False for gname, grid in self.config['grids'].items(): if type(gname) != str: gname = str(gname) self.config['grids'][gname] = grid name = "grid[%s]" % gname error = self.validate( grid, name, 'name', attribute_type=str, default=gname, regex="^[a-zA-Z0-9_\-~\.]+$" ) or error error = self.validate( grid, name, 'resolution_scale', attribute_type=int ) or error error = self.validate( grid, name, 'resolutions', attribute_type=float, is_array=True, required=True ) or error if not error and 'resolution_scale' not in grid: scale = self._resolution_scale(grid['resolutions']) grid['resolution_scale'] = scale elif not error: scale = grid['resolution_scale'] for r in grid['resolutions']: if r * scale % 1 != 0.0: logger.error("The resolution %s * resolution_scale %i is not an integer." % (r, scale)) error = True error = self.validate(grid, name, 'bbox', attribute_type=float, is_array=True, required=True) or error error = self.validate(grid, name, 'srs', attribute_type=str, required=True) or error if not error: srs = grid['srs'].split(':') if len(srs) == 2: if srs[0].lower() == 'epsg': try: srs[1] = int(srs[1]) except ValueError: logger.error("The grid '%s' srs should have an int ref_id but it is %s." % (gname, srs[1])) else: logger.error("The grid '%s' srs should have the authority 'EPSG' but it is %s." % ( gname, srs[0]) ) error = True else: logger.error("The grid '%s' srs should have the syntax : but is %s." % ( gname, grid['srs'] )) error = True error = self.validate(grid, name, 'proj4_literal', attribute_type=str) or error if not error and 'proj4_literal' not in grid: if srs[1] == 3857: # pragma: no cover grid['proj4_literal'] = '+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 ' \ '+x_0=0.0 +y_0=0.0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs +over' elif srs[1] == 21781: grid['proj4_literal'] = '+proj=somerc +lat_0=46.95240555555556 +lon_0=7.439583333333333 +k_0=1 ' \ '+x_0=600000 +y_0=200000 +ellps=bessel +towgs84=674.374,15.056,405.346,0,0,0,0 +units=m ' \ '+no_defs' elif srs[1] == 2056: # pragma: no cover grid['proj4_literal'] = '+proj=somerc +lat_0=46.95240555555556 +lon_0=7.439583333333333 +k_0=1 ' \ '+x_0=2600000 +y_0=1200000 +ellps=bessel +towgs84=674.374,15.056,405.346,0,0,0,0 +units=m ' \ '+no_defs' else: grid['proj4_literal'] = '+init=%s' % grid['srs'] elif not error and grid['proj4_literal'] == '': # pragma: no cover grid['proj4_literal'] = None error = self.validate(grid, name, 'unit', attribute_type=str, default='m') or error error = self.validate(grid, name, 'tile_size', attribute_type=int, default=256) or error error = self.validate( grid, name, 'matrix_identifier', attribute_type=str, default='zoom', enumeration=['zoom', 'resolution'] ) or error grid['obj'] = FreeTileGrid( resolutions=[int(r * scale) for r in grid['resolutions']], scale=scale, max_extent=grid['bbox'], tile_size=grid['tile_size']) if not error else None default = self.config.get('layer_default', {}) self.layers = {} self.validate_exists(self.config, 'config', 'layers') for lname, layer in self.config['layers'].items(): name = "layer[%s]" % lname for k, v in default.items(): if k not in layer: layer[k] = v error = self.validate( layer, name, 'name', attribute_type=str, default=lname, regex="^[a-zA-Z0-9_\-~\.]+$" ) or error error = self.validate(layer, name, 'grid', attribute_type=str, required=True) or error error = self.validate(layer, name, 'min_resolution_seed', attribute_type=float) or error error = self.validate(layer, name, 'px_buffer', attribute_type=float, default=False) or error error = self.validate( layer, name, 'type', attribute_type=str, required=True, enumeration=['wms', 'mapnik'] ) or error error = self.validate(layer, name, 'meta', attribute_type=bool, default=False) or error if not layer['meta']: layer['meta_size'] = 1 else: error = self.validate(layer, name, 'meta_size', attribute_type=int, default=8) or error error = self.validate( layer, name, 'meta_buffer', attribute_type=int, default=0 if layer['type'] == 'mapnik' else 128 ) or error error = self.validate( layer, name, 'query_layers', attribute_type=str, is_array=True ) or error if not error and layer['type'] == 'wms': error = self.validate(layer, name, 'url', attribute_type=str, required=True) or error error = self.validate(layer, name, 'generate_salt', attribute_type=bool, default=False) or error if 'query_layers' in layer: error = self.validate( layer, name, 'info_formats', attribute_type=str, is_array=True, default=['application/vnd.ogc.gml'] ) or error if not error and layer['type'] == 'mapnik': error = self.validate(layer, name, 'mapfile', attribute_type=str, required=True) or error error = self.validate( layer, name, 'output_format', attribute_type=str, default='png', enumeration=['png', 'png256', 'jpeg', 'grid'] ) or error error = self.validate(layer, name, 'data_buffer', attribute_type=int, default=128) or error if layer['output_format'] == 'grid': error = self.validate(layer, name, 'resolution', attribute_type=int, default=4) or error error = self.validate(layer, name, 'layers_fields', attribute_type=dict, default={}) or error error = self.validate( layer, name, 'drop_empty_utfgrid', attribute_type=bool, default=False ) or error if layer['meta']: logger.error( "The layer '%s' is of type Mapnik/Grid, that can't support matatiles." % (layer['name']) ) error = True if 'min_resolution_seed' in layer or 'info_formats' in layer or \ 'wms_url' in layer or 'query_layers' in layer: error = self.validate(layer, name, 'wms_url', attribute_type=str, required=True) or error error = self.validate( layer, name, 'layers', attribute_type=str, default=['__all__'], is_array=True ) or error if 'info_formats' in layer or 'query_layers' in layer: error = self.validate( layer, name, 'query_layers', attribute_type=str, default=['__all__'], is_array=True ) or error error = self.validate( layer, name, 'info_formats', attribute_type=str, is_array=True, required=True ) or error if not error and (layer['type'] == 'wms' or 'wms_url' in layer): error = self.validate(layer, name, 'params', attribute_type=dict, default={ }) or error for key in layer['params']: self.validate(layer['params'], name + '/params', key, attribute_type=str) or error error = self.validate(layer, name, 'headers', attribute_type=dict, default={ 'Cache-Control': 'no-cache, no-store', 'Pragma': 'no-cache', }) or error for key in layer['headers']: self.validate(layer['headers'], name + '/headers', key, attribute_type=str) or error error = self.validate( layer, name, 'layers', attribute_type=str, required=True, is_array=True ) or error error = self.validate(layer, name, 'extension', attribute_type=str, required=True) or error error = self.validate(layer, name, 'mime_type', attribute_type=str, required=True) or error error = self.validate( layer, name, 'wmts_style', attribute_type=str, required=True, regex="^[a-zA-Z0-9_\-~\.]+$" ) or error error = self.validate(layer, name, 'dimensions', is_array=True, default=[]) or error for d in layer['dimensions']: dname = name + ".dimensions[%s]" % d.get('name', '') error = self.validate( d, dname, 'name', attribute_type=str, required=True, regex="^[a-zA-Z0-9_\-~\.]+$" ) or error error = self.validate( d, dname, 'value', attribute_type=str, required=True, regex="^[a-zA-Z0-9_\-~\.]+$" ) or error error = self.validate( d, dname, 'values', attribute_type=str, is_array=True, default=[d['value']] ) or error error = self.validate( d, dname, 'default', attribute_type=str, default=d['value'], regex="^[a-zA-Z0-9_\-~\.]+$" ) or error error = self.validate( layer, name, 'pre_hash_post_process', attribute_type=str, default=False ) or error error = self.validate( layer, name, 'post_process', attribute_type=str, default=False ) or error error = self.validate(layer, name, 'geoms', is_array=True, default=[]) or error for i, g in enumerate(layer['geoms']): gname = name + ".geoms[%i]" % i # => connection required on the layer. error = self.validate( layer, name, 'connection', attribute_type=str, required=True ) or error error = self.validate( g, gname, 'sql', attribute_type=str, required=True ) or error error = self.validate( g, gname, 'min_resolution', attribute_type=float ) or error error = self.validate( g, gname, 'max_resolution', attribute_type=float ) or error if 'empty_tile_detection' in layer: error = self.validate( layer['empty_tile_detection'], name + '.empty_tile_detection', 'size', attribute_type=int, required=True ) or error error = self.validate( layer['empty_tile_detection'], name + '.empty_tile_detection', 'hash', attribute_type=str, required=True ) or error if 'empty_metatile_detection' in layer: error = self.validate( layer['empty_metatile_detection'], name + '.empty_metatile_detection', 'size', attribute_type=int, required=True ) or error error = self.validate( layer['empty_metatile_detection'], name + '.empty_metatile_detection', 'hash', attribute_type=str, required=True ) or error if 'sqs' in layer: error = self.validate( layer['sqs'], name + '.sqs', 'queue', attribute_type=str, required=True ) or error error = self.validate( layer['sqs'], name + '.sqs', 'region', attribute_type=str, default='eu-west-1' ) or error layer['grid_ref'] = self.grids[layer['grid']] if not error else None self.layers[lname] = layer self.validate_exists(self.config, 'config', 'caches') self.caches = self.config['caches'] for cname, cache in self.caches.items(): name = "caches[%s]" % cname error = self.validate(cache, name, 'name', attribute_type=str, default=cname) or error error = self.validate( cache, name, 'type', attribute_type=str, required=True, enumeration=['s3', 'filesystem', 'mbtiles', 'bsddb'] ) or error error = self.validate( cache, 'cache[%s]' % cache['name'], 'wmtscapabilities_file', attribute_type=str, default='1.0.0/WMTSCapabilities.xml' ) or error if cache['type'] == 'filesystem' or cache['type'] == 'mbtiles' or cache['type'] == 'bsddb': error = self.validate(cache, name, 'folder', attribute_type=str, required=True) or error elif cache['type'] == 's3': error = self.validate(cache, name, 'bucket', attribute_type=str, required=True) or error error = self.validate(cache, name, 'region', attribute_type=str, default='eu-west-1') or error error = self.validate(cache, name, 'folder', attribute_type=str, default='') or error error = self.validate(self.config, 'config', 'generation', attribute_type=dict, default={}) or error error = self.validate( self.config['generation'], 'generation', 'default_cache', attribute_type=str, default='default' ) or error error = self.validate( self.config['generation'], 'generation', 'default_layers', is_array=True, attribute_type=str, default=self.layers.keys() ) or error error = self.validate( self.config['generation'], 'generation', 'log_format', attribute_type=str, default='%(levelname)s:%(name)s:%(funcName)s:%(message)s', ) or error error = self.validate(self.config['generation'], 'generation', 'authorised_user', attribute_type=str) or error error = self.validate( self.config['generation'], 'generation', 'maxconsecutive_errors', attribute_type=int, default=10 ) or error error = self.validate( self.config, 'config', 'process', attribute_type=dict, default={} ) or error for cmd_name, cmds in self.config['process'].items(): for i, cmd in enumerate(cmds): error = self.validate( cmd, 'process[%s][%i]' % (cmd_name, i), 'cmd', attribute_type=str, required=True ) or error error = self.validate( cmd, 'process[%s][%i]' % (cmd_name, i), 'need_out', attribute_type=bool, default=False ) or error error = self.validate( cmd, 'process[%s][%i]' % (cmd_name, i), 'arg', attribute_type=dict, default={} ) or error error = self.validate( cmd['arg'], 'process[%s][%i].arg' % (cmd_name, i), 'default', attribute_type=str ) or error error = self.validate( cmd['arg'], 'process[%s][%i].arg' % (cmd_name, i), 'verbose', attribute_type=str ) or error error = self.validate( cmd['arg'], 'process[%s][%i].arg' % (cmd_name, i), 'debug', attribute_type=str ) or error error = self.validate( cmd['arg'], 'process[%s][%i].arg' % (cmd_name, i), 'quiet', attribute_type=str ) or error error = self.validate(self.config, 'config', 'ec2', attribute_type=dict) or error if 'ec2' in self.config: error = self.validate( self.config['ec2'], 'ec2', 'number_process', attribute_type=int, default=1 ) or error error = self.validate( self.config['ec2'], 'ec2', 'host_type', attribute_type=str, default='m1.medium', enumeration=[ 't1.micro', 'm1.small', 'm1.medium', 'm1.large', 'm1.xlarge', 'm2.xlarge', 'm2.2xlarge', 'm2.4xlarge', 'c1.medium', 'c1.xlarge', 'cc1.4xlarge', 'cc2.8xlarge', 'cg1.4xlarge', 'hi1.4xlarge' ] ) or error error = self.validate( self.config['ec2'], 'ec2', 'ssh_options', attribute_type=str ) or error error = self.validate(self.config['ec2'], 'ec2', 'geodata_folder', attribute_type=str) or error if 'geodata_folder' in self.config['ec2'] and self.config['ec2']['geodata_folder'][-1] != '/': self.config['ec2']['geodata_folder'] += '/' error = self.validate(self.config['ec2'], 'ec2', 'code_folder', attribute_type=str) or error if 'code_folder' in self.config['ec2'] and self.config['ec2']['code_folder'][-1] != '/': self.config['ec2']['code_folder'] += '/' error = self.validate( self.config['ec2'], 'ec2', 'deploy_config', attribute_type=str, default="tilegeneration/deploy.cfg") or error error = self.validate( self.config['ec2'], 'ec2', 'build_cmds', attribute_type=str, is_array=True, default=[ "mkdir .build", "virtualenv .build/venv", ".build/venv/bin/pip install ." ] ) or error error = self.validate(self.config['ec2'], 'ec2', 'apache_config', attribute_type=str) or error error = self.validate(self.config['ec2'], 'ec2', 'apache_content', attribute_type=str) or error error = self.validate( self.config['ec2'], 'ec2', 'disable_geodata', attribute_type=bool, default=False ) or error error = self.validate( self.config['ec2'], 'ec2', 'disable_code', attribute_type=bool, default=False ) or error error = self.validate( self.config['ec2'], 'ec2', 'disable_database', attribute_type=bool, default=False ) or error error = self.validate( self.config['ec2'], 'ec2', 'disable_fillqueue', attribute_type=bool, default=False ) or error error = self.validate( self.config['ec2'], 'ec2', 'disable_tilesgen', attribute_type=bool, default=False ) or error if 'sns' in self.config: # pragma: no cover error = self.validate(self.config['sns'], 'sns', 'topic', attribute_type=str, required=True) or error error = self.validate(self.config['sns'], 'sns', 'region', attribute_type=str, default='eu-west-1') or error if error: exit(1) logging.basicConfig( format=self.config['generation']['log_format'], level=level) if options and options.zoom is not None: error_message = ( "The zoom argument '%s' has incorect format, " "it can be a single value, a range (3-9), a list of values (2,5,7)." ) % options.zoom if options.zoom.find('-') >= 0: r = options.zoom.split('-') if len(r) != 2: # pragma: no cover logger.error(error_message) error = True try: options.zoom = range(int(r[0]), int(r[1]) + 1) except ValueError: # pragma: no cover logger.error(error_message) error = True elif options.zoom.find(',') >= 0: try: options.zoom = [int(z) for z in options.zoom.split(',')] except ValueError: # pragma: no cover logger.error(error_message) error = True else: try: options.zoom = [int(options.zoom)] except ValueError: # pragma: no cover logger.error(error_message) error = True if error: # pragma: no cover exit(1) self.layer = None if layer_name and not error: self.set_layer(layer_name, options) def _primefactors(self, x): factorlist = [] loop = 2 while loop <= x: if x % loop == 0: x /= loop factorlist.append(loop) else: loop += 1 return factorlist def _resolution_scale(self, resolutions): prime_fact = {} for resolution in resolutions: denominator = Fraction(str(resolution)).denominator prime_factors = self._primefactors(denominator) for factor in set(prime_factors): if factor not in prime_fact: prime_fact[factor] = 0 prime_fact[factor] = max(prime_fact[factor], len([f for f in prime_factors if f == factor])) result = 1 for fact, nb in prime_fact.items(): result *= fact ** nb return result def get_store(self, cache, layer, dimensions=None, read_only=False): # build layout grid = layer['grid_ref'] if 'grid_ref' in layer else None layout = WMTSTileLayout( layer=layer['name'], url=cache['folder'], style=layer['wmts_style'], format='.' + layer['extension'], dimensions=dimensions if dimensions is not None else [ (dimension['name'], dimension['value']) for dimension in layer['dimensions'] ], tile_matrix_set=layer['grid'], tile_matrix=lambda z: get_tile_matrix_identifier(grid, zoom=z), request_encoding='REST', ) # store if cache['type'] == 's3': # on s3 cache_tilestore = S3TileStore(cache['bucket'], layout) # pragma: no cover elif cache['type'] == 'mbtiles': # on mbtiles file filename = layout.filename(TileCoord(0, 0, 0)).replace( '/0/0/0', '' ) + '.mbtiles' if not os.path.exists(os.path.dirname(filename)): os.makedirs(os.path.dirname(filename)) cache_tilestore = MBTilesTileStore( sqlite3.connect(filename), content_type=layer['mime_type'], tilecoord_in_topleft=True, ) elif cache['type'] == 'bsddb': # on bsddb file filename = layout.filename(TileCoord(0, 0, 0)).replace( '/0/0/0', '' ) + '.bsddb' if not os.path.exists(os.path.dirname(filename)): os.makedirs(os.path.dirname(filename)) db = bsddb.hashopen( filename, # and os.path.exists(filename) to avoid error on non existing file 'r' if read_only and os.path.exists(filename) else 'c' ) class Close: def __call__(self): self.db.close() ca = Close() ca.db = db self.close_actions.append(ca) cache_tilestore = BSDDBTileStore( db, content_type=layer['mime_type'], ) elif cache['type'] == 'filesystem': # on filesystem cache_tilestore = FilesystemTileStore( layout, content_type=layer['mime_type'], ) else: exit('unknown cache type: ' + cache['type']) # pragma: no cover return cache_tilestore def validate_exists(self, obj, obj_name, attribute): if attribute not in obj: logger.error("The attribute '%s' is required in the object %s." % (attribute, obj_name)) exit(1) def _validate_type(self, value, attribute_type, enumeration, regex=None): if attribute_type is not None: if attribute_type == int and type(value) == str: try: value = int(round(eval(value))) except: return (True, None, 'right int expression: %s' % value) if attribute_type == float: if type(value) == int: value = float(value) if type(value) == str: try: value = float(eval(value)) except: return (True, None, 'right float expression: %s' % value) elif type(value) != attribute_type: return (True, None, str(attribute_type)) elif attribute_type == str: typ = type(value) if typ == list or typ == dict: return (True, None, str(attribute_type)) if typ != str: value = str(value) if regex is not None: if re.search(regex, value) is None: return (True, None, "value '%s' don't respect regex '%s'" % (value, regex)) else: if type(value) != attribute_type: return (True, None, str(attribute_type)) if enumeration: return (value not in enumeration, value, str(enumeration)) return (False, value, None) def validate( self, obj, obj_name, attribute, attribute_type=None, is_array=False, default=None, required=False, enumeration=None, **kargs): if attribute not in obj: if required: logger.error("The attribute '%s' is required in the object %s." % (attribute, obj_name)) return True elif default is False: # no value obj[attribute] = False # no test return False elif default is not None: obj[attribute] = default else: # no value to test return False if is_array: if type(obj[attribute]) == str: obj[attribute] = [v.strip() for v in obj[attribute].split(',')] if type(obj[attribute]) == list: for n, v in enumerate(obj[attribute]): result, value, type_error = self._validate_type(v, attribute_type, enumeration, **kargs) if result: logger.error( "The attribute '%s' of the object %s has an element who is not a %s." % (attribute, obj_name, type_error) ) return True obj[attribute][n] = value else: logger.error("The attribute '%s' of the object %s is not an array." % (attribute, obj_name)) return True else: result, value, type_error = self._validate_type(obj[attribute], attribute_type, enumeration, **kargs) if result: logger.error( "The attribute '%s' of the object %s is not a %s." % (attribute, obj_name, type_error) ) return True obj[attribute] = value return False def validate_apache_config(self): error = False error = self.validate(self.config, 'config', 'apache', attribute_type=dict, default={}) or error error = self.validate( self.config['apache'], 'apache', 'location', attribute_type=str, default='/tiles' ) or error error = self.validate( self.config['apache'], 'apache', 'config_file', attribute_type=str, default='apache/tiles.conf' ) or error error = self.validate( self.config['apache'], 'apache', 'expires', attribute_type=int, default=8 ) or error return not error def validate_mapcache_config(self): error = False error = self.validate(self.config, 'config', 'mapcache', attribute_type=dict, default={}) or error error = self.validate( self.config['mapcache'], 'mapcache', 'config_file', attribute_type=str, default='apache/mapcache.xml' ) or error error = self.validate( self.config['mapcache'], 'mapcache', 'memcache_host', attribute_type=str, default='localhost' ) or error error = self.validate( self.config['mapcache'], 'mapcache', 'memcache_port', attribute_type=int, default='11211' ) or error error = self.validate( self.config['mapcache'], 'mapcache', 'location', attribute_type=str, default='/mapcache' ) or error return not error def set_layer(self, layer, options): self.create_log_tiles_error(layer) self.layer = self.layers[layer] if options.near is not None or ( options.time is not None and 'bbox' in self.layer and options.zoom is not None ): if options.zoom is None or len(options.zoom) != 1: # pragma: no cover exit('Option --near needs the option --zoom with one value.') if not (options.time is not None or options.test is not None): # pragma: no cover exit('Option --near needs the option --time or --test.') position = options.near if options.near is not None else [ (self.layer['bbox'][0] + self.layer['bbox'][2]) / 2, (self.layer['bbox'][1] + self.layer['bbox'][3]) / 2, ] bbox = self.layer['grid_ref']['bbox'] diff = [position[0] - bbox[0], position[1] - bbox[1]] resolution = self.layer['grid_ref']['resolutions'][options.zoom[0]] mt_to_m = self.layer['meta_size'] * self.layer['grid_ref']['tile_size'] * resolution mt = [float(d) / mt_to_m for d in diff] nb_tile = options.time * 3 if options.time is not None else options.test nb_mt = nb_tile / (self.layer['meta_size'] ** 2) nb_sqrt_mt = ceil(sqrt(nb_mt)) mt_origin = [round(m - nb_sqrt_mt / 2) for m in mt] self.init_geom([ bbox[0] + mt_origin[0] * mt_to_m, bbox[1] + mt_origin[1] * mt_to_m, bbox[0] + (mt_origin[0] + nb_sqrt_mt) * mt_to_m, bbox[1] + (mt_origin[1] + nb_sqrt_mt) * mt_to_m, ]) elif options.bbox is not None: self.init_geom(options.bbox) elif 'bbox' in self.layer: self.init_geom(self.layer['bbox']) else: self.init_geom(self.layer['grid_ref']['bbox']) def get_grid(self, name=None): if not name: name = self.layer['grid'] return self.grids[name] def get_tilesstore(self, cache_name): cache = self.caches[cache_name] dimensions_args = {} for dim in self.options.dimensions: dim = dim.split('=') if len(dim) != 2: # pragma: no cover exit( 'the DIMENTIONS option should be like this ' 'DATE=2013 VERSION=13.' ) dimensions_args[dim[0]] = dim[1] dimensions = [] for dim in self.layer['dimensions']: dimensions.append(( dim['name'], dimensions_args[dim['name']] if dim['name'] in dimensions_args else dim['value'] )) cache_tilestore = self.get_store(cache, self.layer, dimensions=dimensions) if cache_tilestore is None: exit('Unknown cache type: ' + cache['type']) # pragma: no cover return cache_tilestore def get_sqs_queue(self): # pragma: no cover if self.layer is None: exit("A layer must be specified.") if 'sqs' not in self.layer: exit("The layer '%s' hasn't any configured queue" % self.layer['name']) connection = boto.sqs.connect_to_region(self.layer['sqs']['region']) queue = connection.get_queue(self.layer['sqs']['queue']) queue.set_message_class(JSONMessage) return queue def init_geom(self, extent=None): self.geoms = self.get_geoms(self.layer, extent) def get_geoms(self, layer, extent=None): if not hasattr(self, 'layers_geoms'): layers_geoms = {} if layer['name'] in layers_geoms: # pragma: no cover # already build return layers_geoms[layer['name']] layer_geoms = {} layers_geoms[layer['name']] = layer_geoms if extent: geom = Polygon(( (extent[0], extent[1]), (extent[0], extent[3]), (extent[2], extent[3]), (extent[2], extent[1]), )) for z, r in enumerate(layer['grid_ref']['resolutions']): layer_geoms[z] = geom if self.options is None or ( self.options.near is None and self.options.geom ): if 'connection' in layer: connection = psycopg2.connect(layer['connection']) cursor = connection.cursor() for g in layer['geoms']: sql = 'SELECT ST_AsBinary(geom) FROM (SELECT %s) AS g' % g['sql'] logger.info('Execute SQL: %s.' % sql) cursor.execute(sql) geoms = [loads_wkb(str(r[0])) for r in cursor.fetchall()] geom = cascaded_union(geoms) if extent: geom = geom.intersection(Polygon(( (extent[0], extent[1]), (extent[0], extent[3]), (extent[2], extent[3]), (extent[2], extent[1]), ))) for z, r in enumerate(layer['grid_ref']['resolutions']): if ('min_resolution' not in g or g['min_resolution'] <= r) and \ ('max_resolution' not in g or g['max_resolution'] >= r): layer_geoms[z] = geom cursor.close() connection.close() return layer_geoms def add_local_process_filter(self): # pragma: no cover self.ifilter(LocalProcessFilter( self.config["ec2"]["number_process"], self.options.local_process_number )) def get_geoms_filter(self, layer, grid, geoms, queue_store=None): return IntersectGeometryFilter( grid=grid, geoms=geoms, queue_store=queue_store, px_buffer=( layer['px_buffer'] + layer['meta_buffer'] if layer['meta'] else 0 ) ) def add_geom_filter(self, queue_store=None): self.ifilter(self.get_geoms_filter( layer=self.layer, grid=self.get_grid(), geoms=self.geoms, queue_store=queue_store, ), "Intersect with geom") def add_logger(self): if not self.options.quiet and \ not self.options.verbose and \ not self.options.debug: def log_tiles(tile): variables = dict() variables.update(tile.__dict__) variables.update(tile.tilecoord.__dict__) sys.stdout.write("%(tilecoord)s \r" % variables) sys.stdout.flush() return tile self.imap(log_tiles) elif self.options.verbose: self.imap(Logger(logger, logging.INFO, '%(tilecoord)s')) def add_metatile_splitter(self): store = MetaTileSplitterTileStore( self.layer['mime_type'], self.layer['grid_ref']['tile_size'], self.layer['meta_buffer']) if self.options.debug: def meta_get(tilestream): # pragma: no cover for metatile in tilestream: substream = store.get((metatile,)) for tile in substream: tile.metatile = metatile yield tile self.tilestream = meta_get(self.tilestream) # pragma: no cover else: def safe_get(tilestream): for metatile in tilestream: try: substream = store.get((metatile,)) for tile in substream: tile.metatile = metatile yield tile except GeneratorExit as e: raise e except: # pragma: no cover data = repr(metatile.data) if len(data) < 2000: metatile.error = str(sys.exc_info()[1]) + " - " + metatile.data else: class NoRepr: def __init__(self, value): self.value = value def __repr__(self): return self.value metatile.error = NoRepr( repr(str(sys.exc_info()[1])) + " - " + data[0:2000] + '...' ) yield metatile self.tilestream = safe_get(self.tilestream) error_file = None def create_log_tiles_error(self, layer): if 'error_file' in self.config['generation']: now = datetime.now() time = now.strftime('%d-%m-%Y %H:%M:%S') self.error_file = open( self.config['generation']['error_file'].format( layer=layer, datetime=now ), 'a' ) self.error_file.write("# [%s] Start the layer '%s' generation\n" % (time, layer)) def log_tiles_error(self, tilecoord=None, message=None): if 'error_file' in self.config['generation']: time = datetime.now().strftime('%d-%m-%Y %H:%M:%S') if self.error_file is None: # pragma: no cover raise "Missing error file" tilecoord = "" if tilecoord is None else "%s " % tilecoord message = "" if message is None else " %s" % message self.error_file.write('%s# [%s]%s\n' % (tilecoord, time, message.replace('\n', ' '))) def add_error_filters(self): self.imap(LogErrors( logger, logging.ERROR, "Error in tile: %(tilecoord)s, %(error)r" )) if 'error_file' in self.config['generation']: def do(tile): if tile and tile.error: self.log_tiles_error(tilecoord=tile.tilecoord, message=repr(tile.error)) return tile self.imap(do) if 'maxconsecutive_errors' in self.config['generation']: self.tilestream = map(MaximumConsecutiveErrors( self.config['generation']['maxconsecutive_errors']), self.tilestream) def drop_count(tile): if tile and tile.error: self.error += 1 return None return tile self.ifilter(drop_count) def init_tilecoords(self): resolutions = self.layer['grid_ref']['resolutions'] if self.options.time is not None and self.options.zoom is None: if 'min_resolution_seed' in self.layer: # pragma: no cover self.options.zoom = [resolutions.index( self.layer['min_resolution_seed'] )] else: self.options.zoom = [len(resolutions) - 1] if self.options.zoom is not None: zoom_max = len(resolutions) - 1 for zoom in self.options.zoom: if zoom > zoom_max: logger.warn( "zoom %i is greater than the maximum zoom %i" " of grid %s of layer %s, ignored." % ( zoom, zoom_max, self.layer['grid'], self.layer['name'] ) ) self.options.zoom = [z for z in self.options.zoom if z <= zoom_max] if 'min_resolution_seed' in self.layer: if self.options.zoom is None: self.options.zoom = [] for z, resolution in enumerate(resolutions): if resolution >= self.layer['min_resolution_seed']: self.options.zoom.append(z) else: for zoom in self.options.zoom: resolution = resolutions[zoom] if resolution < self.layer['min_resolution_seed']: logger.warn( "zoom %i corresponds to resolution %s is smaller" " than the 'min_resolution_seed' %s of layer %s, ignored." % ( zoom, resolution, self.layer['min_resolution_seed'], self.layer['name'] ) ) self.options.zoom = [ z for z in self.options.zoom if resolutions[z] >= self.layer['min_resolution_seed'] ] if self.options.zoom is None: self.options.zoom = [z for z, r in enumerate(resolutions)] # fill the bounding pyramid tilegrid = self.layer['grid_ref']['obj'] bounding_pyramid = BoundingPyramid(tilegrid=tilegrid) for zoom in self.options.zoom: if zoom in self.geoms: extent = self.geoms[zoom].bounds if len(extent) == 0: logger.warn("bounds empty for zoom %i" % zoom) else: minx, miny, maxx, maxy = extent px_buffer = self.layer['px_buffer'] m_buffer = px_buffer * resolutions[zoom] minx -= m_buffer miny -= m_buffer maxx += m_buffer maxy += m_buffer bounding_pyramid.add(tilegrid.tilecoord( zoom, max(minx, tilegrid.max_extent[0]), max(miny, tilegrid.max_extent[1]), )) bounding_pyramid.add(tilegrid.tilecoord( zoom, min(maxx, tilegrid.max_extent[2]), min(maxy, tilegrid.max_extent[3]), )) meta = self.layer['meta'] if meta: self.set_tilecoords(bounding_pyramid.metatilecoords(self.layer['meta_size'])) else: self.set_tilecoords(bounding_pyramid) def set_tilecoords(self, tilecoords): self.tilestream = ( Tile(tilecoord) for tilecoord in tilecoords ) def set_store(self, store): # pragma: no cover self.tilestream = store.list() def counter(self, size=False): count = CountSize() if size else Count() self.imap(count) return count def process(self, name=None): if name is None: name = self.layer['post_process'] if name: self.imap(Process(self.config['process'][name], self.options)) def get(self, store, time_message=None): if self.options.debug: self.tilestream = store.get(self.tilestream) # pragma: no cover else: def safe_get(tile): try: n = datetime.now() t = store.get_one(tile) if time_message: logger.info("%s in %s" % (time_message, str(datetime.now() - n))) return t except GeneratorExit as e: # pragma: no cover raise e except SystemExit as e: # pragma: no cover raise e except KeyboardInterrupt: # pragma: no cover exit("User interrupt") except: # pragma: no cover tile.error = sys.exc_info()[1] return tile self.tilestream = map(safe_get, filter(None, self.tilestream)) def put(self, store, time_message=None): if self.options.debug: self.tilestream = store.put(self.tilestream) # pragma: no cover else: def safe_put(tile): try: n = datetime.now() t = store.put_one(tile) if time_message: logger.info("%s in %s" % (time_message, str(datetime.now() - n))) return t except GeneratorExit as e: # pragma: no cover raise e except SystemExit as e: # pragma: no cover raise e except KeyboardInterrupt: # pragma: no cover exit("User interrupt") except: # pragma: no cover tile.error = sys.exc_info()[1] return tile self.tilestream = map(safe_put, filter(None, self.tilestream)) def delete(self, store, time_message=None): # pragma: no cover if self.options.debug: self.tilestream = store.delete(self.tilestream) else: def safe_delete(tile): try: n = datetime.now() t = store.delete_one(tile) if time_message: logger.info("%s in %s" % (time_message, str(datetime.now() - n))) return t except GeneratorExit as e: # pragma: no cover raise e except SystemExit as e: # pragma: no cover raise e except KeyboardInterrupt: # pragma: no cover exit("User interrupt") except: # pragma: no cover tile.error = sys.exc_info()[1] return tile self.tilestream = map(safe_delete, filter(None, self.tilestream)) def imap(self, tile_filter, time_message=None): if self.options.debug: self.tilestream = map(tile_filter, self.tilestream) # pragma: no cover else: def safe_imap(tile): try: n = datetime.now() t = tile_filter(tile) if time_message: # pragma: no cover logger.info("%s in %s" % (time_message, str(datetime.now() - n))) return t except GeneratorExit as e: # pragma: no cover raise e except SystemExit as e: # pragma: no cover raise e except KeyboardInterrupt: # pragma: no cover exit("User interrupt") except: # pragma: no cover tile.error = sys.exc_info()[1] return tile self.tilestream = map(safe_imap, filter(None, self.tilestream)) def ifilter(self, tile_filter, time_message=None): if self.options.debug: self.tilestream = filter(tile_filter, self.tilestream) # pragma: no cover else: def safe_filter(tile): if tile: try: n = datetime.now() t = tile_filter(tile) if time_message: logger.debug("%s in %s" % (time_message, str(datetime.now() - n))) return t except GeneratorExit as e: # pragma: no cover raise e except SystemExit as e: # pragma: no cover raise e except KeyboardInterrupt: # pragma: no cover exit("User interrupt") except: # pragma: no cover tile.error = sys.exc_info()[1] return tile self.tilestream = filter(safe_filter, self.tilestream) def consume(self, test=None): if test is None: test = self.options.test start = datetime.now() consume(self.tilestream, test) self.duration = datetime.now() - start for ca in self.close_actions: ca() class Count: def __init__(self): self.nb = 0 def __call__(self, tile=None): self.nb += 1 return tile class CountSize: def __init__(self): self.nb = 0 self.size = 0 def __call__(self, tile=None): if tile and tile.data: self.nb += 1 self.size += len(tile.data) return tile class HashDropper: """ Create a filter to remove the tiles data where they have the specified size and hash. Used to drop the empty tiles. The ``store`` is used to delete the empty tiles. """ def __init__(self, size, sha1code, store=None, queue_store=None, count=None): self.size = size self.sha1code = sha1code self.store = store self.queue_store = queue_store self.count = count def __call__(self, tile): if len(tile.data) != self.size or \ sha1(tile.data).hexdigest() != self.sha1code: return tile else: if self.store is not None: if tile.tilecoord.n != 1: for tilecoord in tile.tilecoord: self.store.delete_one(Tile(tilecoord)) else: self.store.delete_one(tile) logger.info("The tile %s is dropped" % str(tile.tilecoord)) if hasattr(tile, 'metatile'): tile.metatile.elapsed_togenerate -= 1 if tile.metatile.elapsed_togenerate == 0 and self.queue_store is not None: self.queue_store.delete_one(tile.metatile) # pragma: no cover elif self.queue_store is not None: # pragma: no cover self.queue_store.delete_one(tile) if self.count: self.count() return None class HashLogger: """ Log the tile size and hash. """ def __init__(self, block): self.block = block def __call__(self, tile): ref = None try: image = Image.open(cStringIO(tile.data)) except IOError as e: # pragma: no cover logger.error(tile.data) raise e for px in image.getdata(): if ref is None: ref = px elif px != ref: exit("Error: image is not uniform.") print("""Tile: %s %s: size: %i hash: %s""" % (str(tile.tilecoord), self.block, len(tile.data), sha1(tile.data).hexdigest())) return tile class LocalProcessFilter: # pragma: no cover def __init__(self, nb_process, process_nb): self.nb_process = nb_process self.process_nb = int(process_nb) def filter(self, tilecoord): nb = tilecoord.z + tilecoord.x / tilecoord.n + tilecoord.y / tilecoord.n return nb % self.nb_process == self.process_nb def __call__(self, tile): return tile if self.filter(tile.tilecoord) else None class IntersectGeometryFilter: def __init__(self, grid, geoms=None, queue_store=None, px_buffer=0): self.grid = grid self.geoms = geoms self.queue_store = queue_store self.px_buffer = px_buffer def filter_tilecoord(self, tilecoord): return self.bbox_polygon( self.grid['obj'].extent( tilecoord, self.grid['resolutions'][tilecoord.z] * self.px_buffer ) ).intersects(self.geoms[tilecoord.z]) def __call__(self, tile): return tile if self.filter_tilecoord(tile.tilecoord) else None def bbox_polygon(self, bbox): return Polygon(( (bbox[0], bbox[1]), (bbox[0], bbox[3]), (bbox[2], bbox[3]), (bbox[2], bbox[1]) )) class DropEmpty: """ Create a filter for dropping all tiles with errors. """ def __init__(self, gene): self.gene = gene def __call__(self, tile): if not tile or not tile.data: # pragma: no cover logger.error("The tile: %(tilecoord)s is empty" % { 'tilecoord': tile.tilecoord if tile else 'not defined' }) if 'error_file' in self.gene.config['generation'] and tile: self.gene.log_tiles_error(tilecoord=tile.tilecoord, message='The tile is empty') return None else: return tile def quote(arg): if ' ' in arg: if "'" in arg: if '"' in arg: return "'%s'" % arg.replace("'", "\\'") else: return '"%s"' % arg else: return "'%s'" % arg elif arg == '': return "''" else: return arg def parse_tilecoord(string_representation): parts = string_representation.split(':') coords = [int(v) for v in parts[0].split('/')] if len(coords) != 3: # pragma: no cover raise ValueError("Wrong number of coordinates") z, x, y = coords if len(parts) == 1: tilecoord = TileCoord(z, x, y) elif len(parts) == 2: meta = parts[1].split('/') if len(meta) != 2: # pragma: no cover raise ValueError("No one '/' in meta coordinates") tilecoord = TileCoord(z, x, y, int(meta[0])) else: # pragma: no cover raise ValueError("More than on ':' in the tilecoord") return tilecoord class Process: def __init__(self, config, options): self.config = config self.options = options def __call__(self, tile): if tile and tile.data: fd_in, name_in = tempfile.mkstemp() file_in = open(name_in, 'wb') file_in.write(tile.data) file_in.close() for cmd in self.config: args = [] if not self.options.verbose and \ not self.options.debug and \ not self.options.quiet and \ 'default' in cmd['arg']: args.append(cmd['arg']['default']) if self.options.verbose and 'verbose' in cmd['arg']: args.append(cmd['arg']['verbose']) if self.options.debug and 'debug' in cmd['arg']: args.append(cmd['arg']['debug']) if self.options.quiet and 'quiet' in cmd['arg']: args.append(cmd['arg']['quiet']) if cmd['need_out']: fd_out, name_out = tempfile.mkstemp() os.unlink(name_out) else: # pragma: no cover name_out = name_in command = cmd['cmd'] % { 'in': name_in, 'out': name_out, 'args': ' '.join(args), 'x': tile.tilecoord.x, 'y': tile.tilecoord.y, 'z': tile.tilecoord.z } logger.info('process: %s' % command) code = subprocess.call(command, shell=True) if code != 0: # pragma: no cover tile.error = "Command '%s' on tile %s " \ "return error code %i" % \ (command, tile.tilecoord, code) tile.data = None return tile if cmd['need_out']: os.close(fd_in) name_in = name_out fd_in = fd_out file_out = open(name_in, 'rb') tile.data = file_out.read() file_out.close() os.close(fd_in) return tile class TilesFileStore: def __init__(self, tiles_file): self.tiles_file = open(tiles_file) def list(self): while True: line = self.tiles_file.readline() if not line: return line = line.split('#')[0].strip() if line != '': try: yield Tile(parse_tilecoord(line)) except ValueError as e: # pragma: no cover logger.error("A tile '%s' is not in the format 'z/x/y' or z/x/y:+n/+n\n%r" % (line, e)) PK:KD4ĉ tilecloud_chain/openlayers_js.pyopenlayers_js = """var callback = function(infoLookup) { var msg = ""; if (infoLookup) { var info; for (var idx in infoLookup) { // idx can be used to retrieve layer from map.layers[idx] info = infoLookup[idx]; if (info && info.data) { msg += "[" + info.id + "]" for (k in info.data) { msg += '
' + k + ': ' + info.data[k]; } } } } document.getElementById("attrs").innerHTML = msg; }; map = new OpenLayers.Map({ div: "map", projection: "{{srs}}", controls: [ new OpenLayers.Control.Navigation(), new OpenLayers.Control.Zoom(), new OpenLayers.Control.MousePosition(), new OpenLayers.Control.LayerSwitcher(), new OpenLayers.Control.Permalink(), new OpenLayers.Control.UTFGrid({ callback: callback, handlerMode: "hover", handlerOptions: { 'delay': 0, 'pixelTolerance': 0 }, reset: function() {} }) ], center: [{{center_x}}, {{center_y}}], zoom: 0 }); var format = new OpenLayers.Format.WMTSCapabilities(); OpenLayers.Request.GET({ url: "{{http_url}}1.0.0/WMTSCapabilities.xml", success: function(request) { var doc = request.responseXML; if (!doc || !doc.documentElement) { doc = request.responseText; } var capabilities = format.read(doc); {% for layer in layers %} map.addLayer(format.createLayer(capabilities, { layer: "{{layer.name}}", maxExtent: {{layer.maxExtent}},{% if layer.grid %} isBaseLayer: false, utfgridResolution: {{layer.resolution}}{% else %} isBaseLayer: true{% endif %} }));{% endfor %} }, failure: function() { alert("Trouble getting capabilities doc"); OpenLayers.Console.error.apply(OpenLayers.Console, arguments); } });""" PKzkGhthttilecloud_chain/OpenLayers.jsvar OpenLayers={VERSION_NUMBER:"Release 2.13 dev",singleFile:true,_getScriptLocation:(function(){var r=new RegExp("(^|(.*?\\/))(OpenLayers[^\\/]*?\\.js)(\\?|$)"),s=document.getElementsByTagName('script'),src,m,l="";for(var i=0,len=s.length;i0){fig=parseFloat(num.toPrecision(sig));} return fig;},format:function(num,dec,tsep,dsep){dec=(typeof dec!="undefined")?dec:0;tsep=(typeof tsep!="undefined")?tsep:OpenLayers.Number.thousandsSeparator;dsep=(typeof dsep!="undefined")?dsep:OpenLayers.Number.decimalSeparator;if(dec!=null){num=parseFloat(num.toFixed(dec));} var parts=num.toString().split(".");if(parts.length==1&&dec==null){dec=0;} var integer=parts[0];if(tsep){var thousands=/(-?[0-9]+)([0-9]{3})/;while(thousands.test(integer)){integer=integer.replace(thousands,"$1"+tsep+"$2");}} var str;if(dec==0){str=integer;}else{var rem=parts.length>1?parts[1]:"0";if(dec!=null){rem=rem+new Array(dec-rem.length+1).join("0");} str=integer+dsep+rem;} return str;},zeroPad:function(num,len,radix){var str=num.toString(radix||10);while(str.length1){var newArgs=[C,P].concat(Array.prototype.slice.call(arguments).slice(1,len-1),F);OpenLayers.inherit.apply(null,newArgs);}else{C.prototype=F;} return C;};OpenLayers.inherit=function(C,P){var F=function(){};F.prototype=P.prototype;C.prototype=new F;var i,l,o;for(i=2,l=arguments.length;ithis.right)){this.right=object.right;} if((this.top==null)||(object.top>this.top)){this.top=object.top;} break;}}},extendXY:function(x,y){this.centerLonLat=null;if((this.left==null)||(xthis.right)){this.right=x;} if((this.top==null)||(y>this.top)){this.top=y;}},containsLonLat:function(ll,options){if(typeof options==="boolean"){options={inclusive:options};} options=options||{};var contains=this.contains(ll.lon,ll.lat,options.inclusive),worldBounds=options.worldBounds;if(worldBounds&&!contains){var worldWidth=worldBounds.getWidth();var worldCenterX=(worldBounds.left+worldBounds.right)/2;var worldsAway=Math.round((ll.lon-worldCenterX)/worldWidth);contains=this.containsLonLat({lon:ll.lon-worldsAway*worldWidth,lat:ll.lat},{inclusive:options.inclusive});} return contains;},containsPixel:function(px,inclusive){return this.contains(px.x,px.y,inclusive);},contains:function(x,y,inclusive){if(inclusive==null){inclusive=true;} if(x==null||y==null){return false;} x=OpenLayers.Util.toFloat(x);y=OpenLayers.Util.toFloat(y);var contains=false;if(inclusive){contains=((x>=this.left)&&(x<=this.right)&&(y>=this.bottom)&&(y<=this.top));}else{contains=((x>this.left)&&(xthis.bottom)&&(y=self.bottom)&&(bounds.bottom<=self.top))||((self.bottom>=bounds.bottom)&&(self.bottom<=bounds.top)));var inTop=(((bounds.top>=self.bottom)&&(bounds.top<=self.top))||((self.top>bounds.bottom)&&(self.top=self.left)&&(bounds.left<=self.right))||((self.left>=bounds.left)&&(self.left<=bounds.right)));var inRight=(((bounds.right>=self.left)&&(bounds.right<=self.right))||((self.right>=bounds.left)&&(self.right<=bounds.right)));intersects=((inBottom||inTop)&&(inLeft||inRight));} if(options.worldBounds&&!intersects){var world=options.worldBounds;var width=world.getWidth();var selfCrosses=!world.containsBounds(self);var boundsCrosses=!world.containsBounds(bounds);if(selfCrosses&&!boundsCrosses){bounds=bounds.add(-width,0);intersects=self.intersectsBounds(bounds,{inclusive:options.inclusive});}else if(boundsCrosses&&!selfCrosses){self=self.add(-width,0);intersects=bounds.intersectsBounds(self,{inclusive:options.inclusive});}} return intersects;},containsBounds:function(bounds,partial,inclusive){if(partial==null){partial=false;} if(inclusive==null){inclusive=true;} var bottomLeft=this.contains(bounds.left,bounds.bottom,inclusive);var bottomRight=this.contains(bounds.right,bounds.bottom,inclusive);var topLeft=this.contains(bounds.left,bounds.top,inclusive);var topRight=this.contains(bounds.right,bounds.top,inclusive);return(partial)?(bottomLeft||bottomRight||topLeft||topRight):(bottomLeft&&bottomRight&&topLeft&&topRight);},determineQuadrant:function(lonlat){var quadrant="";var center=this.getCenterLonLat();quadrant+=(lonlat.lat=maxExtent.right&&newBounds.right>maxExtent.right){newBounds=newBounds.add(-width,0);} var newLeft=newBounds.left+leftTolerance;if(newLeftmaxExtent.left&&newBounds.right-rightTolerance>maxExtent.right){newBounds=newBounds.add(-width,0);}} return newBounds;},CLASS_NAME:"OpenLayers.Bounds"});OpenLayers.Bounds.fromString=function(str,reverseAxisOrder){var bounds=str.split(",");return OpenLayers.Bounds.fromArray(bounds,reverseAxisOrder);};OpenLayers.Bounds.fromArray=function(bbox,reverseAxisOrder){return reverseAxisOrder===true?new OpenLayers.Bounds(bbox[1],bbox[0],bbox[3],bbox[2]):new OpenLayers.Bounds(bbox[0],bbox[1],bbox[2],bbox[3]);};OpenLayers.Bounds.fromSize=function(size){return new OpenLayers.Bounds(0,size.h,size.w,0);};OpenLayers.Bounds.oppositeQuadrant=function(quadrant){var opp="";opp+=(quadrant.charAt(0)=='t')?'b':'t';opp+=(quadrant.charAt(1)=='l')?'r':'l';return opp;};OpenLayers.LonLat=OpenLayers.Class({lon:0.0,lat:0.0,initialize:function(lon,lat){if(OpenLayers.Util.isArray(lon)){lat=lon[1];lon=lon[0];} this.lon=OpenLayers.Util.toFloat(lon);this.lat=OpenLayers.Util.toFloat(lat);},toString:function(){return("lon="+this.lon+",lat="+this.lat);},toShortString:function(){return(this.lon+", "+this.lat);},clone:function(){return new OpenLayers.LonLat(this.lon,this.lat);},add:function(lon,lat){if((lon==null)||(lat==null)){throw new TypeError('LonLat.add cannot receive null values');} return new OpenLayers.LonLat(this.lon+OpenLayers.Util.toFloat(lon),this.lat+OpenLayers.Util.toFloat(lat));},equals:function(ll){var equals=false;if(ll!=null){equals=((this.lon==ll.lon&&this.lat==ll.lat)||(isNaN(this.lon)&&isNaN(this.lat)&&isNaN(ll.lon)&&isNaN(ll.lat)));} return equals;},transform:function(source,dest){var point=OpenLayers.Projection.transform({'x':this.lon,'y':this.lat},source,dest);this.lon=point.x;this.lat=point.y;return this;},wrapDateLine:function(maxExtent){var newLonLat=this.clone();if(maxExtent){while(newLonLat.lonmaxExtent.right){newLonLat.lon-=maxExtent.getWidth();}} return newLonLat;},CLASS_NAME:"OpenLayers.LonLat"});OpenLayers.LonLat.fromString=function(str){var pair=str.split(",");return new OpenLayers.LonLat(pair[0],pair[1]);};OpenLayers.LonLat.fromArray=function(arr){var gotArr=OpenLayers.Util.isArray(arr),lon=gotArr&&arr[0],lat=gotArr&&arr[1];return new OpenLayers.LonLat(lon,lat);};OpenLayers.Element={visible:function(element){return OpenLayers.Util.getElement(element).style.display!='none';},toggle:function(){for(var i=0,len=arguments.length;i=0;i--){if(array[i]==item){array.splice(i,1);}} return array;};OpenLayers.Util.indexOf=function(array,obj){if(typeof array.indexOf=="function"){return array.indexOf(obj);}else{for(var i=0,len=array.length;i=0.0&&parseFloat(opacity)<1.0){element.style.filter='alpha(opacity='+(opacity*100)+')';element.style.opacity=opacity;}else if(parseFloat(opacity)==1.0){element.style.filter='';element.style.opacity='';}};OpenLayers.Util.createDiv=function(id,px,sz,imgURL,position,border,overflow,opacity){var dom=document.createElement('div');if(imgURL){dom.style.backgroundImage='url('+imgURL+')';} if(!id){id=OpenLayers.Util.createUniqueID("OpenLayersDiv");} if(!position){position="absolute";} OpenLayers.Util.modifyDOMElement(dom,id,px,sz,position,border,overflow,opacity);return dom;};OpenLayers.Util.createImage=function(id,px,sz,imgURL,position,border,opacity,delayDisplay){var image=document.createElement("img");if(!id){id=OpenLayers.Util.createUniqueID("OpenLayersDiv");} if(!position){position="relative";} OpenLayers.Util.modifyDOMElement(image,id,px,sz,position,border,null,opacity);if(delayDisplay){image.style.display="none";function display(){image.style.display="";OpenLayers.Event.stopObservingElement(image);} OpenLayers.Event.observe(image,"load",display);OpenLayers.Event.observe(image,"error",display);} image.style.alt=id;image.galleryImg="no";if(imgURL){image.src=imgURL;} return image;};OpenLayers.IMAGE_RELOAD_ATTEMPTS=0;OpenLayers.Util.alphaHackNeeded=null;OpenLayers.Util.alphaHack=function(){if(OpenLayers.Util.alphaHackNeeded==null){var arVersion=navigator.appVersion.split("MSIE");var version=parseFloat(arVersion[1]);var filter=false;try{filter=!!(document.body.filters);}catch(e){} OpenLayers.Util.alphaHackNeeded=(filter&&(version>=5.5)&&(version<7));} return OpenLayers.Util.alphaHackNeeded;};OpenLayers.Util.modifyAlphaImageDiv=function(div,id,px,sz,imgURL,position,border,sizing,opacity){OpenLayers.Util.modifyDOMElement(div,id,px,sz,position,null,null,opacity);var img=div.childNodes[0];if(imgURL){img.src=imgURL;} OpenLayers.Util.modifyDOMElement(img,div.id+"_innerImage",null,sz,"relative",border);if(OpenLayers.Util.alphaHack()){if(div.style.display!="none"){div.style.display="inline-block";} if(sizing==null){sizing="scale";} div.style.filter="progid:DXImageTransform.Microsoft"+".AlphaImageLoader(src='"+img.src+"', "+"sizingMethod='"+sizing+"')";if(parseFloat(div.style.opacity)>=0.0&&parseFloat(div.style.opacity)<1.0){div.style.filter+=" alpha(opacity="+div.style.opacity*100+")";} img.style.filter="alpha(opacity=0)";}};OpenLayers.Util.createAlphaImageDiv=function(id,px,sz,imgURL,position,border,sizing,opacity,delayDisplay){var div=OpenLayers.Util.createDiv();var img=OpenLayers.Util.createImage(null,null,null,null,null,null,null,delayDisplay);img.className="olAlphaImg";div.appendChild(img);OpenLayers.Util.modifyAlphaImageDiv(div,id,px,sz,imgURL,position,border,sizing,opacity);return div;};OpenLayers.Util.upperCaseObject=function(object){var uObject={};for(var key in object){uObject[key.toUpperCase()]=object[key];} return uObject;};OpenLayers.Util.applyDefaults=function(to,from){to=to||{};var fromIsEvt=typeof window.Event=="function"&&from instanceof window.Event;for(var key in from){if(to[key]===undefined||(!fromIsEvt&&from.hasOwnProperty&&from.hasOwnProperty(key)&&!to.hasOwnProperty(key))){to[key]=from[key];}} if(!fromIsEvt&&from&&from.hasOwnProperty&&from.hasOwnProperty('toString')&&!to.hasOwnProperty('toString')){to.toString=from.toString;} return to;};OpenLayers.Util.getParameterString=function(params){var paramsArray=[];for(var key in params){var value=params[key];if((value!=null)&&(typeof value!='function')){var encodedValue;if(typeof value=='object'&&value.constructor==Array){var encodedItemArray=[];var item;for(var itemIndex=0,len=value.length;itemIndex1e-12&&--iterLimit>0){var sinLambda=Math.sin(lambda),cosLambda=Math.cos(lambda);var sinSigma=Math.sqrt((cosU2*sinLambda)*(cosU2*sinLambda)+ (cosU1*sinU2-sinU1*cosU2*cosLambda)*(cosU1*sinU2-sinU1*cosU2*cosLambda));if(sinSigma==0){return 0;} var cosSigma=sinU1*sinU2+cosU1*cosU2*cosLambda;var sigma=Math.atan2(sinSigma,cosSigma);var alpha=Math.asin(cosU1*cosU2*sinLambda/sinSigma);var cosSqAlpha=Math.cos(alpha)*Math.cos(alpha);var cos2SigmaM=cosSigma-2*sinU1*sinU2/cosSqAlpha;var C=f/16*cosSqAlpha*(4+f*(4-3*cosSqAlpha));lambdaP=lambda;lambda=L+(1-C)*f*Math.sin(alpha)*(sigma+C*sinSigma*(cos2SigmaM+C*cosSigma*(-1+2*cos2SigmaM*cos2SigmaM)));} if(iterLimit==0){return NaN;} var uSq=cosSqAlpha*(a*a-b*b)/(b*b);var A=1+uSq/16384*(4096+uSq*(-768+uSq*(320-175*uSq)));var B=uSq/1024*(256+uSq*(-128+uSq*(74-47*uSq)));var deltaSigma=B*sinSigma*(cos2SigmaM+B/4*(cosSigma*(-1+2*cos2SigmaM*cos2SigmaM)- B/6*cos2SigmaM*(-3+4*sinSigma*sinSigma)*(-3+4*cos2SigmaM*cos2SigmaM)));var s=b*A*(sigma-deltaSigma);var d=s.toFixed(3)/1000;return d;};OpenLayers.Util.destinationVincenty=function(lonlat,brng,dist){var u=OpenLayers.Util;var ct=u.VincentyConstants;var a=ct.a,b=ct.b,f=ct.f;var lon1=lonlat.lon;var lat1=lonlat.lat;var s=dist;var alpha1=u.rad(brng);var sinAlpha1=Math.sin(alpha1);var cosAlpha1=Math.cos(alpha1);var tanU1=(1-f)*Math.tan(u.rad(lat1));var cosU1=1/Math.sqrt((1+tanU1*tanU1)),sinU1=tanU1*cosU1;var sigma1=Math.atan2(tanU1,cosAlpha1);var sinAlpha=cosU1*sinAlpha1;var cosSqAlpha=1-sinAlpha*sinAlpha;var uSq=cosSqAlpha*(a*a-b*b)/(b*b);var A=1+uSq/16384*(4096+uSq*(-768+uSq*(320-175*uSq)));var B=uSq/1024*(256+uSq*(-128+uSq*(74-47*uSq)));var sigma=s/(b*A),sigmaP=2*Math.PI;while(Math.abs(sigma-sigmaP)>1e-12){var cos2SigmaM=Math.cos(2*sigma1+sigma);var sinSigma=Math.sin(sigma);var cosSigma=Math.cos(sigma);var deltaSigma=B*sinSigma*(cos2SigmaM+B/4*(cosSigma*(-1+2*cos2SigmaM*cos2SigmaM)- B/6*cos2SigmaM*(-3+4*sinSigma*sinSigma)*(-3+4*cos2SigmaM*cos2SigmaM)));sigmaP=sigma;sigma=s/(b*A)+deltaSigma;} var tmp=sinU1*sinSigma-cosU1*cosSigma*cosAlpha1;var lat2=Math.atan2(sinU1*cosSigma+cosU1*sinSigma*cosAlpha1,(1-f)*Math.sqrt(sinAlpha*sinAlpha+tmp*tmp));var lambda=Math.atan2(sinSigma*sinAlpha1,cosU1*cosSigma-sinU1*sinSigma*cosAlpha1);var C=f/16*cosSqAlpha*(4+f*(4-3*cosSqAlpha));var L=lambda-(1-C)*f*sinAlpha*(sigma+C*sinSigma*(cos2SigmaM+C*cosSigma*(-1+2*cos2SigmaM*cos2SigmaM)));var revAz=Math.atan2(sinAlpha,-tmp);return new OpenLayers.LonLat(lon1+u.deg(L),u.deg(lat2));};OpenLayers.Util.getParameters=function(url,options){options=options||{};url=(url===null||url===undefined)?window.location.href:url;var paramsString="";if(OpenLayers.String.contains(url,'?')){var start=url.indexOf('?')+1;var end=OpenLayers.String.contains(url,"#")?url.indexOf('#'):url.length;paramsString=url.substring(start,end);} var parameters={};var pairs=paramsString.split(/[&;]/);for(var i=0,len=pairs.length;i1.0)?(1.0/scale):scale;return normScale;};OpenLayers.Util.getResolutionFromScale=function(scale,units){var resolution;if(scale){if(units==null){units="degrees";} var normScale=OpenLayers.Util.normalizeScale(scale);resolution=1/(normScale*OpenLayers.INCHES_PER_UNIT[units]*OpenLayers.DOTS_PER_INCH);} return resolution;};OpenLayers.Util.getScaleFromResolution=function(resolution,units){if(units==null){units="degrees";} var scale=resolution*OpenLayers.INCHES_PER_UNIT[units]*OpenLayers.DOTS_PER_INCH;return scale;};OpenLayers.Util.pagePosition=function(forElement){var pos=[0,0];var viewportElement=OpenLayers.Util.getViewportElement();if(!forElement||forElement==window||forElement==viewportElement){return pos;} var BUGGY_GECKO_BOX_OBJECT=OpenLayers.IS_GECKO&&document.getBoxObjectFor&&OpenLayers.Element.getStyle(forElement,'position')=='absolute'&&(forElement.style.top==''||forElement.style.left=='');var parent=null;var box;if(forElement.getBoundingClientRect){box=forElement.getBoundingClientRect();var scrollTop=window.pageYOffset||viewportElement.scrollTop;var scrollLeft=window.pageXOffset||viewportElement.scrollLeft;pos[0]=box.left+scrollLeft;pos[1]=box.top+scrollTop;}else if(document.getBoxObjectFor&&!BUGGY_GECKO_BOX_OBJECT){box=document.getBoxObjectFor(forElement);var vpBox=document.getBoxObjectFor(viewportElement);pos[0]=box.screenX-vpBox.screenX;pos[1]=box.screenY-vpBox.screenY;}else{pos[0]=forElement.offsetLeft;pos[1]=forElement.offsetTop;parent=forElement.offsetParent;if(parent!=forElement){while(parent){pos[0]+=parent.offsetLeft;pos[1]+=parent.offsetTop;parent=parent.offsetParent;}} var browser=OpenLayers.BROWSER_NAME;if(browser=="opera"||(browser=="safari"&&OpenLayers.Element.getStyle(forElement,'position')=='absolute')){pos[1]-=document.body.offsetTop;} parent=forElement.offsetParent;while(parent&&parent!=document.body){pos[0]-=parent.scrollLeft;if(browser!="opera"||parent.tagName!='TR'){pos[1]-=parent.scrollTop;} parent=parent.offsetParent;}} return pos;};OpenLayers.Util.getViewportElement=function(){var viewportElement=arguments.callee.viewportElement;if(viewportElement==undefined){viewportElement=(OpenLayers.BROWSER_NAME=="msie"&&document.compatMode!='CSS1Compat')?document.body:document.documentElement;arguments.callee.viewportElement=viewportElement;} return viewportElement;};OpenLayers.Util.isEquivalentUrl=function(url1,url2,options){options=options||{};OpenLayers.Util.applyDefaults(options,{ignoreCase:true,ignorePort80:true,ignoreHash:true,splitArgs:false});var urlObj1=OpenLayers.Util.createUrlObject(url1,options);var urlObj2=OpenLayers.Util.createUrlObject(url2,options);for(var key in urlObj1){if(key!=="args"){if(urlObj1[key]!=urlObj2[key]){return false;}}} for(var key in urlObj1.args){if(urlObj1.args[key]!=urlObj2.args[key]){return false;} delete urlObj2.args[key];} for(var key in urlObj2.args){return false;} return true;};OpenLayers.Util.createUrlObject=function(url,options){options=options||{};if(!(/^\w+:\/\//).test(url)){var loc=window.location;var port=loc.port?":"+loc.port:"";var fullUrl=loc.protocol+"//"+loc.host.split(":").shift()+port;if(url.indexOf("/")===0){url=fullUrl+url;}else{var parts=loc.pathname.split("/");parts.pop();url=fullUrl+parts.join("/")+"/"+url;}} if(options.ignoreCase){url=url.toLowerCase();} var a=document.createElement('a');a.href=url;var urlObject={};urlObject.host=a.host.split(":").shift();urlObject.protocol=a.protocol;if(options.ignorePort80){urlObject.port=(a.port=="80"||a.port=="0")?"":a.port;}else{urlObject.port=(a.port==""||a.port=="0")?"80":a.port;} urlObject.hash=(options.ignoreHash||a.hash==="#")?"":a.hash;var queryString=a.search;if(!queryString){var qMark=url.indexOf("?");queryString=(qMark!=-1)?url.substr(qMark):"";} urlObject.args=OpenLayers.Util.getParameters(queryString,{splitArgs:options.splitArgs});urlObject.pathname=(a.pathname.charAt(0)=="/")?a.pathname:"/"+a.pathname;return urlObject;};OpenLayers.Util.removeTail=function(url){var head=null;var qMark=url.indexOf("?");var hashMark=url.indexOf("#");if(qMark==-1){head=(hashMark!=-1)?url.substr(0,hashMark):url;}else{head=(hashMark!=-1)?url.substr(0,Math.min(qMark,hashMark)):url.substr(0,qMark);} return head;};OpenLayers.IS_GECKO=(function(){var ua=navigator.userAgent.toLowerCase();return ua.indexOf("webkit")==-1&&ua.indexOf("gecko")!=-1;})();OpenLayers.CANVAS_SUPPORTED=(function(){var elem=document.createElement('canvas');return!!(elem.getContext&&elem.getContext('2d'));})();OpenLayers.BROWSER_NAME=(function(){var name="";var ua=navigator.userAgent.toLowerCase();if(ua.indexOf("opera")!=-1){name="opera";}else if(ua.indexOf("msie")!=-1){name="msie";}else if(ua.indexOf("safari")!=-1){name="safari";}else if(ua.indexOf("mozilla")!=-1){if(ua.indexOf("firefox")!=-1){name="firefox";}else{name="mozilla";}} return name;})();OpenLayers.Util.getBrowserName=function(){return OpenLayers.BROWSER_NAME;};OpenLayers.Util.getRenderedDimensions=function(contentHTML,size,options){var w,h;var container=document.createElement("div");container.style.visibility="hidden";var containerElement=(options&&options.containerElement)?options.containerElement:document.body;var parentHasPositionAbsolute=false;var superContainer=null;var parent=containerElement;while(parent&&parent.tagName.toLowerCase()!="body"){var parentPosition=OpenLayers.Element.getStyle(parent,"position");if(parentPosition=="absolute"){parentHasPositionAbsolute=true;break;}else if(parentPosition&&parentPosition!="static"){break;} parent=parent.parentNode;} if(parentHasPositionAbsolute&&(containerElement.clientHeight===0||containerElement.clientWidth===0)){superContainer=document.createElement("div");superContainer.style.visibility="hidden";superContainer.style.position="absolute";superContainer.style.overflow="visible";superContainer.style.width=document.body.clientWidth+"px";superContainer.style.height=document.body.clientHeight+"px";superContainer.appendChild(container);} container.style.position="absolute";if(size){if(size.w){w=size.w;container.style.width=w+"px";}else if(size.h){h=size.h;container.style.height=h+"px";}} if(options&&options.displayClass){container.className=options.displayClass;} var content=document.createElement("div");content.innerHTML=contentHTML;content.style.overflow="visible";if(content.childNodes){for(var i=0,l=content.childNodes.length;i=60){coordinateseconds-=60;coordinateminutes+=1;if(coordinateminutes>=60){coordinateminutes-=60;coordinatedegrees+=1;}} if(coordinatedegrees<10){coordinatedegrees="0"+coordinatedegrees;} var str=coordinatedegrees+"\u00B0";if(dmsOption.indexOf('dm')>=0){if(coordinateminutes<10){coordinateminutes="0"+coordinateminutes;} str+=coordinateminutes+"'";if(dmsOption.indexOf('dms')>=0){if(coordinateseconds<10){coordinateseconds="0"+coordinateseconds;} str+=coordinateseconds+'"';}} if(axis=="lon"){str+=coordinate<0?OpenLayers.i18n("W"):OpenLayers.i18n("E");}else{str+=coordinate<0?OpenLayers.i18n("S"):OpenLayers.i18n("N");} return str;};OpenLayers.Lang={code:null,defaultCode:"en",getCode:function(){if(!OpenLayers.Lang.code){OpenLayers.Lang.setCode();} return OpenLayers.Lang.code;},setCode:function(code){var lang;if(!code){code=(OpenLayers.BROWSER_NAME=="msie")?navigator.userLanguage:navigator.language;} var parts=code.split('-');parts[0]=parts[0].toLowerCase();if(typeof OpenLayers.Lang[parts[0]]=="object"){lang=parts[0];} if(parts[1]){var testLang=parts[0]+'-'+parts[1].toUpperCase();if(typeof OpenLayers.Lang[testLang]=="object"){lang=testLang;}} if(!lang){OpenLayers.Console.warn('Failed to find OpenLayers.Lang.'+parts.join("-")+' dictionary, falling back to default language');lang=OpenLayers.Lang.defaultCode;} OpenLayers.Lang.code=lang;},translate:function(key,context){var dictionary=OpenLayers.Lang[OpenLayers.Lang.getCode()];var message=dictionary&&dictionary[key];if(!message){message=key;} if(context){message=OpenLayers.String.format(message,context);} return message;}};OpenLayers.i18n=OpenLayers.Lang.translate;OpenLayers.Format=OpenLayers.Class({options:null,externalProjection:null,internalProjection:null,data:null,keepData:false,initialize:function(options){OpenLayers.Util.extend(this,options);this.options=options;},destroy:function(){},read:function(data){throw new Error('Read not implemented.');},write:function(object){throw new Error('Write not implemented.');},CLASS_NAME:"OpenLayers.Format"});OpenLayers.Format.XML=OpenLayers.Class(OpenLayers.Format,{namespaces:null,namespaceAlias:null,defaultPrefix:null,readers:{},writers:{},xmldom:null,initialize:function(options){if(window.ActiveXObject){this.xmldom=new ActiveXObject("Microsoft.XMLDOM");} OpenLayers.Format.prototype.initialize.apply(this,[options]);this.namespaces=OpenLayers.Util.extend({},this.namespaces);this.namespaceAlias={};for(var alias in this.namespaces){this.namespaceAlias[this.namespaces[alias]]=alias;}},destroy:function(){this.xmldom=null;OpenLayers.Format.prototype.destroy.apply(this,arguments);},setNamespace:function(alias,uri){this.namespaces[alias]=uri;this.namespaceAlias[uri]=alias;},read:function(text){var index=text.indexOf('<');if(index>0){text=text.substring(index);} var node=OpenLayers.Util.Try(OpenLayers.Function.bind((function(){var xmldom;if(window.ActiveXObject&&!this.xmldom){xmldom=new ActiveXObject("Microsoft.XMLDOM");}else{xmldom=this.xmldom;} xmldom.loadXML(text);return xmldom;}),this),function(){return new DOMParser().parseFromString(text,'text/xml');},function(){var req=new XMLHttpRequest();req.open("GET","data:"+"text/xml"+";charset=utf-8,"+encodeURIComponent(text),false);if(req.overrideMimeType){req.overrideMimeType("text/xml");} req.send(null);return req.responseXML;});if(this.keepData){this.data=node;} return node;},write:function(node){var data;if(this.xmldom){data=node.xml;}else{var serializer=new XMLSerializer();if(node.nodeType==1){var doc=document.implementation.createDocument("","",null);if(doc.importNode){node=doc.importNode(node,true);} doc.appendChild(node);data=serializer.serializeToString(doc);}else{data=serializer.serializeToString(node);}} return data;},createElementNS:function(uri,name){var element;if(this.xmldom){if(typeof uri=="string"){element=this.xmldom.createNode(1,name,uri);}else{element=this.xmldom.createNode(1,name,"");}}else{element=document.createElementNS(uri,name);} return element;},createDocumentFragment:function(){var element;if(this.xmldom){element=this.xmldom.createDocumentFragment();}else{element=document.createDocumentFragment();} return element;},createTextNode:function(text){var node;if(typeof text!=="string"){text=String(text);} if(this.xmldom){node=this.xmldom.createTextNode(text);}else{node=document.createTextNode(text);} return node;},getElementsByTagNameNS:function(node,uri,name){var elements=[];if(node.getElementsByTagNameNS){elements=node.getElementsByTagNameNS(uri,name);}else{var allNodes=node.getElementsByTagName("*");var potentialNode,fullName;for(var i=0,len=allNodes.length;i0){prefix=name.substring(0,split);local=name.substring(split+1);}else{if(parent){prefix=this.namespaceAlias[parent.namespaceURI];}else{prefix=this.defaultPrefix;} local=name;} var child=this.writers[prefix][local].apply(this,[obj]);if(parent){parent.appendChild(child);} return child;},getChildEl:function(node,name,uri){return node&&this.getThisOrNextEl(node.firstChild,name,uri);},getNextEl:function(node,name,uri){return node&&this.getThisOrNextEl(node.nextSibling,name,uri);},getThisOrNextEl:function(node,name,uri){outer:for(var sibling=node;sibling;sibling=sibling.nextSibling){switch(sibling.nodeType){case 1:if((!name||name===(sibling.localName||sibling.nodeName.split(":").pop()))&&(!uri||uri===sibling.namespaceURI)){break outer;} sibling=null;break outer;case 3:if(/^\s*$/.test(sibling.nodeValue)){break;} case 4:case 6:case 12:case 10:case 11:sibling=null;break outer;}} return sibling||null;},lookupNamespaceURI:function(node,prefix){var uri=null;if(node){if(node.lookupNamespaceURI){uri=node.lookupNamespaceURI(prefix);}else{outer:switch(node.nodeType){case 1:if(node.namespaceURI!==null&&node.prefix===prefix){uri=node.namespaceURI;break outer;} var len=node.attributes.length;if(len){var attr;for(var i=0;i=1){matrixSet=contents.tileMatrixSets[layerDef.tileMatrixSetLinks[0].tileMatrixSet];} if(!matrixSet){throw new Error("matrixSet not found");} var style;for(var i=0,ii=layerDef.styles.length;i0?duration:Number.POSITIVE_INFINITY;var id=++counter;var start=+new Date;loops[id]=function(){if(loops[id]&&+new Date-start<=duration){callback();if(loops[id]){requestFrame(loops[id],element);}}else{delete loops[id];}};requestFrame(loops[id],element);return id;} function stop(id){delete loops[id];} return{isNative:isNative,requestFrame:requestFrame,start:start,stop:stop};})(window);OpenLayers.Tween=OpenLayers.Class({easing:null,begin:null,finish:null,duration:null,callbacks:null,time:null,minFrameRate:null,startTime:null,animationId:null,playing:false,initialize:function(easing){this.easing=(easing)?easing:OpenLayers.Easing.Expo.easeOut;},start:function(begin,finish,duration,options){this.playing=true;this.begin=begin;this.finish=finish;this.duration=duration;this.callbacks=options.callbacks;this.minFrameRate=options.minFrameRate||30;this.time=0;this.startTime=new Date().getTime();OpenLayers.Animation.stop(this.animationId);this.animationId=null;if(this.callbacks&&this.callbacks.start){this.callbacks.start.call(this,this.begin);} this.animationId=OpenLayers.Animation.start(OpenLayers.Function.bind(this.play,this));},stop:function(){if(!this.playing){return;} if(this.callbacks&&this.callbacks.done){this.callbacks.done.call(this,this.finish);} OpenLayers.Animation.stop(this.animationId);this.animationId=null;this.playing=false;},play:function(){var value={};for(var i in this.begin){var b=this.begin[i];var f=this.finish[i];if(b==null||f==null||isNaN(b)||isNaN(f)){throw new TypeError('invalid value for Tween');} var c=f-b;value[i]=this.easing.apply(this,[this.time,b,c,this.duration]);} this.time++;if(this.callbacks&&this.callbacks.eachStep){if((new Date().getTime()-this.startTime)/this.time<=1000/this.minFrameRate){this.callbacks.eachStep.call(this,value);}} if(this.time>this.duration){this.stop();}},CLASS_NAME:"OpenLayers.Tween"});OpenLayers.Easing={CLASS_NAME:"OpenLayers.Easing"};OpenLayers.Easing.Linear={easeIn:function(t,b,c,d){return c*t/d+b;},easeOut:function(t,b,c,d){return c*t/d+b;},easeInOut:function(t,b,c,d){return c*t/d+b;},CLASS_NAME:"OpenLayers.Easing.Linear"};OpenLayers.Easing.Expo={easeIn:function(t,b,c,d){return(t==0)?b:c*Math.pow(2,10*(t/d-1))+b;},easeOut:function(t,b,c,d){return(t==d)?b+c:c*(-Math.pow(2,-10*t/d)+1)+b;},easeInOut:function(t,b,c,d){if(t==0)return b;if(t==d)return b+c;if((t/=d/2)<1)return c/2*Math.pow(2,10*(t-1))+b;return c/2*(-Math.pow(2,-10*--t)+2)+b;},CLASS_NAME:"OpenLayers.Easing.Expo"};OpenLayers.Easing.Quad={easeIn:function(t,b,c,d){return c*(t/=d)*t+b;},easeOut:function(t,b,c,d){return-c*(t/=d)*(t-2)+b;},easeInOut:function(t,b,c,d){if((t/=d/2)<1)return c/2*t*t+b;return-c/2*((--t)*(t-2)-1)+b;},CLASS_NAME:"OpenLayers.Easing.Quad"};OpenLayers.Control=OpenLayers.Class({id:null,map:null,div:null,type:null,allowSelection:false,displayClass:"",title:"",autoActivate:false,active:null,handlerOptions:null,handler:null,eventListeners:null,events:null,initialize:function(options){this.displayClass=this.CLASS_NAME.replace("OpenLayers.","ol").replace(/\./g,"");OpenLayers.Util.extend(this,options);this.events=new OpenLayers.Events(this);if(this.eventListeners instanceof Object){this.events.on(this.eventListeners);} if(this.id==null){this.id=OpenLayers.Util.createUniqueID(this.CLASS_NAME+"_");}},destroy:function(){if(this.events){if(this.eventListeners){this.events.un(this.eventListeners);} this.events.destroy();this.events=null;} this.eventListeners=null;if(this.handler){this.handler.destroy();this.handler=null;} if(this.handlers){for(var key in this.handlers){if(this.handlers.hasOwnProperty(key)&&typeof this.handlers[key].destroy=="function"){this.handlers[key].destroy();}} this.handlers=null;} if(this.map){this.map.removeControl(this);this.map=null;} this.div=null;},setMap:function(map){this.map=map;if(this.handler){this.handler.setMap(map);}},draw:function(px){if(this.div==null){this.div=OpenLayers.Util.createDiv(this.id);this.div.className=this.displayClass;if(!this.allowSelection){this.div.className+=" olControlNoSelect";this.div.setAttribute("unselectable","on",0);this.div.onselectstart=OpenLayers.Function.False;} if(this.title!=""){this.div.title=this.title;}} if(px!=null){this.position=px.clone();} this.moveTo(this.position);return this.div;},moveTo:function(px){if((px!=null)&&(this.div!=null)){this.div.style.left=px.x+"px";this.div.style.top=px.y+"px";}},activate:function(){if(this.active){return false;} if(this.handler){this.handler.activate();} this.active=true;if(this.map){OpenLayers.Element.addClass(this.map.viewPortDiv,this.displayClass.replace(/ /g,"")+"Active");} this.events.triggerEvent("activate");return true;},deactivate:function(){if(this.active){if(this.handler){this.handler.deactivate();} this.active=false;if(this.map){OpenLayers.Element.removeClass(this.map.viewPortDiv,this.displayClass.replace(/ /g,"")+"Active");} this.events.triggerEvent("deactivate");return true;} return false;},CLASS_NAME:"OpenLayers.Control"});OpenLayers.Control.TYPE_BUTTON=1;OpenLayers.Control.TYPE_TOGGLE=2;OpenLayers.Control.TYPE_TOOL=3;OpenLayers.Event={observers:false,KEY_SPACE:32,KEY_BACKSPACE:8,KEY_TAB:9,KEY_RETURN:13,KEY_ESC:27,KEY_LEFT:37,KEY_UP:38,KEY_RIGHT:39,KEY_DOWN:40,KEY_DELETE:46,element:function(event){return event.target||event.srcElement;},isSingleTouch:function(event){return event.touches&&event.touches.length==1;},isMultiTouch:function(event){return event.touches&&event.touches.length>1;},isLeftClick:function(event){return(((event.which)&&(event.which==1))||((event.button)&&(event.button==1)));},isRightClick:function(event){return(((event.which)&&(event.which==3))||((event.button)&&(event.button==2)));},stop:function(event,allowDefault){if(!allowDefault){OpenLayers.Event.preventDefault(event);} if(event.stopPropagation){event.stopPropagation();}else{event.cancelBubble=true;}},preventDefault:function(event){if(event.preventDefault){event.preventDefault();}else{event.returnValue=false;}},findElement:function(event,tagName){var element=OpenLayers.Event.element(event);while(element.parentNode&&(!element.tagName||(element.tagName.toUpperCase()!=tagName.toUpperCase()))){element=element.parentNode;} return element;},observe:function(elementParam,name,observer,useCapture){var element=OpenLayers.Util.getElement(elementParam);useCapture=useCapture||false;if(name=='keypress'&&(navigator.appVersion.match(/Konqueror|Safari|KHTML/)||element.attachEvent)){name='keydown';} if(!this.observers){this.observers={};} if(!element._eventCacheID){var idPrefix="eventCacheID_";if(element.id){idPrefix=element.id+"_"+idPrefix;} element._eventCacheID=OpenLayers.Util.createUniqueID(idPrefix);} var cacheID=element._eventCacheID;if(!this.observers[cacheID]){this.observers[cacheID]=[];} this.observers[cacheID].push({'element':element,'name':name,'observer':observer,'useCapture':useCapture});if(element.addEventListener){element.addEventListener(name,observer,useCapture);}else if(element.attachEvent){element.attachEvent('on'+name,observer);}},stopObservingElement:function(elementParam){var element=OpenLayers.Util.getElement(elementParam);var cacheID=element._eventCacheID;this._removeElementObservers(OpenLayers.Event.observers[cacheID]);},_removeElementObservers:function(elementObservers){if(elementObservers){for(var i=elementObservers.length-1;i>=0;i--){var entry=elementObservers[i];OpenLayers.Event.stopObserving.apply(this,[entry.element,entry.name,entry.observer,entry.useCapture]);}}},stopObserving:function(elementParam,name,observer,useCapture){useCapture=useCapture||false;var element=OpenLayers.Util.getElement(elementParam);var cacheID=element._eventCacheID;if(name=='keypress'){if(navigator.appVersion.match(/Konqueror|Safari|KHTML/)||element.detachEvent){name='keydown';}} var foundEntry=false;var elementObservers=OpenLayers.Event.observers[cacheID];if(elementObservers){var i=0;while(!foundEntry&&iMath.floor(evt.pageY)||evt.pageX===0&&Math.floor(x)>Math.floor(evt.pageX)){x=x-winPageX;y=y-winPageY;}else if(y<(evt.pageY-winPageY)||x<(evt.pageX-winPageX)){x=evt.pageX-winPageX;y=evt.pageY-winPageY;} evt.olClientX=x;evt.olClientY=y;return{clientX:x,clientY:y};},clearMouseCache:function(){this.element.scrolls=null;this.element.lefttop=null;this.element.offsets=null;},getMousePosition:function(evt){if(!this.includeXY){this.clearMouseCache();}else if(!this.element.hasScrollEvent){OpenLayers.Event.observe(window,"scroll",this.clearMouseListener);this.element.hasScrollEvent=true;} if(!this.element.scrolls){var viewportElement=OpenLayers.Util.getViewportElement();this.element.scrolls=[window.pageXOffset||viewportElement.scrollLeft,window.pageYOffset||viewportElement.scrollTop];} if(!this.element.lefttop){this.element.lefttop=[(document.documentElement.clientLeft||0),(document.documentElement.clientTop||0)];} if(!this.element.offsets){this.element.offsets=OpenLayers.Util.pagePosition(this.element);} return new OpenLayers.Pixel((evt.clientX+this.element.scrolls[0])-this.element.offsets[0] -this.element.lefttop[0],(evt.clientY+this.element.scrolls[1])-this.element.offsets[1] -this.element.lefttop[1]);},addMsTouchListener:function(element,type,handler){var eventHandler=this.eventHandler;var touches=this._msTouches;function msHandler(evt){handler(OpenLayers.Util.applyDefaults({stopPropagation:function(){for(var i=touches.length-1;i>=0;--i){touches[i].stopPropagation();}},preventDefault:function(){for(var i=touches.length-1;i>=0;--i){touches[i].preventDefault();}},type:type},evt));} switch(type){case'touchstart':return this.addMsTouchListenerStart(element,type,msHandler);case'touchend':return this.addMsTouchListenerEnd(element,type,msHandler);case'touchmove':return this.addMsTouchListenerMove(element,type,msHandler);default:throw'Unknown touch event type';}},addMsTouchListenerStart:function(element,type,handler){var touches=this._msTouches;var cb=function(e){var alreadyInArray=false;for(var i=0,ii=touches.length;i=this.down.xy.distanceTo(evt.xy);if(passes&&this.touch&&this.down.touches.length===this.last.touches.length){for(var i=0,ii=this.down.touches.length;ithis.pixelTolerance){passes=false;break;}}}} return passes;},getTouchDistance:function(from,to){return Math.sqrt(Math.pow(from.clientX-to.clientX,2)+ Math.pow(from.clientY-to.clientY,2));},passesDblclickTolerance:function(evt){var passes=true;if(this.down&&this.first){passes=this.down.xy.distanceTo(this.first.xy)<=this.dblclickTolerance;} return passes;},clearTimer:function(){if(this.timerId!=null){window.clearTimeout(this.timerId);this.timerId=null;} if(this.rightclickTimerId!=null){window.clearTimeout(this.rightclickTimerId);this.rightclickTimerId=null;}},delayedCall:function(evt){this.timerId=null;if(evt){this.callback("click",[evt]);}},getEventInfo:function(evt){var touches;if(evt.touches){var len=evt.touches.length;touches=new Array(len);var touch;for(var i=0;i0){var infoLookup={};var layer,idx;for(var i=0,len=layers.length;i=0;--i){layer=candidates[i];if(OpenLayers.Layer.UTFGrid&&layer instanceof OpenLayers.Layer.UTFGrid||OpenLayers.Layer.WMTSUTFGrid&&layer instanceof OpenLayers.Layer.WMTSUTFGrid){layers.push(layer);}} return layers;},CLASS_NAME:"OpenLayers.Control.UTFGrid"});OpenLayers.Projection=OpenLayers.Class({proj:null,projCode:null,titleRegEx:/\+title=[^\+]*/,initialize:function(projCode,options){OpenLayers.Util.extend(this,options);this.projCode=projCode;if(typeof Proj4js=="object"){this.proj=new Proj4js.Proj(projCode);}},getCode:function(){return this.proj?this.proj.srsCode:this.projCode;},getUnits:function(){return this.proj?this.proj.units:null;},toString:function(){return this.getCode();},equals:function(projection){var p=projection,equals=false;if(p){if(!(p instanceof OpenLayers.Projection)){p=new OpenLayers.Projection(p);} if((typeof Proj4js=="object")&&this.proj.defData&&p.proj.defData){equals=this.proj.defData.replace(this.titleRegEx,"")==p.proj.defData.replace(this.titleRegEx,"");}else if(p.getCode){var source=this.getCode(),target=p.getCode();equals=source==target||!!OpenLayers.Projection.transforms[source]&&OpenLayers.Projection.transforms[source][target]===OpenLayers.Projection.nullTransform;}} return equals;},destroy:function(){delete this.proj;delete this.projCode;},CLASS_NAME:"OpenLayers.Projection"});OpenLayers.Projection.transforms={};OpenLayers.Projection.defaults={"EPSG:4326":{units:"degrees",maxExtent:[-180,-90,180,90],yx:true},"CRS:84":{units:"degrees",maxExtent:[-180,-90,180,90]},"EPSG:900913":{units:"m",maxExtent:[-20037508.34,-20037508.34,20037508.34,20037508.34]}};OpenLayers.Projection.addTransform=function(from,to,method){if(method===OpenLayers.Projection.nullTransform){var defaults=OpenLayers.Projection.defaults[from];if(defaults&&!OpenLayers.Projection.defaults[to]){OpenLayers.Projection.defaults[to]=defaults;}} if(!OpenLayers.Projection.transforms[from]){OpenLayers.Projection.transforms[from]={};} OpenLayers.Projection.transforms[from][to]=method;};OpenLayers.Projection.transform=function(point,source,dest){if(source&&dest){if(!(source instanceof OpenLayers.Projection)){source=new OpenLayers.Projection(source);} if(!(dest instanceof OpenLayers.Projection)){dest=new OpenLayers.Projection(dest);} if(source.proj&&dest.proj){point=Proj4js.transform(source.proj,dest.proj,point);}else{var sourceCode=source.getCode();var destCode=dest.getCode();var transforms=OpenLayers.Projection.transforms;if(transforms[sourceCode]&&transforms[sourceCode][destCode]){transforms[sourceCode][destCode](point);}}} return point;};OpenLayers.Projection.nullTransform=function(point){return point;};(function(){var pole=20037508.34;function inverseMercator(xy){xy.x=180*xy.x/pole;xy.y=180/Math.PI*(2*Math.atan(Math.exp((xy.y/pole)*Math.PI))-Math.PI/2);return xy;} function forwardMercator(xy){xy.x=xy.x*pole/180;var y=Math.log(Math.tan((90+xy.y)*Math.PI/360))/Math.PI*pole;xy.y=Math.max(-20037508.34,Math.min(y,20037508.34));return xy;} function map(base,codes){var add=OpenLayers.Projection.addTransform;var same=OpenLayers.Projection.nullTransform;var i,len,code,other,j;for(i=0,len=codes.length;i=0;--i){map(mercator[i],geographic);} for(i=geographic.length-1;i>=0;--i){map(geographic[i],mercator);}})();OpenLayers.Map=OpenLayers.Class({Z_INDEX_BASE:{BaseLayer:100,Overlay:325,Feature:725,Popup:750,Control:1000},id:null,fractionalZoom:false,events:null,allOverlays:false,div:null,dragging:false,size:null,viewPortDiv:null,layerContainerOrigin:null,layerContainerDiv:null,layers:null,controls:null,popups:null,baseLayer:null,center:null,resolution:null,zoom:0,panRatio:1.5,options:null,tileSize:null,projection:"EPSG:4326",units:null,resolutions:null,maxResolution:null,minResolution:null,maxScale:null,minScale:null,maxExtent:null,minExtent:null,restrictedExtent:null,numZoomLevels:16,theme:null,displayProjection:null,tileManager:null,fallThrough:false,autoUpdateSize:true,eventListeners:null,panTween:null,panMethod:OpenLayers.Easing.Expo.easeOut,panDuration:50,zoomTween:null,zoomMethod:OpenLayers.Easing.Quad.easeOut,zoomDuration:20,paddingForPopups:null,layerContainerOriginPx:null,minPx:null,maxPx:null,initialize:function(div,options){if(arguments.length===1&&typeof div==="object"){options=div;div=options&&options.div;} this.tileSize=new OpenLayers.Size(OpenLayers.Map.TILE_WIDTH,OpenLayers.Map.TILE_HEIGHT);this.paddingForPopups=new OpenLayers.Bounds(15,15,15,15);this.theme=OpenLayers._getScriptLocation()+'theme/default/style.css';this.options=OpenLayers.Util.extend({},options);OpenLayers.Util.extend(this,options);var projCode=this.projection instanceof OpenLayers.Projection?this.projection.projCode:this.projection;OpenLayers.Util.applyDefaults(this,OpenLayers.Projection.defaults[projCode]);if(this.maxExtent&&!(this.maxExtent instanceof OpenLayers.Bounds)){this.maxExtent=new OpenLayers.Bounds(this.maxExtent);} if(this.minExtent&&!(this.minExtent instanceof OpenLayers.Bounds)){this.minExtent=new OpenLayers.Bounds(this.minExtent);} if(this.restrictedExtent&&!(this.restrictedExtent instanceof OpenLayers.Bounds)){this.restrictedExtent=new OpenLayers.Bounds(this.restrictedExtent);} if(this.center&&!(this.center instanceof OpenLayers.LonLat)){this.center=new OpenLayers.LonLat(this.center);} this.layers=[];this.id=OpenLayers.Util.createUniqueID("OpenLayers.Map_");this.div=OpenLayers.Util.getElement(div);if(!this.div){this.div=document.createElement("div");this.div.style.height="1px";this.div.style.width="1px";} OpenLayers.Element.addClass(this.div,'olMap');var id=this.id+"_OpenLayers_ViewPort";this.viewPortDiv=OpenLayers.Util.createDiv(id,null,null,null,"relative",null,"hidden");this.viewPortDiv.style.width="100%";this.viewPortDiv.style.height="100%";this.viewPortDiv.className="olMapViewport";this.div.appendChild(this.viewPortDiv);this.events=new OpenLayers.Events(this,this.viewPortDiv,null,this.fallThrough,{includeXY:true});if(this.tileManager){this.tileManager.addMap(this);} id=this.id+"_OpenLayers_Container";this.layerContainerDiv=OpenLayers.Util.createDiv(id);this.layerContainerDiv.style.zIndex=this.Z_INDEX_BASE['Popup']-1;this.layerContainerOriginPx={x:0,y:0};this.applyTransform();this.viewPortDiv.appendChild(this.layerContainerDiv);this.updateSize();if(this.eventListeners instanceof Object){this.events.on(this.eventListeners);} if(this.autoUpdateSize===true){this.updateSizeDestroy=OpenLayers.Function.bind(this.updateSize,this);OpenLayers.Event.observe(window,'resize',this.updateSizeDestroy);} if(this.theme){var addNode=true;var nodes=document.getElementsByTagName('link');for(var i=0,len=nodes.length;i=0;--i){this.controls[i].destroy();} this.controls=null;} if(this.layers!=null){for(var i=this.layers.length-1;i>=0;--i){this.layers[i].destroy(false);} this.layers=null;} if(this.viewPortDiv&&this.viewPortDiv.parentNode){this.viewPortDiv.parentNode.removeChild(this.viewPortDiv);} this.viewPortDiv=null;if(this.tileManager){this.tileManager.removeMap(this);this.tileManager=null;} if(this.eventListeners){this.events.un(this.eventListeners);this.eventListeners=null;} this.events.destroy();this.events=null;this.options=null;},setOptions:function(options){var updatePxExtent=this.minPx&&options.restrictedExtent!=this.restrictedExtent;OpenLayers.Util.extend(this,options);updatePxExtent&&this.moveTo(this.getCachedCenter(),this.zoom,{forceZoomChange:true});},getTileSize:function(){return this.tileSize;},getBy:function(array,property,match){var test=(typeof match.test=="function");var found=OpenLayers.Array.filter(this[array],function(item){return item[property]==match||(test&&match.test(item[property]));});return found;},getLayersBy:function(property,match){return this.getBy("layers",property,match);},getLayersByName:function(match){return this.getLayersBy("name",match);},getLayersByClass:function(match){return this.getLayersBy("CLASS_NAME",match);},getControlsBy:function(property,match){return this.getBy("controls",property,match);},getControlsByClass:function(match){return this.getControlsBy("CLASS_NAME",match);},getLayer:function(id){var foundLayer=null;for(var i=0,len=this.layers.length;ithis.layers.length){idx=this.layers.length;} if(base!=idx){this.layers.splice(base,1);this.layers.splice(idx,0,layer);for(var i=0,len=this.layers.length;i=0;--i){this.removePopup(this.popups[i]);}} popup.map=this;this.popups.push(popup);var popupDiv=popup.draw();if(popupDiv){popupDiv.style.zIndex=this.Z_INDEX_BASE['Popup']+ this.popups.length;this.layerContainerDiv.appendChild(popupDiv);}},removePopup:function(popup){OpenLayers.Util.removeItem(this.popups,popup);if(popup.div){try{this.layerContainerDiv.removeChild(popup.div);} catch(e){}} popup.map=null;},getSize:function(){var size=null;if(this.size!=null){size=this.size.clone();} return size;},updateSize:function(){var newSize=this.getCurrentSize();if(newSize&&!isNaN(newSize.h)&&!isNaN(newSize.w)){this.events.clearMouseCache();var oldSize=this.getSize();if(oldSize==null){this.size=oldSize=newSize;} if(!newSize.equals(oldSize)){this.size=newSize;for(var i=0,len=this.layers.length;i=this.minPx.x+xRestriction?Math.round(dx):0;dy=y<=this.maxPx.y-yRestriction&&y>=this.minPx.y+yRestriction?Math.round(dy):0;if(dx||dy){if(!this.dragging){this.dragging=true;this.events.triggerEvent("movestart");} this.center=null;if(dx){this.layerContainerOriginPx.x-=dx;this.minPx.x-=dx;this.maxPx.x-=dx;} if(dy){this.layerContainerOriginPx.y-=dy;this.minPx.y-=dy;this.maxPx.y-=dy;} this.applyTransform();var layer,i,len;for(i=0,len=this.layers.length;imaxResolution){if(this.fractionalZoom){zoom=this.getZoomForResolution(maxResolution);}else{for(var i=zoom|0,ii=resolutions.length;ithis.restrictedExtent.getWidth()){lonlat=new OpenLayers.LonLat(maxCenter.lon,lonlat.lat);}else if(extent.leftthis.restrictedExtent.right){lonlat=lonlat.add(this.restrictedExtent.right- extent.right,0);} if(extent.getHeight()>this.restrictedExtent.getHeight()){lonlat=new OpenLayers.LonLat(lonlat.lon,maxCenter.lat);}else if(extent.bottomthis.restrictedExtent.top){lonlat=lonlat.add(0,this.restrictedExtent.top- extent.top);}}} var zoomChanged=forceZoomChange||((this.isValidZoomLevel(zoom))&&(zoom!=this.getZoom()));var centerChanged=(this.isValidLonLat(lonlat))&&(!lonlat.equals(this.center));if(zoomChanged||centerChanged||dragging){dragging||this.events.triggerEvent("movestart",{zoomChanged:zoomChanged});if(centerChanged){if(!zoomChanged&&this.center){this.centerLayerContainer(lonlat);} this.center=lonlat.clone();} var res=zoomChanged?this.getResolutionForZoom(zoom):this.getResolution();if(zoomChanged||this.layerContainerOrigin==null){this.layerContainerOrigin=this.getCachedCenter();this.layerContainerOriginPx.x=0;this.layerContainerOriginPx.y=0;this.applyTransform();var maxExtent=this.getMaxExtent({restricted:true});var maxExtentCenter=maxExtent.getCenterLonLat();var lonDelta=this.center.lon-maxExtentCenter.lon;var latDelta=maxExtentCenter.lat-this.center.lat;var extentWidth=Math.round(maxExtent.getWidth()/res);var extentHeight=Math.round(maxExtent.getHeight()/res);this.minPx={x:(this.size.w-extentWidth)/2-lonDelta/res,y:(this.size.h-extentHeight)/2-latDelta/res};this.maxPx={x:this.minPx.x+Math.round(maxExtent.getWidth()/res),y:this.minPx.y+Math.round(maxExtent.getHeight()/res)};} if(zoomChanged){this.zoom=zoom;this.resolution=res;} var bounds=this.getExtent();if(this.baseLayer.visibility){this.baseLayer.moveTo(bounds,zoomChanged,options.dragging);options.dragging||this.baseLayer.events.triggerEvent("moveend",{zoomChanged:zoomChanged});} bounds=this.baseLayer.getExtent();for(var i=this.layers.length-1;i>=0;--i){var layer=this.layers[i];if(layer!==this.baseLayer&&!layer.isBaseLayer){var inRange=layer.calculateInRange();if(layer.inRange!=inRange){layer.inRange=inRange;if(!inRange){layer.display(false);}} if(inRange&&layer.visibility){layer.moveTo(bounds,zoomChanged,options.dragging);options.dragging||layer.events.triggerEvent("moveend",{zoomChanged:zoomChanged});}}} this.events.triggerEvent("move");dragging||this.events.triggerEvent("moveend");if(zoomChanged){for(var i=0,len=this.popups.length;i=0)&&(zoomLevel0){resolution=this.layers[0].getResolution();} return resolution;},getUnits:function(){var units=null;if(this.baseLayer!=null){units=this.baseLayer.units;} return units;},getScale:function(){var scale=null;if(this.baseLayer!=null){var res=this.getResolution();var units=this.baseLayer.units;scale=OpenLayers.Util.getScaleFromResolution(res,units);} return scale;},getZoomForExtent:function(bounds,closest){var zoom=null;if(this.baseLayer!=null){zoom=this.baseLayer.getZoomForExtent(bounds,closest);} return zoom;},getResolutionForZoom:function(zoom){var resolution=null;if(this.baseLayer){resolution=this.baseLayer.getResolutionForZoom(zoom);} return resolution;},getZoomForResolution:function(resolution,closest){var zoom=null;if(this.baseLayer!=null){zoom=this.baseLayer.getZoomForResolution(resolution,closest);} return zoom;},zoomTo:function(zoom,xy){var map=this;if(map.isValidZoomLevel(zoom)){if(map.baseLayer.wrapDateLine){zoom=map.adjustZoom(zoom);} if(map.zoomTween){var currentRes=map.getResolution(),targetRes=map.getResolutionForZoom(zoom),start={scale:1},end={scale:currentRes/targetRes};if(map.zoomTween.playing&&map.zoomTween.duration<3*map.zoomDuration){map.zoomTween.finish={scale:map.zoomTween.finish.scale*end.scale};}else{if(!xy){var size=map.getSize();xy={x:size.w/2,y:size.h/2};} map.zoomTween.start(start,end,map.zoomDuration,{minFrameRate:50,callbacks:{eachStep:function(data){var containerOrigin=map.layerContainerOriginPx,scale=data.scale,dx=((scale-1)*(containerOrigin.x-xy.x))|0,dy=((scale-1)*(containerOrigin.y-xy.y))|0;map.applyTransform(containerOrigin.x+dx,containerOrigin.y+dy,scale);},done:function(data){map.applyTransform();var resolution=map.getResolution()/data.scale,zoom=map.getZoomForResolution(resolution,true) map.moveTo(map.getZoomTargetCenter(xy,resolution),zoom,true);}}});}}else{var center=xy?map.getZoomTargetCenter(xy,map.getResolutionForZoom(zoom)):null;map.setCenter(center,zoom);}}},zoomIn:function(){this.zoomTo(this.getZoom()+1);},zoomOut:function(){this.zoomTo(this.getZoom()-1);},zoomToExtent:function(bounds,closest){if(!(bounds instanceof OpenLayers.Bounds)){bounds=new OpenLayers.Bounds(bounds);} var center=bounds.getCenterLonLat();if(this.baseLayer.wrapDateLine){var maxExtent=this.getMaxExtent();bounds=bounds.clone();while(bounds.right=0){this.initResolutions();if(reinitialize&&this.map.baseLayer===this){this.map.setCenter(this.map.getCenter(),this.map.getZoomForResolution(resolution),false,true);this.map.events.triggerEvent("changebaselayer",{layer:this});} break;}}}},onMapResize:function(){},redraw:function(){var redrawn=false;if(this.map){this.inRange=this.calculateInRange();var extent=this.getExtent();if(extent&&this.inRange&&this.visibility){var zoomChanged=true;this.moveTo(extent,zoomChanged,false);this.events.triggerEvent("moveend",{"zoomChanged":zoomChanged});redrawn=true;}} return redrawn;},moveTo:function(bounds,zoomChanged,dragging){var display=this.visibility;if(!this.isBaseLayer){display=display&&this.inRange;} this.display(display);},moveByPx:function(dx,dy){},setMap:function(map){if(this.map==null){this.map=map;this.maxExtent=this.maxExtent||this.map.maxExtent;this.minExtent=this.minExtent||this.map.minExtent;this.projection=this.projection||this.map.projection;if(typeof this.projection=="string"){this.projection=new OpenLayers.Projection(this.projection);} this.units=this.projection.getUnits()||this.units||this.map.units;this.initResolutions();if(!this.isBaseLayer){this.inRange=this.calculateInRange();var show=((this.visibility)&&(this.inRange));this.div.style.display=show?"":"none";} this.setTileSize();}},afterAdd:function(){},removeMap:function(map){},getImageSize:function(bounds){return(this.imageSize||this.tileSize);},setTileSize:function(size){var tileSize=(size)?size:((this.tileSize)?this.tileSize:this.map.getTileSize());this.tileSize=tileSize;if(this.gutter){this.imageSize=new OpenLayers.Size(tileSize.w+(2*this.gutter),tileSize.h+(2*this.gutter));}},getVisibility:function(){return this.visibility;},setVisibility:function(visibility){if(visibility!=this.visibility){this.visibility=visibility;this.display(visibility);this.redraw();this.events.triggerEvent("visibilitychanged");}},display:function(display){if(display!=(this.div.style.display!="none")){this.div.style.display=(display&&this.calculateInRange())?"block":"none";if(this.map){this.map.events.triggerEvent("changelayer",{layer:this,property:"visibility"});}}},calculateInRange:function(){var inRange=false;if(this.alwaysInRange){inRange=true;}else{if(this.map){var resolution=this.map.getResolution();inRange=((resolution>=this.minResolution)&&(resolution<=this.maxResolution));}} return inRange;},setIsBaseLayer:function(isBaseLayer){if(isBaseLayer!=this.isBaseLayer){this.isBaseLayer=isBaseLayer;if(this.map!=null){this.map.events.triggerEvent("changebaselayer",{layer:this});}}},initResolutions:function(){var i,len,p;var props={},alwaysInRange=true;for(i=0,len=this.RESOLUTION_PROPERTIES.length;i=resolution){highRes=res;lowZoom=i;} if(res<=resolution){lowRes=res;highZoom=i;break;}} var dRes=highRes-lowRes;if(dRes>0){zoom=lowZoom+((highRes-resolution)/dRes);}else{zoom=lowZoom;}}else{var diff;var minDiff=Number.POSITIVE_INFINITY;for(i=0,len=this.resolutions.length;iminDiff){break;} minDiff=diff;}else{if(this.resolutions[i]=0&&row=0;i--){newResolution=this.serverResolutions[i];newDistance=Math.abs(newResolution-resolution);if(newDistance>distance){break;} distance=newDistance;serverResolution=newResolution;} resolution=serverResolution;} return resolution;},getServerZoom:function(){var resolution=this.getServerResolution();return this.serverResolutions?OpenLayers.Util.indexOf(this.serverResolutions,resolution):this.map.getZoomForResolution(resolution)+(this.zoomOffset||0);},applyBackBuffer:function(resolution){if(this.backBufferTimerId!==null){this.removeBackBuffer();} var backBuffer=this.backBuffer;if(!backBuffer){backBuffer=this.createBackBuffer();if(!backBuffer){return;} if(resolution===this.gridResolution){this.div.insertBefore(backBuffer,this.div.firstChild);}else{this.map.baseLayer.div.parentNode.insertBefore(backBuffer,this.map.baseLayer.div);} this.backBuffer=backBuffer;var topLeftTileBounds=this.grid[0][0].bounds;this.backBufferLonLat={lon:topLeftTileBounds.left,lat:topLeftTileBounds.top};this.backBufferResolution=this.gridResolution;} var ratio=this.backBufferResolution/resolution;var tiles=backBuffer.childNodes,tile;for(var i=tiles.length-1;i>=0;--i){tile=tiles[i];tile.style.top=((ratio*tile._i*tile._h)|0)+'px';tile.style.left=((ratio*tile._j*tile._w)|0)+'px';tile.style.width=Math.round(ratio*tile._w)+'px';tile.style.height=Math.round(ratio*tile._h)+'px';} var position=this.getViewPortPxFromLonLat(this.backBufferLonLat,resolution);var leftOffset=this.map.layerContainerOriginPx.x;var topOffset=this.map.layerContainerOriginPx.y;backBuffer.style.left=Math.round(position.x-leftOffset)+'px';backBuffer.style.top=Math.round(position.y-topOffset)+'px';},createBackBuffer:function(){var backBuffer;if(this.grid.length>0){backBuffer=document.createElement('div');backBuffer.id=this.div.id+'_bb';backBuffer.className='olBackBuffer';backBuffer.style.position='absolute';var map=this.map;backBuffer.style.zIndex=this.transitionEffect==='resize'?this.getZIndex()-1:map.Z_INDEX_BASE.BaseLayer- (map.getNumLayers()-map.getLayerIndex(this));for(var i=0,lenI=this.grid.length;i=0;--i){OpenLayers.Event.stopObserving(this._transitionElement,this.transitionendEvents[i],this._removeBackBuffer);} delete this._transitionElement;} if(this.backBuffer){if(this.backBuffer.parentNode){this.backBuffer.parentNode.removeChild(this.backBuffer);} this.backBuffer=null;this.backBufferResolution=null;if(this.backBufferTimerId!==null){window.clearTimeout(this.backBufferTimerId);this.backBufferTimerId=null;}}},moveByPx:function(dx,dy){if(!this.singleTile){this.moveGriddedTiles();}},setTileSize:function(size){if(this.singleTile){size=this.map.getSize();size.h=parseInt(size.h*this.ratio,10);size.w=parseInt(size.w*this.ratio,10);} OpenLayers.Layer.HTTPRequest.prototype.setTileSize.apply(this,[size]);},getTilesBounds:function(){var bounds=null;var length=this.grid.length;if(length){var bottomLeftTileBounds=this.grid[length-1][0].bounds,width=this.grid[0].length*bottomLeftTileBounds.getWidth(),height=this.grid.length*bottomLeftTileBounds.getHeight();bounds=new OpenLayers.Bounds(bottomLeftTileBounds.left,bottomLeftTileBounds.bottom,bottomLeftTileBounds.left+width,bottomLeftTileBounds.bottom+height);} return bounds;},initSingleTile:function(bounds){this.events.triggerEvent("retile");var center=bounds.getCenterLonLat();var tileWidth=bounds.getWidth()*this.ratio;var tileHeight=bounds.getHeight()*this.ratio;var tileBounds=new OpenLayers.Bounds(center.lon-(tileWidth/2),center.lat-(tileHeight/2),center.lon+(tileWidth/2),center.lat+(tileHeight/2));var px=this.map.getLayerPxFromLonLat({lon:tileBounds.left,lat:tileBounds.top});if(!this.grid.length){this.grid[0]=[];} var tile=this.grid[0][0];if(!tile){tile=this.addTile(tileBounds,px);this.addTileMonitoringHooks(tile);tile.draw();this.grid[0][0]=tile;}else{tile.moveTo(tileBounds,px);} this.removeExcessTiles(1,1);this.gridResolution=this.getServerResolution();},calculateGridLayout:function(bounds,origin,resolution){var tilelon=resolution*this.tileSize.w;var tilelat=resolution*this.tileSize.h;var offsetlon=bounds.left-origin.lon;var tilecol=Math.floor(offsetlon/tilelon)-this.buffer;var rowSign=this.rowSign;var offsetlat=rowSign*(origin.lat-bounds.top+tilelat);var tilerow=Math[~rowSign?'floor':'ceil'](offsetlat/tilelat)-this.buffer*rowSign;return{tilelon:tilelon,tilelat:tilelat,startcol:tilecol,startrow:tilerow};},getTileOrigin:function(){var origin=this.tileOrigin;if(!origin){var extent=this.getMaxExtent();var edges=({"tl":["left","top"],"tr":["right","top"],"bl":["left","bottom"],"br":["right","bottom"]})[this.tileOriginCorner];origin=new OpenLayers.LonLat(extent[edges[0]],extent[edges[1]]);} return origin;},getTileBoundsForGridIndex:function(row,col){var origin=this.getTileOrigin();var tileLayout=this.gridLayout;var tilelon=tileLayout.tilelon;var tilelat=tileLayout.tilelat;var startcol=tileLayout.startcol;var startrow=tileLayout.startrow;var rowSign=this.rowSign;return new OpenLayers.Bounds(origin.lon+(startcol+col)*tilelon,origin.lat-(startrow+row*rowSign)*tilelat*rowSign,origin.lon+(startcol+col+1)*tilelon,origin.lat-(startrow+(row-1)*rowSign)*tilelat*rowSign);},initGriddedTiles:function(bounds){this.events.triggerEvent("retile");var viewSize=this.map.getSize();var origin=this.getTileOrigin();var resolution=this.map.getResolution(),serverResolution=this.getServerResolution(),ratio=resolution/serverResolution,tileSize={w:this.tileSize.w/ratio,h:this.tileSize.h/ratio};var minRows=Math.ceil(viewSize.h/tileSize.h)+ 2*this.buffer+1;var minCols=Math.ceil(viewSize.w/tileSize.w)+ 2*this.buffer+1;var tileLayout=this.calculateGridLayout(bounds,origin,serverResolution);this.gridLayout=tileLayout;var tilelon=tileLayout.tilelon;var tilelat=tileLayout.tilelat;var layerContainerDivLeft=this.map.layerContainerOriginPx.x;var layerContainerDivTop=this.map.layerContainerOriginPx.y;var tileBounds=this.getTileBoundsForGridIndex(0,0);var startPx=this.map.getViewPortPxFromLonLat(new OpenLayers.LonLat(tileBounds.left,tileBounds.top));startPx.x=Math.round(startPx.x)-layerContainerDivLeft;startPx.y=Math.round(startPx.y)-layerContainerDivTop;var tileData=[],center=this.map.getCenter();var rowidx=0;do{var row=this.grid[rowidx];if(!row){row=[];this.grid.push(row);} var colidx=0;do{tileBounds=this.getTileBoundsForGridIndex(rowidx,colidx);var px=startPx.clone();px.x=px.x+colidx*Math.round(tileSize.w);px.y=px.y+rowidx*Math.round(tileSize.h);var tile=row[colidx];if(!tile){tile=this.addTile(tileBounds,px);this.addTileMonitoringHooks(tile);row.push(tile);}else{tile.moveTo(tileBounds,px,false);} var tileCenter=tileBounds.getCenterLonLat();tileData.push({tile:tile,distance:Math.pow(tileCenter.lon-center.lon,2)+ Math.pow(tileCenter.lat-center.lat,2)});colidx+=1;}while((tileBounds.right<=bounds.right+tilelon*this.buffer)||colidx=bounds.bottom-tilelat*this.buffer)||rowidx=0;--i){OpenLayers.Event.observe(this._transitionElement,transitionendEvents[i],this._removeBackBuffer);} this.backBufferTimerId=window.setTimeout(this._removeBackBuffer,this.removeBackBufferDelay);}} this.loading=false;this.events.triggerEvent("loadend");}};tile.onLoadError=function(){this.events.triggerEvent("tileerror",{tile:tile});};tile.events.on({"loadstart":tile.onLoadStart,"loadend":tile.onLoadEnd,"unload":tile.onLoadEnd,"loaderror":tile.onLoadError,scope:this});},removeTileMonitoringHooks:function(tile){tile.unload();tile.events.un({"loadstart":tile.onLoadStart,"loadend":tile.onLoadEnd,"unload":tile.onLoadEnd,"loaderror":tile.onLoadError,scope:this});},moveGriddedTiles:function(){var buffer=this.buffer+1;while(true){var tlTile=this.grid[0][0];var tlViewPort={x:tlTile.position.x+ this.map.layerContainerOriginPx.x,y:tlTile.position.y+ this.map.layerContainerOriginPx.y};var ratio=this.getServerResolution()/this.map.getResolution();var tileSize={w:Math.round(this.tileSize.w*ratio),h:Math.round(this.tileSize.h*ratio)};if(tlViewPort.x>-tileSize.w*(buffer-1)){this.shiftColumn(true,tileSize);}else if(tlViewPort.x<-tileSize.w*buffer){this.shiftColumn(false,tileSize);}else if(tlViewPort.y>-tileSize.h*(buffer-1)){this.shiftRow(true,tileSize);}else if(tlViewPort.y<-tileSize.h*buffer){this.shiftRow(false,tileSize);}else{break;}}},shiftRow:function(prepend,tileSize){var grid=this.grid;var rowIndex=prepend?0:(grid.length-1);var sign=prepend?-1:1;var rowSign=this.rowSign;var tileLayout=this.gridLayout;tileLayout.startrow+=sign*rowSign;var modelRow=grid[rowIndex];var row=grid[prepend?'pop':'shift']();for(var i=0,len=row.length;irows){var row=this.grid.pop();for(i=0,l=row.length;icolumns){var row=this.grid[i];var tile=row.pop();this.destroyTile(tile);}}},onMapResize:function(){if(this.singleTile){this.clearGrid();this.setTileSize();}},getTileBounds:function(viewPortPx){var maxExtent=this.maxExtent;var resolution=this.getResolution();var tileMapWidth=resolution*this.tileSize.w;var tileMapHeight=resolution*this.tileSize.h;var mapPoint=this.getLonLatFromViewPortPx(viewPortPx);var tileLeft=maxExtent.left+(tileMapWidth*Math.floor((mapPoint.lon- maxExtent.left)/tileMapWidth));var tileBottom=maxExtent.bottom+(tileMapHeight*Math.floor((mapPoint.lat- maxExtent.bottom)/tileMapHeight));return new OpenLayers.Bounds(tileLeft,tileBottom,tileLeft+tileMapWidth,tileBottom+tileMapHeight);},CLASS_NAME:"OpenLayers.Layer.Grid"});OpenLayers.Format.JSON=OpenLayers.Class(OpenLayers.Format,{indent:" ",space:" ",newline:"\n",level:0,pretty:false,nativeJSON:(function(){return!!(window.JSON&&typeof JSON.parse=="function"&&typeof JSON.stringify=="function");})(),read:function(json,filter){var object;if(this.nativeJSON){object=JSON.parse(json,filter);}else try{if(/^[\],:{}\s]*$/.test(json.replace(/\\["\\\/bfnrtu]/g,'@').replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,']').replace(/(?:^|:|,)(?:\s*\[)+/g,''))){object=eval('('+json+')');if(typeof filter==='function'){function walk(k,v){if(v&&typeof v==='object'){for(var i in v){if(v.hasOwnProperty(i)){v[i]=walk(i,v[i]);}}} return filter(k,v);} object=walk('',object);}}}catch(e){} if(this.keepData){this.data=object;} return object;},write:function(value,pretty){this.pretty=!!pretty;var json=null;var type=typeof value;if(this.serialize[type]){try{json=(!this.pretty&&this.nativeJSON)?JSON.stringify(value):this.serialize[type].apply(this,[value]);}catch(err){OpenLayers.Console.error("Trouble serializing: "+err);}} return json;},writeIndent:function(){var pieces=[];if(this.pretty){for(var i=0;i0){pieces.push(',');} pieces.push(this.writeNewline(),this.writeIndent(),json);}} this.level-=1;pieces.push(this.writeNewline(),this.writeIndent(),']');return pieces.join('');},'string':function(string){var m={'\b':'\\b','\t':'\\t','\n':'\\n','\f':'\\f','\r':'\\r','"':'\\"','\\':'\\\\'};if(/["\\\x00-\x1f]/.test(string)){return'"'+string.replace(/([\x00-\x1f\\"])/g,function(a,b){var c=m[b];if(c){return c;} c=b.charCodeAt();return'\\u00'+ Math.floor(c/16).toString(16)+ (c%16).toString(16);})+'"';} return'"'+string+'"';},'number':function(number){return isFinite(number)?String(number):"null";},'boolean':function(bool){return String(bool);},'date':function(date){function format(number){return(number<10)?'0'+number:number;} return'"'+date.getFullYear()+'-'+ format(date.getMonth()+1)+'-'+ format(date.getDate())+'T'+ format(date.getHours())+':'+ format(date.getMinutes())+':'+ format(date.getSeconds())+'"';}},CLASS_NAME:"OpenLayers.Format.JSON"});OpenLayers.Handler.Drag=OpenLayers.Class(OpenLayers.Handler,{started:false,stopDown:true,dragging:false,last:null,start:null,lastMoveEvt:null,oldOnselectstart:null,interval:0,timeoutId:null,documentDrag:false,documentEvents:null,initialize:function(control,callbacks,options){OpenLayers.Handler.prototype.initialize.apply(this,arguments);if(this.documentDrag===true){var me=this;this._docMove=function(evt){me.mousemove({xy:{x:evt.clientX,y:evt.clientY},element:document});};this._docUp=function(evt){me.mouseup({xy:{x:evt.clientX,y:evt.clientY}});};}},dragstart:function(evt){var propagate=true;this.dragging=false;if(this.checkModifiers(evt)&&(OpenLayers.Event.isLeftClick(evt)||OpenLayers.Event.isSingleTouch(evt))){this.started=true;this.start=evt.xy;this.last=evt.xy;OpenLayers.Element.addClass(this.map.viewPortDiv,"olDragDown");this.down(evt);this.callback("down",[evt.xy]);OpenLayers.Event.preventDefault(evt);if(!this.oldOnselectstart){this.oldOnselectstart=document.onselectstart?document.onselectstart:OpenLayers.Function.True;} document.onselectstart=OpenLayers.Function.False;propagate=!this.stopDown;}else{this.started=false;this.start=null;this.last=null;} return propagate;},dragmove:function(evt){this.lastMoveEvt=evt;if(this.started&&!this.timeoutId&&(evt.xy.x!=this.last.x||evt.xy.y!=this.last.y)){if(this.documentDrag===true&&this.documentEvents){if(evt.element===document){this.adjustXY(evt);this.setEvent(evt);}else{this.removeDocumentEvents();}} if(this.interval>0){this.timeoutId=setTimeout(OpenLayers.Function.bind(this.removeTimeout,this),this.interval);} this.dragging=true;this.move(evt);this.callback("move",[evt.xy]);if(!this.oldOnselectstart){this.oldOnselectstart=document.onselectstart;document.onselectstart=OpenLayers.Function.False;} this.last=evt.xy;} return true;},dragend:function(evt){if(this.started){if(this.documentDrag===true&&this.documentEvents){this.adjustXY(evt);this.removeDocumentEvents();} var dragged=(this.start!=this.last);this.started=false;this.dragging=false;OpenLayers.Element.removeClass(this.map.viewPortDiv,"olDragDown");this.up(evt);this.callback("up",[evt.xy]);if(dragged){this.callback("done",[evt.xy]);} document.onselectstart=this.oldOnselectstart;} return true;},down:function(evt){},move:function(evt){},up:function(evt){},out:function(evt){},mousedown:function(evt){return this.dragstart(evt);},touchstart:function(evt){this.startTouch();return this.dragstart(evt);},mousemove:function(evt){return this.dragmove(evt);},touchmove:function(evt){return this.dragmove(evt);},removeTimeout:function(){this.timeoutId=null;if(this.dragging){this.mousemove(this.lastMoveEvt);}},mouseup:function(evt){return this.dragend(evt);},touchend:function(evt){evt.xy=this.last;return this.dragend(evt);},mouseout:function(evt){if(this.started&&OpenLayers.Util.mouseLeft(evt,this.map.viewPortDiv)){if(this.documentDrag===true){this.addDocumentEvents();}else{var dragged=(this.start!=this.last);this.started=false;this.dragging=false;OpenLayers.Element.removeClass(this.map.viewPortDiv,"olDragDown");this.out(evt);this.callback("out",[]);if(dragged){this.callback("done",[evt.xy]);} if(document.onselectstart){document.onselectstart=this.oldOnselectstart;}}} return true;},click:function(evt){return(this.start==this.last);},activate:function(){var activated=false;if(OpenLayers.Handler.prototype.activate.apply(this,arguments)){this.dragging=false;activated=true;} return activated;},deactivate:function(){var deactivated=false;if(OpenLayers.Handler.prototype.deactivate.apply(this,arguments)){this.started=false;this.dragging=false;this.start=null;this.last=null;deactivated=true;OpenLayers.Element.removeClass(this.map.viewPortDiv,"olDragDown");} return deactivated;},adjustXY:function(evt){var pos=OpenLayers.Util.pagePosition(this.map.viewPortDiv);evt.xy.x-=pos[0];evt.xy.y-=pos[1];},addDocumentEvents:function(){OpenLayers.Element.addClass(document.body,"olDragDown");this.documentEvents=true;OpenLayers.Event.observe(document,"mousemove",this._docMove);OpenLayers.Event.observe(document,"mouseup",this._docUp);},removeDocumentEvents:function(){OpenLayers.Element.removeClass(document.body,"olDragDown");this.documentEvents=false;OpenLayers.Event.stopObserving(document,"mousemove",this._docMove);OpenLayers.Event.stopObserving(document,"mouseup",this._docUp);},CLASS_NAME:"OpenLayers.Handler.Drag"});OpenLayers.Handler.Box=OpenLayers.Class(OpenLayers.Handler,{dragHandler:null,boxDivClassName:'olHandlerBoxZoomBox',boxOffsets:null,initialize:function(control,callbacks,options){OpenLayers.Handler.prototype.initialize.apply(this,arguments);this.dragHandler=new OpenLayers.Handler.Drag(this,{down:this.startBox,move:this.moveBox,out:this.removeBox,up:this.endBox},{keyMask:this.keyMask});},destroy:function(){OpenLayers.Handler.prototype.destroy.apply(this,arguments);if(this.dragHandler){this.dragHandler.destroy();this.dragHandler=null;}},setMap:function(map){OpenLayers.Handler.prototype.setMap.apply(this,arguments);if(this.dragHandler){this.dragHandler.setMap(map);}},startBox:function(xy){this.callback("start",[]);this.zoomBox=OpenLayers.Util.createDiv('zoomBox',{x:-9999,y:-9999});this.zoomBox.className=this.boxDivClassName;this.zoomBox.style.zIndex=this.map.Z_INDEX_BASE["Popup"]-1;this.map.viewPortDiv.appendChild(this.zoomBox);OpenLayers.Element.addClass(this.map.viewPortDiv,"olDrawBox");},moveBox:function(xy){var startX=this.dragHandler.start.x;var startY=this.dragHandler.start.y;var deltaX=Math.abs(startX-xy.x);var deltaY=Math.abs(startY-xy.y);var offset=this.getBoxOffsets();this.zoomBox.style.width=(deltaX+offset.width+1)+"px";this.zoomBox.style.height=(deltaY+offset.height+1)+"px";this.zoomBox.style.left=(xy.x5||Math.abs(this.dragHandler.start.y-end.y)>5){var start=this.dragHandler.start;var top=Math.min(start.y,end.y);var bottom=Math.max(start.y,end.y);var left=Math.min(start.x,end.x);var right=Math.max(start.x,end.x);result=new OpenLayers.Bounds(left,bottom,right,top);}else{result=this.dragHandler.start.clone();} this.removeBox();this.callback("done",[result]);},removeBox:function(){this.map.viewPortDiv.removeChild(this.zoomBox);this.zoomBox=null;this.boxOffsets=null;OpenLayers.Element.removeClass(this.map.viewPortDiv,"olDrawBox");},activate:function(){if(OpenLayers.Handler.prototype.activate.apply(this,arguments)){this.dragHandler.activate();return true;}else{return false;}},deactivate:function(){if(OpenLayers.Handler.prototype.deactivate.apply(this,arguments)){if(this.dragHandler.deactivate()){if(this.zoomBox){this.removeBox();}} return true;}else{return false;}},getBoxOffsets:function(){if(!this.boxOffsets){var testDiv=document.createElement("div");testDiv.style.position="absolute";testDiv.style.border="1px solid black";testDiv.style.width="3px";document.body.appendChild(testDiv);var w3cBoxModel=testDiv.clientWidth==3;document.body.removeChild(testDiv);var left=parseInt(OpenLayers.Element.getStyle(this.zoomBox,"border-left-width"));var right=parseInt(OpenLayers.Element.getStyle(this.zoomBox,"border-right-width"));var top=parseInt(OpenLayers.Element.getStyle(this.zoomBox,"border-top-width"));var bottom=parseInt(OpenLayers.Element.getStyle(this.zoomBox,"border-bottom-width"));this.boxOffsets={left:left,right:right,top:top,bottom:bottom,width:w3cBoxModel===false?left+right:0,height:w3cBoxModel===false?top+bottom:0};} return this.boxOffsets;},CLASS_NAME:"OpenLayers.Handler.Box"});OpenLayers.Control.ZoomBox=OpenLayers.Class(OpenLayers.Control,{type:OpenLayers.Control.TYPE_TOOL,out:false,keyMask:null,alwaysZoom:false,zoomOnClick:true,draw:function(){this.handler=new OpenLayers.Handler.Box(this,{done:this.zoomBox},{keyMask:this.keyMask});},zoomBox:function(position){if(position instanceof OpenLayers.Bounds){var bounds,targetCenterPx=position.getCenterPixel();if(!this.out){var minXY=this.map.getLonLatFromPixel({x:position.left,y:position.bottom});var maxXY=this.map.getLonLatFromPixel({x:position.right,y:position.top});bounds=new OpenLayers.Bounds(minXY.lon,minXY.lat,maxXY.lon,maxXY.lat);}else{var pixWidth=position.right-position.left;var pixHeight=position.bottom-position.top;var zoomFactor=Math.min((this.map.size.h/pixHeight),(this.map.size.w/pixWidth));var extent=this.map.getExtent();var center=this.map.getLonLatFromPixel(targetCenterPx);var xmin=center.lon-(extent.getWidth()/2)*zoomFactor;var xmax=center.lon+(extent.getWidth()/2)*zoomFactor;var ymin=center.lat-(extent.getHeight()/2)*zoomFactor;var ymax=center.lat+(extent.getHeight()/2)*zoomFactor;bounds=new OpenLayers.Bounds(xmin,ymin,xmax,ymax);} var lastZoom=this.map.getZoom(),size=this.map.getSize(),centerPx={x:size.w/2,y:size.h/2},zoom=this.map.getZoomForExtent(bounds),oldRes=this.map.getResolution(),newRes=this.map.getResolutionForZoom(zoom),zoomOriginPx={x:(oldRes*targetCenterPx.x-newRes*centerPx.x)/(oldRes-newRes),y:(oldRes*targetCenterPx.y-newRes*centerPx.y)/(oldRes-newRes)};this.map.zoomTo(zoom,zoomOriginPx);if(lastZoom==this.map.getZoom()&&this.alwaysZoom==true){this.map.zoomTo(lastZoom+(this.out?-1:1));}}else if(this.zoomOnClick){if(!this.out){this.map.zoomTo(this.map.getZoom()+1,position);}else{this.map.zoomTo(this.map.getZoom()-1,position);}}},CLASS_NAME:"OpenLayers.Control.ZoomBox"});OpenLayers.Control.DragPan=OpenLayers.Class(OpenLayers.Control,{type:OpenLayers.Control.TYPE_TOOL,panned:false,interval:1,documentDrag:false,kinetic:null,enableKinetic:true,kineticInterval:10,draw:function(){if(this.enableKinetic&&OpenLayers.Kinetic){var config={interval:this.kineticInterval};if(typeof this.enableKinetic==="object"){config=OpenLayers.Util.extend(config,this.enableKinetic);} this.kinetic=new OpenLayers.Kinetic(config);} this.handler=new OpenLayers.Handler.Drag(this,{"move":this.panMap,"done":this.panMapDone,"down":this.panMapStart},{interval:this.interval,documentDrag:this.documentDrag});},panMapStart:function(){if(this.kinetic){this.kinetic.begin();}},panMap:function(xy){if(this.kinetic){this.kinetic.update(xy);} this.panned=true;this.map.pan(this.handler.last.x-xy.x,this.handler.last.y-xy.y,{dragging:true,animate:false});},panMapDone:function(xy){if(this.panned){var res=null;if(this.kinetic){res=this.kinetic.end(xy);} this.map.pan(this.handler.last.x-xy.x,this.handler.last.y-xy.y,{dragging:!!res,animate:false});if(res){var self=this;this.kinetic.move(res,function(x,y,end){self.map.pan(x,y,{dragging:!end,animate:false});});} this.panned=false;}},CLASS_NAME:"OpenLayers.Control.DragPan"});OpenLayers.Handler.MouseWheel=OpenLayers.Class(OpenLayers.Handler,{wheelListener:null,interval:0,maxDelta:Number.POSITIVE_INFINITY,delta:0,cumulative:true,initialize:function(control,callbacks,options){OpenLayers.Handler.prototype.initialize.apply(this,arguments);this.wheelListener=OpenLayers.Function.bindAsEventListener(this.onWheelEvent,this);},destroy:function(){OpenLayers.Handler.prototype.destroy.apply(this,arguments);this.wheelListener=null;},onWheelEvent:function(e){if(!this.map||!this.checkModifiers(e)){return;} var overScrollableDiv=false;var allowScroll=false;var overMapDiv=false;var elem=OpenLayers.Event.element(e);while((elem!=null)&&!overMapDiv&&!overScrollableDiv){if(!overScrollableDiv){try{var overflow;if(elem.currentStyle){overflow=elem.currentStyle["overflow"];}else{var style=document.defaultView.getComputedStyle(elem,null);overflow=style.getPropertyValue("overflow");} overScrollableDiv=(overflow&&(overflow=="auto")||(overflow=="scroll"));}catch(err){}} if(!allowScroll){allowScroll=OpenLayers.Element.hasClass(elem,'olScrollable');if(!allowScroll){for(var i=0,len=this.map.layers.length;i=0;--i){this.target.register(this.events[i],this,this.buttonClick,{extension:true});}},destroy:function(){for(var i=this.events.length-1;i>=0;--i){this.target.unregister(this.events[i],this,this.buttonClick);} delete this.target;},getPressedButton:function(element){var depth=3,button;do{if(OpenLayers.Element.hasClass(element,"olButton")){button=element;break;} element=element.parentNode;}while(--depth>0&&element);return button;},ignore:function(element){var depth=3,ignore=false;do{if(element.nodeName.toLowerCase()==='a'){ignore=true;break;} element=element.parentNode;}while(--depth>0&&element);return ignore;},buttonClick:function(evt){var propagate=true,element=OpenLayers.Event.element(evt);if(element&&(OpenLayers.Event.isLeftClick(evt)||!~evt.type.indexOf("mouse"))){var button=this.getPressedButton(element);if(button){if(evt.type==="keydown"){switch(evt.keyCode){case OpenLayers.Event.KEY_RETURN:case OpenLayers.Event.KEY_SPACE:this.target.triggerEvent("buttonclick",{buttonElement:button});OpenLayers.Event.stop(evt);propagate=false;break;}}else if(this.startEvt){if(this.completeRegEx.test(evt.type)){var pos=OpenLayers.Util.pagePosition(button);var viewportElement=OpenLayers.Util.getViewportElement();var scrollTop=window.pageYOffset||viewportElement.scrollTop;var scrollLeft=window.pageXOffset||viewportElement.scrollLeft;pos[0]=pos[0]-scrollLeft;pos[1]=pos[1]-scrollTop;this.target.triggerEvent("buttonclick",{buttonElement:button,buttonXY:{x:this.startEvt.clientX-pos[0],y:this.startEvt.clientY-pos[1]}});} if(this.cancelRegEx.test(evt.type)){delete this.startEvt;} OpenLayers.Event.stop(evt);propagate=false;} if(this.startRegEx.test(evt.type)){this.startEvt=evt;OpenLayers.Event.stop(evt);propagate=false;}}else{propagate=!this.ignore(OpenLayers.Event.element(evt));delete this.startEvt;}} return propagate;}});OpenLayers.Layer.WMTS=OpenLayers.Class(OpenLayers.Layer.Grid,{isBaseLayer:true,version:"1.0.0",requestEncoding:"KVP",url:null,layer:null,matrixSet:null,style:null,format:"image/jpeg",tileOrigin:null,tileFullExtent:null,formatSuffix:null,matrixIds:null,dimensions:null,params:null,zoomOffset:0,serverResolutions:null,formatSuffixMap:{"image/png":"png","image/png8":"png","image/png24":"png","image/png32":"png","png":"png","image/jpeg":"jpg","image/jpg":"jpg","jpeg":"jpg","jpg":"jpg"},matrix:null,initialize:function(config){var required={url:true,layer:true,style:true,matrixSet:true};for(var prop in required){if(!(prop in config)){throw new Error("Missing property '"+prop+"' in layer configuration.");}} config.params=OpenLayers.Util.upperCaseObject(config.params);var args=[config.name,config.url,config.params,config];OpenLayers.Layer.Grid.prototype.initialize.apply(this,args);if(!this.formatSuffix){this.formatSuffix=this.formatSuffixMap[this.format]||this.format.split("/").pop();} if(this.matrixIds){var len=this.matrixIds.length;if(len&&typeof this.matrixIds[0]==="string"){var ids=this.matrixIds;this.matrixIds=new Array(len);for(var i=0;i=0;--i){dimension=dimensions[i];context[dimension]=params[dimension.toUpperCase()];}} url=OpenLayers.String.format(template,context);}else{var path=this.version+"/"+this.layer+"/"+this.style+"/";if(dimensions){for(var i=0;i4) this._object.open(sMethod,sUrl,bAsync,sUser,sPassword);else if(arguments.length>3) this._object.open(sMethod,sUrl,bAsync,sUser);else this._object.open(sMethod,sUrl,bAsync);this.readyState=cXMLHttpRequest.OPENED;fReadyStateChange(this);this._object.onreadystatechange=function(){if(bGecko&&!bAsync) return;oRequest.readyState=oRequest._object.readyState;fSynchronizeValues(oRequest);if(oRequest._aborted){oRequest.readyState=cXMLHttpRequest.UNSENT;return;} if(oRequest.readyState==cXMLHttpRequest.DONE){delete oRequest._data;fCleanTransport(oRequest);if(bIE&&bAsync) window.detachEvent("onunload",fOnUnload);} if(nState!=oRequest.readyState) fReadyStateChange(oRequest);nState=oRequest.readyState;}};function fXMLHttpRequest_send(oRequest){oRequest._object.send(oRequest._data);if(bGecko&&!oRequest._async){oRequest.readyState=cXMLHttpRequest.OPENED;fSynchronizeValues(oRequest);while(oRequest.readyStatecXMLHttpRequest.UNSENT) this._aborted=true;this._object.abort();fCleanTransport(this);this.readyState=cXMLHttpRequest.UNSENT;delete this._data;};cXMLHttpRequest.prototype.getAllResponseHeaders=function(){return this._object.getAllResponseHeaders();};cXMLHttpRequest.prototype.getResponseHeader=function(sName){return this._object.getResponseHeader(sName);};cXMLHttpRequest.prototype.setRequestHeader=function(sName,sValue){if(!this._headers) this._headers={};this._headers[sName]=sValue;return this._object.setRequestHeader(sName,sValue);};cXMLHttpRequest.prototype.addEventListener=function(sName,fHandler,bUseCapture){for(var nIndex=0,oListener;oListener=this._listeners[nIndex];nIndex++) if(oListener[0]==sName&&oListener[1]==fHandler&&oListener[2]==bUseCapture) return;this._listeners.push([sName,fHandler,bUseCapture]);};cXMLHttpRequest.prototype.removeEventListener=function(sName,fHandler,bUseCapture){for(var nIndex=0,oListener;oListener=this._listeners[nIndex];nIndex++) if(oListener[0]==sName&&oListener[1]==fHandler&&oListener[2]==bUseCapture) break;if(oListener) this._listeners.splice(nIndex,1);};cXMLHttpRequest.prototype.dispatchEvent=function(oEvent){var oEventPseudo={'type':oEvent.type,'target':this,'currentTarget':this,'eventPhase':2,'bubbles':oEvent.bubbles,'cancelable':oEvent.cancelable,'timeStamp':oEvent.timeStamp,'stopPropagation':function(){},'preventDefault':function(){},'initEvent':function(){}};if(oEventPseudo.type=="readystatechange"&&this.onreadystatechange) (this.onreadystatechange.handleEvent||this.onreadystatechange).apply(this,[oEventPseudo]);for(var nIndex=0,oListener;oListener=this._listeners[nIndex];nIndex++) if(oListener[0]==oEventPseudo.type&&!oListener[2]) (oListener[1].handleEvent||oListener[1]).apply(this,[oEventPseudo]);};cXMLHttpRequest.prototype.toString=function(){return'['+"object"+' '+"XMLHttpRequest"+']';};cXMLHttpRequest.toString=function(){return'['+"XMLHttpRequest"+']';};function fReadyStateChange(oRequest){if(cXMLHttpRequest.onreadystatechange) cXMLHttpRequest.onreadystatechange.apply(oRequest);oRequest.dispatchEvent({'type':"readystatechange",'bubbles':false,'cancelable':false,'timeStamp':new Date+0});};function fGetDocument(oRequest){var oDocument=oRequest.responseXML,sResponse=oRequest.responseText;if(bIE&&sResponse&&oDocument&&!oDocument.documentElement&&oRequest.getResponseHeader("Content-Type").match(/[^\/]+\/[^\+]+\+xml/)){oDocument=new window.ActiveXObject("Microsoft.XMLDOM");oDocument.async=false;oDocument.validateOnParse=false;oDocument.loadXML(sResponse);} if(oDocument) if((bIE&&oDocument.parseError!=0)||!oDocument.documentElement||(oDocument.documentElement&&oDocument.documentElement.tagName=="parsererror")) return null;return oDocument;};function fSynchronizeValues(oRequest){try{oRequest.responseText=oRequest._object.responseText;}catch(e){} try{oRequest.responseXML=fGetDocument(oRequest._object);}catch(e){} try{oRequest.status=oRequest._object.status;}catch(e){} try{oRequest.statusText=oRequest._object.statusText;}catch(e){}};function fCleanTransport(oRequest){oRequest._object.onreadystatechange=new window.Function;};if(!window.Function.prototype.apply){window.Function.prototype.apply=function(oRequest,oArguments){if(!oArguments) oArguments=[];oRequest.__func=this;oRequest.__func(oArguments[0],oArguments[1],oArguments[2],oArguments[3],oArguments[4]);delete oRequest.__func;};};if(!OpenLayers.Request){OpenLayers.Request={};} OpenLayers.Request.XMLHttpRequest=cXMLHttpRequest;})();OpenLayers.ProxyHost="";if(!OpenLayers.Request){OpenLayers.Request={};} OpenLayers.Util.extend(OpenLayers.Request,{DEFAULT_CONFIG:{method:"GET",url:window.location.href,async:true,user:undefined,password:undefined,params:null,proxy:OpenLayers.ProxyHost,headers:{},data:null,callback:function(){},success:null,failure:null,scope:null},URL_SPLIT_REGEX:/([^:]*:)\/\/([^:]*:?[^@]*@)?([^:\/\?]*):?([^\/\?]*)/,events:new OpenLayers.Events(this),makeSameOrigin:function(url,proxy){var sameOrigin=url.indexOf("http")!==0;var urlParts=!sameOrigin&&url.match(this.URL_SPLIT_REGEX);if(urlParts){var location=window.location;sameOrigin=urlParts[1]==location.protocol&&urlParts[3]==location.hostname;var uPort=urlParts[4],lPort=location.port;if(uPort!=80&&uPort!=""||lPort!="80"&&lPort!=""){sameOrigin=sameOrigin&&uPort==lPort;}} if(!sameOrigin){if(proxy){if(typeof proxy=="function"){url=proxy(url);}else{url=proxy+encodeURIComponent(url);}}} return url;},issue:function(config){var defaultConfig=OpenLayers.Util.extend(this.DEFAULT_CONFIG,{proxy:OpenLayers.ProxyHost});config=config||{};config.headers=config.headers||{};config=OpenLayers.Util.applyDefaults(config,defaultConfig);config.headers=OpenLayers.Util.applyDefaults(config.headers,defaultConfig.headers);var customRequestedWithHeader=false,headerKey;for(headerKey in config.headers){if(config.headers.hasOwnProperty(headerKey)){if(headerKey.toLowerCase()==='x-requested-with'){customRequestedWithHeader=true;}}} if(customRequestedWithHeader===false){config.headers['X-Requested-With']='XMLHttpRequest';} var request=new OpenLayers.Request.XMLHttpRequest();var url=OpenLayers.Util.urlAppend(config.url,OpenLayers.Util.getParameterString(config.params||{}));url=OpenLayers.Request.makeSameOrigin(url,config.proxy);request.open(config.method,url,config.async,config.user,config.password);for(var header in config.headers){request.setRequestHeader(header,config.headers[header]);} var events=this.events;var self=this;request.onreadystatechange=function(){if(request.readyState==OpenLayers.Request.XMLHttpRequest.DONE){var proceed=events.triggerEvent("complete",{request:request,config:config,requestUrl:url});if(proceed!==false){self.runCallbacks({request:request,config:config,requestUrl:url});}}};if(config.async===false){request.send(config.data);}else{window.setTimeout(function(){if(request.readyState!==0){request.send(config.data);}},0);} return request;},runCallbacks:function(options){var request=options.request;var config=options.config;var complete=(config.scope)?OpenLayers.Function.bind(config.callback,config.scope):config.callback;var success;if(config.success){success=(config.scope)?OpenLayers.Function.bind(config.success,config.scope):config.success;} var failure;if(config.failure){failure=(config.scope)?OpenLayers.Function.bind(config.failure,config.scope):config.failure;} if(OpenLayers.Util.createUrlObject(config.url).protocol=="file:"&&request.responseText){request.status=200;} complete(request);if(!request.status||(request.status>=200&&request.status<300)){this.events.triggerEvent("success",options);if(success){success(request);}} if(request.status&&(request.status<200||request.status>=300)){this.events.triggerEvent("failure",options);if(failure){failure(request);}}},GET:function(config){config=OpenLayers.Util.extend(config,{method:"GET"});return OpenLayers.Request.issue(config);},POST:function(config){config=OpenLayers.Util.extend(config,{method:"POST"});config.headers=config.headers?config.headers:{};if(!("CONTENT-TYPE"in OpenLayers.Util.upperCaseObject(config.headers))){config.headers["Content-Type"]="application/xml";} return OpenLayers.Request.issue(config);},PUT:function(config){config=OpenLayers.Util.extend(config,{method:"PUT"});config.headers=config.headers?config.headers:{};if(!("CONTENT-TYPE"in OpenLayers.Util.upperCaseObject(config.headers))){config.headers["Content-Type"]="application/xml";} return OpenLayers.Request.issue(config);},DELETE:function(config){config=OpenLayers.Util.extend(config,{method:"DELETE"});return OpenLayers.Request.issue(config);},HEAD:function(config){config=OpenLayers.Util.extend(config,{method:"HEAD"});return OpenLayers.Request.issue(config);},OPTIONS:function(config){config=OpenLayers.Util.extend(config,{method:"OPTIONS"});return OpenLayers.Request.issue(config);}});OpenLayers.Tile.UTFGrid=OpenLayers.Class(OpenLayers.Tile,{url:null,utfgridResolution:2,json:null,format:null,destroy:function(){this.clear();OpenLayers.Tile.prototype.destroy.apply(this,arguments);},draw:function(){var drawn=OpenLayers.Tile.prototype.draw.apply(this,arguments);if(drawn){if(this.isLoading){this.abortLoading();this.events.triggerEvent("reload");}else{this.isLoading=true;this.events.triggerEvent("loadstart");} this.url=this.layer.getURL(this.bounds);if(this.layer.useJSONP){var ols=new OpenLayers.Protocol.Script({url:this.url,callback:function(response){this.isLoading=false;this.events.triggerEvent("loadend");this.json=response.data;},scope:this});ols.read();this.request=ols;}else{this.request=OpenLayers.Request.GET({url:this.url,callback:function(response){this.isLoading=false;this.events.triggerEvent("loadend");if(response.status===200){this.parseData(response.responseText);}},scope:this});}}else{this.unload();} return drawn;},abortLoading:function(){if(this.request){this.request.abort();delete this.request;} this.isLoading=false;},getFeatureInfo:function(i,j){var info=null;if(this.json){var id=this.getFeatureId(i,j);if(id!==null){info={id:id,data:this.json.data[id]};}} return info;},getFeatureId:function(i,j){var id=null;if(this.json){var resolution=this.utfgridResolution;var row=Math.floor(j/resolution);var col=Math.floor(i/resolution);var charCode=this.json.grid[row].charCodeAt(col);var index=this.indexFromCharCode(charCode);var keys=this.json.keys;if(!isNaN(index)&&(index in keys)){id=keys[index];}} return id;},indexFromCharCode:function(charCode){if(charCode>=93){charCode--;} if(charCode>=35){charCode--;} return charCode-32;},parseData:function(str){if(!this.format){this.format=new OpenLayers.Format.JSON();} this.json=this.format.read(str);},clear:function(){this.json=null;},CLASS_NAME:"OpenLayers.Tile.UTFGrid"});OpenLayers.Layer.WMTSUTFGrid=OpenLayers.Class(OpenLayers.Layer.WMTS,{isBaseLayer:false,useJSONP:false,tileClass:OpenLayers.Tile.UTFGrid,initialize:function(options){OpenLayers.Layer.WMTS.prototype.initialize.apply(this,[options]);this.tileOptions=OpenLayers.Util.extend({utfgridResolution:this.utfgridResolution},this.tileOptions);},clone:function(obj){if(obj==null){obj=new OpenLayers.Layer.WMTSUTFGrid(this.getOptions());} obj=OpenLayers.Layer.WMTS.prototype.clone.apply(this,[obj]);return obj;},getFeatureInfo:function(location){var info=null;var tileInfo=this.getTileData(location);if(tileInfo.tile){info=tileInfo.tile.getFeatureInfo(tileInfo.i,tileInfo.j);} return info;},getFeatureId:function(location){var id=null;var info=this.getTileData(location);if(info.tile){id=info.tile.getFeatureId(info.i,info.j);} return id;},CLASS_NAME:"OpenLayers.Layer.WMTSUTFGrid"});OpenLayers.Control.MousePosition=OpenLayers.Class(OpenLayers.Control,{autoActivate:true,element:null,prefix:'',separator:', ',suffix:'',numDigits:5,granularity:10,emptyString:null,lastXy:null,displayProjection:null,destroy:function(){this.deactivate();OpenLayers.Control.prototype.destroy.apply(this,arguments);},activate:function(){if(OpenLayers.Control.prototype.activate.apply(this,arguments)){this.map.events.register('mousemove',this,this.redraw);this.map.events.register('mouseout',this,this.reset);this.redraw();return true;}else{return false;}},deactivate:function(){if(OpenLayers.Control.prototype.deactivate.apply(this,arguments)){this.map.events.unregister('mousemove',this,this.redraw);this.map.events.unregister('mouseout',this,this.reset);this.element.innerHTML="";return true;}else{return false;}},draw:function(){OpenLayers.Control.prototype.draw.apply(this,arguments);if(!this.element){this.div.left="";this.div.top="";this.element=this.div;} return this.div;},redraw:function(evt){var lonLat;if(evt==null){this.reset();return;}else{if(this.lastXy==null||Math.abs(evt.xy.x-this.lastXy.x)>this.granularity||Math.abs(evt.xy.y-this.lastXy.y)>this.granularity) {this.lastXy=evt.xy;return;} lonLat=this.map.getLonLatFromPixel(evt.xy);if(!lonLat){return;} if(this.displayProjection){lonLat.transform(this.map.getProjectionObject(),this.displayProjection);} this.lastXy=evt.xy;} var newHtml=this.formatOutput(lonLat);if(newHtml!=this.element.innerHTML){this.element.innerHTML=newHtml;}},reset:function(evt){if(this.emptyString!=null){this.element.innerHTML=this.emptyString;}},formatOutput:function(lonLat){var digits=parseInt(this.numDigits);var newHtml=this.prefix+ lonLat.lon.toFixed(digits)+ this.separator+ lonLat.lat.toFixed(digits)+ this.suffix;return newHtml;},CLASS_NAME:"OpenLayers.Control.MousePosition"});OpenLayers.Control.ArgParser=OpenLayers.Class(OpenLayers.Control,{center:null,zoom:null,layers:null,displayProjection:null,getParameters:function(url){url=url||window.location.href;var parameters=OpenLayers.Util.getParameters(url);var index=url.indexOf('#');if(index>0){url='?'+url.substring(index+1,url.length);OpenLayers.Util.extend(parameters,OpenLayers.Util.getParameters(url));} return parameters;},setMap:function(map){OpenLayers.Control.prototype.setMap.apply(this,arguments);for(var i=0,len=this.map.controls.length;i {{mapcache['memcache_host']}} {{mapcache['memcache_port']}} {% for gridname, grid in grids.items() %} {{grid['tile_size']}} {{grid['tile_size']}} {{grid['bbox'][0]}} {{grid['bbox'][1]}} {{grid['bbox'][2]}} {{grid['bbox'][3]}} {{grid['srs']}} {{grid['unit']}} {% for r in grid['resolutions'] %}{{r}} {% endfor %} top-left {% endfor %} {% for layername, layer in layers.items() %}{% if layer['type'] == 'wms' or 'wms_url' in layer %} {% for key, value in layer['params'].items() %} <{{key}}>{{value}}{% endfor %} {{layer['wms_url'] if 'wms_url' in layer else layer['url']}} {% for key, value in layer['headers'].items() %} <{{key}}>{{value}}{% endfor %} {% endif %}{% endfor %} {% for layername, layer in layers.items() %}{% if layer['type'] == 'wms' or 'wms_url' in layer %} {{layername}} default {{layer['grid']}}{% if layer['meta'] %} {{layer['meta_size']}} {{layer['meta_size']}} {{layer['meta_buffer']}}{% endif %} {{layer['mime_type']}} 3600 13800 {% if len(layer['dimensions']) > 0 %} {% for dim in layer['dimensions'] %} {{ ','.join(dim['values']) }}{% endfor %} {% endif %} {% endif %}{% endfor %} fast 256 90 rgb image/jpeg report /tmp """ PKvBz+1&&$tilecloud_chain/OpenLayers-style.cssdiv.olMap { z-index: 0; padding: 0 !important; margin: 0 !important; cursor: default; } div.olMapViewport { text-align: left; } div.olLayerDiv { -moz-user-select: none; -khtml-user-select: none; } .olLayerGoogleCopyright { left: 2px; bottom: 2px; } .olLayerGoogleV3.olLayerGoogleCopyright { right: auto !important; } .olLayerGooglePoweredBy { left: 2px; bottom: 15px; } .olLayerGoogleV3.olLayerGooglePoweredBy { bottom: 15px !important; } .olControlAttribution { font-size: smaller; right: 3px; bottom: 4.5em; position: absolute; display: block; } .olControlScale { right: 3px; bottom: 3em; display: block; position: absolute; font-size: smaller; } .olControlScaleLine { display: block; position: absolute; left: 10px; bottom: 15px; font-size: xx-small; } .olControlScaleLineBottom { border: solid 2px black; border-bottom: none; margin-top:-2px; text-align: center; } .olControlScaleLineTop { border: solid 2px black; border-top: none; text-align: center; } .olControlPermalink { right: 3px; bottom: 1.5em; display: block; position: absolute; font-size: smaller; } div.olControlMousePosition { bottom: 0; right: 3px; display: block; position: absolute; font-family: Arial; font-size: smaller; } .olControlOverviewMapContainer { position: absolute; bottom: 0; right: 0; } .olControlOverviewMapElement { padding: 10px 18px 10px 10px; background-color: #00008B; -moz-border-radius: 1em 0 0 0; } .olControlOverviewMapMinimizeButton, .olControlOverviewMapMaximizeButton { height: 18px; width: 18px; right: 0; bottom: 80px; cursor: pointer; } .olControlOverviewMapExtentRectangle { overflow: hidden; background-image: url("img/blank.gif"); cursor: move; border: 2px dotted red; } .olControlOverviewMapRectReplacement { overflow: hidden; cursor: move; background-image: url("img/overview_replacement.gif"); background-repeat: no-repeat; background-position: center; } .olLayerGeoRSSDescription { float:left; width:100%; overflow:auto; font-size:1.0em; } .olLayerGeoRSSClose { float:right; color:gray; font-size:1.2em; margin-right:6px; font-family:sans-serif; } .olLayerGeoRSSTitle { float:left;font-size:1.2em; } .olPopupContent { padding:5px; overflow: auto; } .olControlNavigationHistory { background-image: url("img/navigation_history.png"); background-repeat: no-repeat; width: 24px; height: 24px; } .olControlNavigationHistoryPreviousItemActive { background-position: 0 0; } .olControlNavigationHistoryPreviousItemInactive { background-position: 0 -24px; } .olControlNavigationHistoryNextItemActive { background-position: -24px 0; } .olControlNavigationHistoryNextItemInactive { background-position: -24px -24px; } div.olControlSaveFeaturesItemActive { background-image: url(img/save_features_on.png); background-repeat: no-repeat; background-position: 0 1px; } div.olControlSaveFeaturesItemInactive { background-image: url(img/save_features_off.png); background-repeat: no-repeat; background-position: 0 1px; } .olHandlerBoxZoomBox { border: 2px solid red; position: absolute; background-color: white; opacity: 0.50; font-size: 1px; filter: alpha(opacity=50); } .olHandlerBoxSelectFeature { border: 2px solid blue; position: absolute; background-color: white; opacity: 0.50; font-size: 1px; filter: alpha(opacity=50); } .olControlPanPanel { top: 10px; left: 5px; } .olControlPanPanel div { background-image: url(img/pan-panel.png); height: 18px; width: 18px; cursor: pointer; position: absolute; } .olControlPanPanel .olControlPanNorthItemInactive { top: 0; left: 9px; background-position: 0 0; } .olControlPanPanel .olControlPanSouthItemInactive { top: 36px; left: 9px; background-position: 18px 0; } .olControlPanPanel .olControlPanWestItemInactive { position: absolute; top: 18px; left: 0; background-position: 0 18px; } .olControlPanPanel .olControlPanEastItemInactive { top: 18px; left: 18px; background-position: 18px 18px; } .olControlZoomPanel { top: 71px; left: 14px; } .olControlZoomPanel div { background-image: url(img/zoom-panel.png); position: absolute; height: 18px; width: 18px; cursor: pointer; } .olControlZoomPanel .olControlZoomInItemInactive { top: 0; left: 0; background-position: 0 0; } .olControlZoomPanel .olControlZoomToMaxExtentItemInactive { top: 18px; left: 0; background-position: 0 -18px; } .olControlZoomPanel .olControlZoomOutItemInactive { top: 36px; left: 0; background-position: 0 18px; } /* * When a potential text is bigger than the image it move the image * with some headers (closes #3154) */ .olControlPanZoomBar div { font-size: 1px; } .olPopupCloseBox { background: url("img/close.gif") no-repeat; cursor: pointer; } .olFramedCloudPopupContent { padding: 5px; overflow: auto; } .olControlNoSelect { -moz-user-select: none; -khtml-user-select: none; } .olImageLoadError { background-color: pink; opacity: 0.5; filter: alpha(opacity=50); /* IE */ } /** * Cursor styles */ .olCursorWait { cursor: wait; } .olDragDown { cursor: move; } .olDrawBox { cursor: crosshair; } .olControlDragFeatureOver { cursor: move; } .olControlDragFeatureActive.olControlDragFeatureOver.olDragDown { cursor: -moz-grabbing; } /** * Layer switcher */ .olControlLayerSwitcher { position: absolute; top: 25px; right: 0; width: 20em; font-family: sans-serif; font-weight: bold; margin-top: 3px; margin-left: 3px; margin-bottom: 3px; font-size: smaller; color: white; background-color: transparent; } .olControlLayerSwitcher .layersDiv { padding-top: 5px; padding-left: 10px; padding-bottom: 5px; padding-right: 10px; background-color: darkblue; } .olControlLayerSwitcher .layersDiv .baseLbl, .olControlLayerSwitcher .layersDiv .dataLbl { margin-top: 3px; margin-left: 3px; margin-bottom: 3px; } .olControlLayerSwitcher .layersDiv .baseLayersDiv, .olControlLayerSwitcher .layersDiv .dataLayersDiv { padding-left: 10px; } .olControlLayerSwitcher .maximizeDiv, .olControlLayerSwitcher .minimizeDiv { width: 18px; height: 18px; top: 5px; right: 0; cursor: pointer; } .olBingAttribution { color: #DDD; } .olBingAttribution.road { color: #333; } .olGoogleAttribution.hybrid, .olGoogleAttribution.satellite { color: #EEE; } .olGoogleAttribution { color: #333; } span.olGoogleAttribution a { color: #77C; } span.olGoogleAttribution.hybrid a, span.olGoogleAttribution.satellite a { color: #EEE; } /** * Editing and navigation icons. * (using the editing_tool_bar.png sprint image) */ .olControlNavToolbar , .olControlEditingToolbar { margin: 5px 5px 0 0; } .olControlNavToolbar div, .olControlEditingToolbar div { background-image: url("img/editing_tool_bar.png"); background-repeat: no-repeat; margin: 0 0 5px 5px; width: 24px; height: 22px; cursor: pointer } /* positions */ .olControlEditingToolbar { right: 0; top: 0; } .olControlNavToolbar { top: 295px; left: 9px; } /* layouts */ .olControlEditingToolbar div { float: right; } /* individual controls */ .olControlNavToolbar .olControlNavigationItemInactive, .olControlEditingToolbar .olControlNavigationItemInactive { background-position: -103px -1px; } .olControlNavToolbar .olControlNavigationItemActive , .olControlEditingToolbar .olControlNavigationItemActive { background-position: -103px -24px; } .olControlNavToolbar .olControlZoomBoxItemInactive { background-position: -128px -1px; } .olControlNavToolbar .olControlZoomBoxItemActive { background-position: -128px -24px; } .olControlEditingToolbar .olControlDrawFeaturePointItemInactive { background-position: -77px -1px; } .olControlEditingToolbar .olControlDrawFeaturePointItemActive { background-position: -77px -24px; } .olControlEditingToolbar .olControlDrawFeaturePathItemInactive { background-position: -51px -1px; } .olControlEditingToolbar .olControlDrawFeaturePathItemActive { background-position: -51px -24px; } .olControlEditingToolbar .olControlDrawFeaturePolygonItemInactive{ background-position: -26px -1px; } .olControlEditingToolbar .olControlDrawFeaturePolygonItemActive { background-position: -26px -24px; } div.olControlZoom { position: absolute; top: 8px; left: 8px; background: rgba(255,255,255,0.4); border-radius: 4px; padding: 2px; } div.olControlZoom a { display: block; margin: 1px; padding: 0; color: white; font-size: 18px; font-family: 'Lucida Grande', Verdana, Geneva, Lucida, Arial, Helvetica, sans-serif; font-weight: bold; text-decoration: none; text-align: center; height: 22px; width:22px; line-height: 19px; background: #130085; /* fallback for IE - IE6 requires background shorthand*/ background: rgba(0, 60, 136, 0.5); filter: alpha(opacity=80); } div.olControlZoom a:hover { background: #130085; /* fallback for IE */ background: rgba(0, 60, 136, 0.7); filter: alpha(opacity=100); } @media only screen and (max-width: 600px) { div.olControlZoom a:hover { background: rgba(0, 60, 136, 0.5); } } a.olControlZoomIn { border-radius: 4px 4px 0 0; } a.olControlZoomOut { border-radius: 0 0 4px 4px; } /** * Animations */ .olLayerGrid .olTileImage { -webkit-transition: opacity 0.2s linear; -moz-transition: opacity 0.2s linear; -o-transition: opacity 0.2s linear; transition: opacity 0.2s linear; } PKvBb+tilecloud_chain/layer-switcher-maximize.pngPNG  IHDRVΎWgAMA asRGB cHRMz&u0`:pQ<tEXtSoftwarewww.inkscape.org<IDAT8c?:f` O?Af?#J#.k ~B!0.ibbS2w89mc$`q*Ǐ}?#c^jeFtAeY헃ZAK0 H'0 Ÿ?Caᨆa1dA %2 0Ax4\*6uPB$ gqt̴T-F(-#n|MIENDB`PKirGݨ++tilecloud_chain/cost.py# -*- coding: utf-8 -*- import sys import logging from datetime import timedelta from argparse import ArgumentParser from tilecloud import Tile, TileStore, consume from tilecloud_chain import TileGeneration, add_comon_options from tilecloud_chain.format import duration_format logger = logging.getLogger(__name__) def main(): parser = ArgumentParser( description='Used to calculate the generation cost', prog=sys.argv[0] ) add_comon_options(parser, tile_pyramid=False) parser.add_argument( '--cost-algo', '--calculate-cost-algorithm', default='area', dest='cost_algo', choices=('area', 'count'), help="The algorithm use to calculate the cost default base on the 'area' " "of the generation geometry, can also be 'count', to be base on number of tiles to generate." ) options = parser.parse_args() gene = TileGeneration(options.config, options, layer_name=options.layer) all_size = 0 tile_size = 0 all_tiles = 0 if (options.layer): (all_size, all_time, all_price, all_tiles) = _calculate_cost(gene, options) tile_size = gene.layer['cost']['tile_size'] / (1024.0 * 1024) else: all_time = timedelta() all_price = 0 for layer in gene.config['generation']['default_layers']: print("") print("===== %s =====" % layer) gene.set_layer(layer, options) (size, time, price, tiles) = _calculate_cost(gene, options) tile_size += gene.layer['cost']['tile_size'] / (1024.0 * 1024) all_time += time all_price += price all_size += size all_tiles += tiles print("") print("===== GLOBAL =====") print("Total number of tiles: %i" % all_tiles) print('Total generation time: %s [d h:mm:ss]' % (duration_format(all_time))) print('Total generation cost: %0.2f [$]' % all_price) print("") print('S3 Storage: %0.2f [$/month]' % (all_size * gene.config['cost']['s3']['storage'] / (1024.0 * 1024 * 1024))) print('S3 get: %0.2f [$/month]' % ( gene.config['cost']['s3']['get'] * gene.config['cost']['request_per_layers'] / 10000.0 + gene.config['cost']['s3']['download'] * gene.config['cost']['request_per_layers'] * tile_size) ) # if 'cloudfront' in gene.config['cost']: # print('CloudFront: %0.2f [$/month]' % () # gene.config['cost']['cloudfront']['get'] * gene.config['cost']['request_per_layers'] / 10000.0 + # gene.config['cost']['cloudfront']['download'] * gene.config['cost']['request_per_layers'] * tile_size) if 'ec2' in gene.config: print('ESB storage: %0.2f [$/month]' % ( gene.config['cost']['esb']['storage'] * gene.config['cost']['esb_size']) ) sys.exit(0) def validate_calculate_cost(gene): error = False name = "layer[%s]" % gene.layer['name'] error = gene.validate(gene.layer, name, 'cost', attribute_type=dict, default={}) or error error = gene.validate( gene.layer['cost'], name + '.cost', 'tileonly_generation_time', attribute_type=float, default=40.0 ) or error error = gene.validate( gene.layer['cost'], name + '.cost', 'tile_generation_time', attribute_type=float, default=30.0 ) or error error = gene.validate( gene.layer['cost'], name + '.cost', 'metatile_generation_time', attribute_type=float, default=30.0 ) or error error = gene.validate( gene.layer['cost'], name + '.cost', 'tile_size', attribute_type=float, default=20.0 ) or error error = gene.validate(gene.config, 'config', 'cost', attribute_type=dict, default={}) or error error = gene.validate( gene.config['cost'], 'cost', 'request_per_layers', attribute_type=int, default=10000000 ) or error error = gene.validate(gene.config['cost'], 'cost', 'esb_size', attribute_type=int, default=100) or error # http://aws.amazon.com/s3/pricing/ error = gene.validate(gene.config['cost'], 'cost', 's3', attribute_type=dict, default={}) or error # [$/Go/month] error = gene.validate(gene.config['cost']['s3'], 'cost.s3', 'storage', attribute_type=float, default=0.125) or error # [$/put/1000] error = gene.validate(gene.config['cost']['s3'], 'cost.s3', 'put', attribute_type=float, default=0.01) or error # [$/get/10000] error = gene.validate(gene.config['cost']['s3'], 'cost.s3', 'get', attribute_type=float, default=0.01) or error # [$/Go] error = gene.validate(gene.config['cost']['s3'], 'cost.s3', 'download', attribute_type=float, default=0.12) or error # http://aws.amazon.com/cloudfront/pricing/ error = gene.validate(gene.config['cost'], 'cost', 'cloudfront', attribute_type=dict, default={}) or error # [$/get/10000] error = gene.validate( gene.config['cost']['cloudfront'], 'cost.cloudfront', 'get', attribute_type=float, default=0.009 ) or error # [$/Go] error = gene.validate( gene.config['cost']['cloudfront'], 'cost.cloudfront', 'download', attribute_type=float, default=0.12 ) or error # http://aws.amazon.com/ec2/pricing/ error = gene.validate(gene.config['cost'], 'cost', 'ec2', attribute_type=dict, default={}) or error # [$/hour] ec2cost = { 't1.micro': 0.02, 'm1.small': 0.085, 'm1.medium': 0.17, 'm1.large': 0.34, 'm1.xlarge': 0.68, 'm2.xlarge': 0.506, 'm2.2xlarge': 1.012, 'm2.4xlarge': 2.024, 'c1.medium': 0.186, 'c1.xlarge': 0.744, 'cc1.4xlarge': 1.3, # usa-est-1 'cc1.8xlarge': 2.7, 'cg1.4xlarge': 2.36, 'hi1.4xlarge': 3.41, } error = gene.validate( gene.config['cost']['ec2'], 'cost.ec2', 'usage', attribute_type=float, default=ec2cost[gene.config['ec2']['host_type']] if 'ec2' in gene.config else -1, ) or error # http://aws.amazon.com/ebs/ error = gene.validate(gene.config['cost'], 'cost', 'esb', attribute_type=dict, default={}) or error # [$/1Go/month] error = gene.validate( gene.config['cost']['esb'], 'cost.esb', 'storage', attribute_type=float, default=0.11 ) or error # [$/ 1000 E/S/s /month] error = gene.validate(gene.config['cost']['esb'], 'cost.esb', 'io', attribute_type=float, default=260.0) or error # http://aws.amazon.com/sqs/pricing/ error = gene.validate(gene.config['cost'], 'cost', 'sqs', attribute_type=dict, default={}) or error # [$/10000] error = gene.validate( gene.config['cost']['sqs'], 'cost.sqs', 'request', attribute_type=float, default=0.01 ) or error if error: exit(1) # pragma: no cover def _calculate_cost(gene, options): validate_calculate_cost(gene) nb_metatiles = {} nb_tiles = {} meta = gene.layer['meta'] if options.cost_algo == 'area': tile_size = gene.layer['grid_ref']['tile_size'] for zoom, resolution in enumerate(gene.layer['grid_ref']['resolutions']): if 'min_resolution_seed' in gene.layer and resolution < gene.layer['min_resolution_seed']: continue print("Calculate zoom %i." % zoom) px_buffer = gene.layer['px_buffer'] + \ gene.layer['meta_buffer'] if meta else 0 m_buffer = px_buffer * resolution if meta: size = tile_size * gene.layer['meta_size'] * resolution meta_buffer = size * 0.7 + m_buffer meta_geom = gene.geoms[zoom].buffer(meta_buffer, 1) nb_metatiles[zoom] = int(round(meta_geom.area / size ** 2)) size = tile_size * resolution tile_buffer = size * 0.7 + m_buffer geom = gene.geoms[zoom].buffer(tile_buffer, 1) nb_tiles[zoom] = int(round(geom.area / size ** 2)) elif options.cost_algo == 'count': gene.init_tilecoords() gene.add_geom_filter() if meta: def count_metatile(tile): if tile: if tile.tilecoord.z in nb_metatiles: nb_metatiles[tile.tilecoord.z] += 1 else: nb_metatiles[tile.tilecoord.z] = 1 return tile gene.imap(count_metatile) class MetaTileSplitter(TileStore): def get(self, tiles): for metatile in tiles: for tilecoord in metatile.tilecoord: yield Tile(tilecoord) gene.tilestream = MetaTileSplitter().get(gene.tilestream) # Only keep tiles that intersect geometry gene.add_geom_filter() def count_tile(tile): if tile: if tile.tilecoord.z in nb_tiles: nb_tiles[tile.tilecoord.z] += 1 else: print("Calculate zoom %i." % tile.tilecoord.z) nb_tiles[tile.tilecoord.z] = 1 return tile gene.imap(count_tile) consume(gene.tilestream, None) times = {} print for z in nb_metatiles: print("%i meta tiles in zoom %i." % (nb_metatiles[z], z)) times[z] = gene.layer['cost']['metatile_generation_time'] * nb_metatiles[z] price = 0 all_size = 0 all_time = 0 all_tiles = 0 for z in nb_tiles: print print("%i tiles in zoom %i." % (nb_tiles[z], z)) all_tiles += nb_tiles[z] if meta: time = times[z] + gene.layer['cost']['tile_generation_time'] * nb_tiles[z] else: time = gene.layer['cost']['tileonly_generation_time'] * nb_tiles[z] size = gene.layer['cost']['tile_size'] * nb_tiles[z] all_size += size all_time += time td = timedelta(milliseconds=time) print("Time to generate: %s [d h:mm:ss]" % (duration_format(td))) c = gene.config['cost']['s3']['put'] * nb_tiles[z] / 1000.0 price += c print('S3 PUT: %0.2f [$]' % c) if 'ec2' in gene.config: c = time * gene.config['cost']['ec2']['usage'] / (1000.0 * 3600) price += c print('EC2 usage: %0.2f [$]' % c) c = gene.config['cost']['esb']['io'] * time / (1000.0 * 2600 * 24 * 30) price += c print('ESB usage: %0.2f [$]' % c) if 'sqs' in gene.layer: if meta: nb_sqs = nb_metatiles[z] * 3 else: nb_sqs = nb_tiles[z] * 3 c = nb_sqs * gene.config['cost']['sqs']['request'] / 1000000.0 price += c print('SQS usage: %0.2f [$]' % c) print("") td = timedelta(milliseconds=all_time) print("Number of tiles: %i" % all_tiles) print('Generation time: %s [d h:mm:ss]' % (duration_format(td))) print('Generation cost: %0.2f [$]' % price) return (all_size, td, price, all_tiles) PKwH1W:D:Dtilecloud_chain/generate.py# -*- coding: utf-8 -*- import os import re import sys import logging import socket import random from datetime import datetime from getpass import getuser from argparse import ArgumentParser import boto from boto import sns from tilecloud import TileCoord from tilecloud.store.url import URLTileStore from tilecloud.store.sqs import SQSTileStore from tilecloud.layout.wms import WMSTileLayout from tilecloud.filter.logger import Logger from tilecloud_chain import TileGeneration, HashDropper, HashLogger, DropEmpty, TilesFileStore, \ add_comon_options, parse_tilecoord, quote, Count from tilecloud_chain.format import size_format, duration_format logger = logging.getLogger(__name__) class Generate: _re_rm_xml_tag = re.compile('(<[^>]*>|\n)') def gene(self, options, gene, layer): count_metatiles = None count_metatiles_dropped = Count() count_tiles = None count_tiles_dropped = Count() if options.role == 'slave' or options.get_hash or options.get_bbox: gene.layer = gene.layers[layer] else: gene.set_layer(layer, options) if options.get_bbox: try: tilecoord = parse_tilecoord(options.get_bbox) print( "Tile bounds: [%i,%i,%i,%i]" % gene.layer['grid_ref']['obj'].extent(tilecoord) ) exit() except ValueError as e: # pragma: no cover exit( "Tile '%s' is not in the format 'z/x/y' or z/x/y:+n/+n\n%r" % (options.get_bbox, e) ) if options.get_hash: options.role = 'hash' options.test = 1 sqs_tilestore = None if options.role in ('master', 'slave'): # Create SQS queue sqs_tilestore = SQSTileStore(gene.get_sqs_queue()) # pragma: no cover cache_tilestore = None if options.role in ('local', 'slave'): cache_tilestore = gene.get_tilesstore(options.cache) meta = gene.layer['meta'] if options.tiles: gene.set_store(TilesFileStore(options.tiles)) elif options.role in ('local', 'master'): # Generate a stream of metatiles gene.init_tilecoords() gene.add_geom_filter() if options.local_process_number is not None: # pragma: no cover gene.add_local_process_filter() elif options.role == 'slave': # Get the metatiles from the SQS queue gene.set_store(sqs_tilestore) # pragma: no cover elif options.role == 'hash': try: z, x, y = (int(v) for v in options.get_hash.split('/')) if meta: gene.set_tilecoords([TileCoord(z, x, y, gene.layer['meta_size'])]) else: gene.set_tilecoords([TileCoord(z, x, y)]) except ValueError as e: # pragma: no cover exit( "Tile '%s' is not in the format 'z/x/y'\n%r" % (options.get_hash, e) ) # At this stage, the tilestream contains metatiles that intersect geometry gene.add_logger() count_metatiles = gene.counter() if options.role == 'master': # pragma: no cover # Put the metatiles into the SQS queue gene.put(sqs_tilestore) elif options.role in ('local', 'slave', 'hash'): if gene.layer['type'] == 'wms': params = gene.layer['params'].copy() if 'STYLES' not in params: params['STYLES'] = ','.join(gene.layer['wmts_style'] for l in gene.layer['layers']) if gene.layer['generate_salt']: params['SALT'] = str(random.randint(0, sys.maxint)) for dim in gene.layer['dimensions']: params[dim['name']] = dim['value'] for dim in gene.options.dimensions: dim = dim.split('=') if len(dim) != 2: # pragma: no cover exit( 'the DIMENTIONS option should be like this ' 'DATE=2013 VERSION=13.' ) params[dim[0]] = dim[1] # Get the metatile image from the WMS server gene.get(URLTileStore( tilelayouts=(WMSTileLayout( url=gene.layer['url'], layers=','.join(gene.layer['layers']), srs=gene.layer['grid_ref']['srs'], format=gene.layer['mime_type'], border=gene.layer['meta_buffer'] if meta else 0, tilegrid=gene.get_grid()['obj'], params=params, ),), headers=gene.layer['headers'], ), "Get tile from WMS") elif gene.layer['type'] == 'mapnik': from tilecloud.store.mapnik_ import MapnikTileStore from tilecloud_chain.mapnik_ import MapnikDropActionTileStore grid = gene.get_grid() if gene.layer['output_format'] == 'grid': gene.get(MapnikDropActionTileStore( tilegrid=grid['obj'], mapfile=gene.layer['mapfile'], image_buffer=gene.layer['meta_buffer'] if meta else 0, data_buffer=gene.layer['data_buffer'], output_format=gene.layer['output_format'], resolution=gene.layer['resolution'], layers_fields=gene.layer['layers_fields'], drop_empty_utfgrid=gene.layer['drop_empty_utfgrid'], store=cache_tilestore, queue_store=sqs_tilestore, count=count_tiles_dropped, proj4_literal=grid['proj4_literal'], ), "Create Mapnik grid tile") else: gene.get(MapnikTileStore( tilegrid=grid['obj'], mapfile=gene.layer['mapfile'], image_buffer=gene.layer['meta_buffer'] if meta else 0, data_buffer=gene.layer['data_buffer'], output_format=gene.layer['output_format'], proj4_literal=grid['proj4_literal'], ), "Create Mapnik tile") def wrong_content_type_to_error(tile): if tile is not None and tile.content_type is not None \ and tile.content_type.find("image/") != 0: if tile.content_type.find("application/vnd.ogc.se_xml") == 0: tile.error = "WMS server error: %s" % ( self._re_rm_xml_tag.sub('', tile.data) ) else: # pragma: no cover tile.error = "%s is not an image format, error: %s" % ( tile.content_type, tile.data ) return tile gene.imap(wrong_content_type_to_error) # Handle errors gene.add_error_filters() if meta: if options.role == 'hash': gene.imap(HashLogger('empty_metatile_detection')) elif not options.near: # Discard tiles with certain content if 'empty_metatile_detection' in gene.layer: empty_tile = gene.layer['empty_metatile_detection'] gene.imap(HashDropper( empty_tile['size'], empty_tile['hash'], store=cache_tilestore, queue_store=sqs_tilestore, count=count_metatiles_dropped, )) def add_elapsed_togenerate(metatile): if metatile is not None: metatile.elapsed_togenerate = metatile.tilecoord.n ** 2 return True return False # pragma: no cover gene.ifilter(add_elapsed_togenerate) # Split the metatile image into individual tiles gene.add_metatile_splitter() gene.imap(Logger(logger, logging.INFO, '%(tilecoord)s')) # Handle errors gene.add_error_filters() self.count_tiles = gene.counter() if 'pre_hash_post_process' in gene.layer: gene.process(gene.layer['pre_hash_post_process']) if options.role == 'hash': gene.imap(HashLogger('empty_tile_detection')) elif not options.near: # Discard tiles with certain content if 'empty_tile_detection' in gene.layer: empty_tile = gene.layer['empty_tile_detection'] gene.imap(HashDropper( empty_tile['size'], empty_tile['hash'], store=cache_tilestore, queue_store=sqs_tilestore, count=count_tiles_dropped, )) gene.process() if options.role in ('local', 'slave'): gene.add_error_filters() gene.ifilter(DropEmpty(gene)) count_tiles = gene.counter(size=True) if options.time: def log_size(tile): sys.stdout.write('size: %i\n' % len(tile.data)) return tile gene.imap(log_size) gene.put(cache_tilestore, "Store the tile") else: count_tiles = gene.counter(size=True) gene.add_error_filters() if options.generated_tiles_file: # pragma: no cover generated_tiles_file = open(options.generated_tiles_file, 'a') def do(tile): generated_tiles_file.write('%s\n' % (tile.tilecoord, )) return tile gene.imap(do) if options.role == 'slave': # pragma: no cover if meta: def decr_tile_in_metatile(tile): tile.metatile.elapsed_togenerate -= 1 if tile.metatile.elapsed_togenerate == 0: sqs_tilestore.delete_one(tile.metatile) return True gene.ifilter(decr_tile_in_metatile) else: gene.delete(sqs_tilestore) if options.time is not None: class LogTime: n = 0 t1 = None def __call__(self, tile): self.n += 1 if self.n == options.time: self.t1 = datetime.now() elif self.n == 2 * options.time: t2 = datetime.now() d = (t2 - self.t1) / options.time sys.stdout.write('time: %i\n' % ((d.days * 24 * 3600 + d.seconds) * 1000000 + d.microseconds)) return tile gene.imap(LogTime()) gene.consume(options.time * 3) else: gene.consume() if not options.quiet and options.role in ('local', 'slave'): nb_tiles = count_tiles.nb + count_tiles_dropped.nb print( """The tile generation of layer '%s' is finish %sNb generated tiles: %i Nb tiles dropped: %i Nb tiles stored: %i Nb error: %i Total time: %s Total size: %s Time per tiles: %i ms Size per tile: %i o """ % ( gene.layer['name'], """Nb generated metatiles: %i Nb metatiles dropped: %i """ % ( count_metatiles.nb, count_metatiles_dropped.nb ) if meta else '', nb_tiles, count_tiles_dropped.nb, count_tiles.nb, gene.error, duration_format(gene.duration), size_format(count_tiles.size), (gene.duration / nb_tiles * 1000).seconds if nb_tiles != 0 else 0, count_tiles.size / count_tiles.nb if count_tiles.nb != 0 else -1 ) ) if cache_tilestore is not None and hasattr(cache_tilestore, 'connection'): cache_tilestore.connection.close() if options.role != 'hash' and options.time is None and 'sns' in gene.config: # pragma: no cover if 'region' in gene.config['sns']: connection = sns.connect_to_region(gene.config['sns']['region']) else: connection = boto.connect_sns() connection.publish( gene.config['sns']['topic'], """The tile generation is finish Layer: %(layer)s Role: %(role)s Host: %(host)s Command: %(cmd)s %(meta)sNb generated tiles: %(nb_tiles)i Nb tiles dropped: %(nb_tiles_dropped)i Total time: %(duration)s [s] Time per tiles: %(tile_duration)i [ms]""" % { 'role': options.role, 'layer': gene.layer['name'], 'host': socket.getfqdn(), 'cmd': ' '.join([quote(arg) for arg in sys.argv]), 'meta': """Nb generated metatiles: %(nb_metatiles)i Nb metatiles dropped: %(nb_metatiles_dropped)i """ % { 'nb_metatiles': count_metatiles.nb, 'nb_metatiles_dropped': count_metatiles_dropped.nb, } if meta else '', 'nb_tiles': nb_tiles if meta else count_metatiles.nb, 'nb_tiles_dropped': count_tiles_dropped.nb if meta else count_metatiles_dropped.nb, 'duration': duration_format(gene.duration), 'tile_duration': (gene.duration / nb_tiles * 1000).seconds if nb_tiles != 0 else 0, }, "Tile generation (%(layer)s - %(role)s)" % { 'role': options.role, 'layer': gene.layer['name'] } ) def daemonize(): # pragma: no cover try: pid = os.fork() if pid > 0: print("Daemonize with pid %i." % pid) sys.stderr.write(str(pid)) # exit parent sys.exit(0) except OSError as e: exit("fork #1 failed: %d (%s)\n" % (e.errno, e.strerror)) def main(): parser = ArgumentParser(description='Used to generate the tiles', prog=sys.argv[0]) add_comon_options(parser, dimensions=True) parser.add_argument( '--get-hash', metavar="TILE", help='get the empty tiles hash, use the specified TILE z/x/y' ) parser.add_argument( '--get-bbox', metavar="TILE", help='get the bbox of a tile, use the specified TILE z/x/y, or z/x/y:+n/+n for metatiles' ) parser.add_argument( '--role', default='local', choices=('local', 'master', 'slave'), help='local/master/slave, master to file the queue and ' 'slave to generate the tiles' ) parser.add_argument( "--local-process-number", default=None, help="The number of process that we run in parallel" ) parser.add_argument( '--daemonize', default=False, action="store_true", help='run as a daemon' ) parser.add_argument( '--tiles', metavar="FILE", help='Generate the tiles from a tiles file, use the format z/x/y, or z/x/y:+n/+n for metatiles' ) parser.add_argument( '--generated-tiles-file', metavar="FILE", help='Store the tiles in a file (unrecommended)' ) options = parser.parse_args() if options.daemonize: daemonize() # pragma: no cover gene = TileGeneration(options.config, options) if options.get_hash is None and options.get_bbox is None and \ 'authorised_user' in gene.config['generation'] and \ gene.config['generation']['authorised_user'] != getuser(): exit('not authorised, authorised user is: %s.' % gene.config['generation']['authorised_user']) if options.cache is None: options.cache = gene.config['generation']['default_cache'] if options.tiles is not None and options.role not in ['local', 'master']: # pragma: no cover exit("The --tiles option work only with role local or master") try: if (options.layer): generate = Generate() generate.gene(options, gene, options.layer) elif options.get_bbox: # pragma: no cover exit("With --get-bbox option we needs to specify a layer") elif options.get_hash: # pragma: no cover exit("With --get-hash option we needs to specify a layer") elif options.tiles: # pragma: no cover exit("With --tiles option we needs to specify a layer") else: for layer in gene.config['generation']['default_layers']: generate = Generate() generate.gene(options, gene, layer) finally: if gene.error_file is not None: gene.error_file.close() PKwIdCf[[tilecloud_chain/format.py# -*- coding: utf-8 -*- def size_format(number): for unit in ['o', 'Kio', 'Mio', 'Gio', 'Tio']: if number < 1024.0: if number < 10: return "%.1f %s" % (number, unit) else: return "%.0f %s" % (number, unit) number /= 1024.0 def duration_format(duration): hours, remainder = divmod(duration.seconds, 3600) minutes, seconds = divmod(remainder, 60) if duration.days > 0: return '%i %i:%02i:%02i' % (duration.days, hours, minutes, seconds) else: return '%i:%02i:%02i' % (hours, minutes, seconds) PKwH챃AKAKtilecloud_chain/controller.py# -*- coding: utf-8 -*- import os import sys import math import logging import yaml from six.moves import cStringIO from math import exp, log from copy import copy from argparse import ArgumentParser from hashlib import sha1 from urllib import urlencode import requests from bottle import jinja2_template from PIL import Image from tilecloud.lib.s3 import S3Connection from tilecloud.lib.PIL_ import FORMAT_BY_CONTENT_TYPE from tilecloud_chain import TileGeneration, add_comon_options, get_tile_matrix_identifier from tilecloud_chain.cost import validate_calculate_cost logger = logging.getLogger(__name__) def main(): parser = ArgumentParser( description='Used to generate the contextual file like the capabilities, the legends, ' 'the Apache and MapCache configuration', prog=sys.argv[0] ) add_comon_options(parser, tile_pyramid=False, no_geom=False) parser.add_argument( '--capabilities', '--generate-wmts-capabilities', default=False, action='store_true', help='Generate the WMTS Capabilities' ) parser.add_argument( '--legends', '--generate-legend-images', default=False, action='store_true', dest='legends', help='Generate the legend images' ) parser.add_argument( '--openlayers', '--generate-openlayers-test-page', default=False, action='store_true', dest='openlayers', help='Generate openlayers test page' ) parser.add_argument( '--mapcache', '--generate-mapcache-config', default=False, action='store_true', dest='mapcache', help='Generate MapCache configuration file' ) parser.add_argument( '--apache', '--generate-apache-config', default=False, action='store_true', dest='apache', help='Generate Apache configuration file' ) parser.add_argument( '--dump-config', default=False, action='store_true', help='Dump the used config with default values and exit' ) options = parser.parse_args() gene = TileGeneration(options.config, options, layer_name=options.layer) if options.cache is None: options.cache = gene.config['generation']['default_cache'] if options.dump_config: for layer in gene.config['layers'].keys(): gene.set_layer(layer, options) validate_calculate_cost(gene) _validate_generate_wmts_capabilities(gene, gene.caches[options.cache]) gene.validate_mapcache_config() gene.validate_apache_config() _validate_generate_openlayers(gene) for grid in gene.config['grids'].values(): if 'obj' in grid: del grid['obj'] print(yaml.dump(gene.config)) sys.exit(0) if options.legends: _generate_legend_images(gene) if options.capabilities: _generate_wmts_capabilities(gene) if options.mapcache: _generate_mapcache_config(gene) if options.apache: _generate_apache_config(gene) if options.openlayers: _generate_openlayers(gene) def _send(data, path, mime_type, cache): if cache['type'] == 's3': # pragma: no cover s3bucket = S3Connection().bucket(cache['bucket']) s3key = s3bucket.key(os.path.join('%(folder)s' % cache, path)) s3key.body = data s3key['Content-Encoding'] = 'utf-8' s3key['Content-Type'] = mime_type s3key.put() else: folder = cache['folder'] or '' filename = os.path.join(folder, path) directory = os.path.dirname(filename) if not os.path.exists(directory): os.makedirs(directory) f = open(filename, 'wb') f.write(data) f.close() def _get(path, cache): if cache['type'] == 's3': # pragma: no cover s3bucket = S3Connection().bucket(cache['bucket']) s3key = s3bucket.key(os.path.join('%(folder)s' % cache, path)) return s3key.get().body else: p = os.path.join(cache['folder'], path) if not os.path.isfile(p): # pragma: no cover return None with open(p, 'rb') as file: return file.read() def _validate_generate_wmts_capabilities(gene, cache): error = False error = gene.validate(cache, 'cache[%s]' % cache['name'], 'http_url', attribute_type=str, default=False) or error error = gene.validate(cache, 'cache[%s]' % cache['name'], 'http_urls', attribute_type=list, default=False) or error error = gene.validate(cache, 'cache[%s]' % cache['name'], 'hosts', attribute_type=list, default=False) or error if not cache['http_url'] and not cache['http_urls']: # pragma: no cover logger.error( "The attribute 'http_url' or 'http_urls' is required in the object %s." % ('cache[%s]' % cache['name']) ) error = True if cache['http_url'] and cache['http_url'][-1] == '/': # pragma: no cover logger.error( "The attribute 'http_url' shouldn't ends with a '/' in the object %s." % ('cache[%s]' % cache['name']) ) error = True elif cache['http_urls']: for url in cache['http_urls']: if url[-1] == '/': # pragma: no cover logger.error( "The element '%s' of the attribute 'http_urls' shouldn't ends" " with a '/' in the object %s." % ('cache[%s]' % cache['name']) ) error = True if error: # pragma: no cover exit(1) def _generate_wmts_capabilities(gene): from tilecloud_chain.wmts_get_capabilities_template import wmts_get_capabilities_template cache = gene.caches[gene.options.cache] _validate_generate_wmts_capabilities(gene, cache) server = 'server' in gene.config base_urls = [] if cache['http_url']: if cache['hosts']: cc = copy(cache) for host in cache['hosts']: cc['host'] = host base_urls.append(cache['http_url'] % cc) else: base_urls = [cache['http_url'] % cache] if cache['http_urls']: base_urls = [url % cache for url in cache['http_urls']] base_urls = [url + '/' if url[-1] != '/' else url for url in base_urls] for layer in gene.layers.values(): previous_legend = None previous_resolution = None if 'legend_mime' in layer and 'legend_extention' in layer and 'legends' not in layer: layer['legends'] = [] for zoom, resolution in enumerate(layer['grid_ref']['resolutions']): path = '/'.join(['1.0.0', layer['name'], layer['wmts_style'], 'legend%s.%s' % ( zoom, layer['legend_extention'] )]) img = _get(path, cache) if img is not None: new_legend = { 'mime_type': layer['legend_mime'], 'href': os.path.join(base_urls[0], 'static/' if server else '', path), } layer['legends'].append(new_legend) if previous_legend is not None: middle_res = exp((log(previous_resolution) + log(resolution)) / 2) previous_legend['min_resolution'] = middle_res new_legend['max_resolution'] = middle_res try: pil_img = Image.open(cStringIO(img)) new_legend['width'] = pil_img.size[0] new_legend['height'] = pil_img.size[1] except: # pragma: nocover logger.warn("Unable to read legend image '%s', with '%r'" % (path, img)) previous_legend = new_legend previous_resolution = resolution capabilities = jinja2_template( wmts_get_capabilities_template, layers=gene.layers, grids=gene.grids, getcapabilities=base_urls[0] + ( 'wmts/1.0.0/WMTSCapabilities.xml' if server else cache['wmtscapabilities_file']), base_urls=base_urls, base_url_postfix='wmts/' if server else '', get_tile_matrix_identifier=get_tile_matrix_identifier, server=server, enumerate=enumerate, ceil=math.ceil, int=int ) _send(capabilities, cache['wmtscapabilities_file'], 'application/xml', cache) def _generate_legend_images(gene): cache = gene.caches[gene.options.cache] for layer in gene.layers.values(): if 'legend_mime' in layer and 'legend_extention' in layer: if layer['type'] == 'wms': session = requests.session() session.headers.update(layer['headers']) previous_hash = None for zoom, resolution in enumerate(layer['grid_ref']['resolutions']): legends = [] for l in layer['layers']: response = session.get(layer['url'] + '?' + urlencode({ 'SERVICE': 'WMS', 'VERSION': '1.1.1', 'REQUEST': 'GetLegendGraphic', 'LAYER': l, 'FORMAT': layer['legend_mime'], 'TRANSPARENT': 'TRUE' if layer['legend_mime'] == 'image/png' else 'FALSE', 'STYLE': layer['wmts_style'], 'SCALE': resolution / 0.00028 })) try: legends.append(Image.open(cStringIO(response.content))) except: # pragma: nocover logger.warn( "Unable to read legend image for layer '%s', resolution '%i': %r" % ( layer['name'], resolution, response.content ) ) width = max(i.size[0] for i in legends) height = sum(i.size[1] for i in legends) image = Image.new('RGBA', (width, height)) y = 0 for i in legends: image.paste(i, (0, y)) y += i.size[1] string_io = cStringIO() image.save(string_io, FORMAT_BY_CONTENT_TYPE[layer['legend_mime']]) result = string_io.getvalue() new_hash = sha1(result).hexdigest() if new_hash != previous_hash: previous_hash = new_hash _send( result, '1.0.0/%s/%s/legend%s.%s' % ( layer['name'], layer['wmts_style'], zoom, layer['legend_extention'] ), layer['legend_mime'], cache ) def _generate_mapcache_config(gene): from tilecloud_chain.mapcache_config_template import mapcache_config_template if not gene.validate_mapcache_config(): exit(1) # pragma: no cover for layer in gene.layers.values(): if layer['type'] == 'wms' or 'wms_url' in layer: if 'FORMAT' not in layer['params']: layer['params']['FORMAT'] = layer['mime_type'] if 'LAYERS' not in layer['params']: layer['params']['LAYERS'] = ','.join(layer['layers']) if 'TRANSPARENT' not in layer['params']: layer['params']['TRANSPARENT'] = 'TRUE' if layer['mime_type'] == 'image/png' else 'FALSE' config = jinja2_template( mapcache_config_template, layers=gene.layers, grids=gene.grids, mapcache=gene.config['mapcache'], min=min, len=len ) f = open(gene.config['mapcache']['config_file'], 'w') f.write(config) f.close() def _generate_apache_config(gene): if not gene.validate_apache_config(): exit(1) # pragma: no cover cache = gene.caches[gene.options.cache] use_server = 'server' in gene.config f = open(gene.config['apache']['config_file'], 'w') folder = cache['folder'] if folder and folder[-1] != '/': folder += '/' if not use_server: f.write(""" ExpiresActive on ExpiresDefault "now plus %(expires)i hours" %(headers)s """ % { 'location': gene.config['apache']['location'], 'expires': gene.config['apache']['expires'], 'headers': ''.join([ ' Header set %s "%s"' % h for h in gene.config['apache'].get('headers', { 'Cache-Control': 'max-age=864000, public' }).items() ]), }) if cache['type'] == 's3': tiles_url = cache['tiles_url'] if 'tiles_url' in cache else \ 'http://s3-%(region)s.amazonaws.com/%(bucket)s/%(folder)s' % { 'region': cache['region'], 'bucket': cache['bucket'], 'folder': folder } f.write( """ Order deny,allow Allow from all ProxyPass %(location)s/ %(tiles_url)s ProxyPassReverse %(location)s/ %(tiles_url)s """ % { 'location': gene.config['apache']['location'], 'tiles_url': tiles_url, } ) elif cache['type'] == 'filesystem': f.write( """ Alias %(location)s %(files_folder)s """ % { 'location': gene.config['apache']['location'], 'files_folder': folder, 'headers': ''.join([ " Header set %s '%s'" % h for h in gene.config['apache'].get('headers', { 'Cache-Control': 'max-age=864000, public' }).items() ]), } ) use_mapcache = 'mapcache' in gene.config if use_mapcache: if not gene.validate_mapcache_config(): exit(1) # pragma: no cover if use_mapcache and not use_server: token_regex = '([a-zA-Z0-9_\-\+~\.]+)' f.write('\n') for l in gene.config['layers']: layer = gene.config['layers'][l] if 'min_resolution_seed' in layer: res = [r for r in layer['grid_ref']['resolutions'] if r < layer['min_resolution_seed']] dim = len(layer['dimensions']) for r in res: f.write( 'RewriteRule' ' ' '^%(tiles_location)s/1.0.0/%(layer)s/%(token_regex)s' # Baseurl/layer/Style '%(dimensions_re)s' # Dimensions : variable number of values '/%(token_regex)s/%(zoom)s/(.*)$' # TileMatrixSet/TileMatrix/TileRow/TileCol.extension ' ' '%(mapcache_location)s/wmts/1.0.0/%(layer)s/$1' '%(dimensions_rep)s' '/$%(tilematrixset)s/%(zoom)s/$%(final)s' ' ' '[PT]\n' % { 'tiles_location': gene.config['apache']['location'], 'mapcache_location': gene.config['mapcache']['location'], 'layer': layer['name'], 'token_regex': token_regex, 'dimensions_re': ''.join(['/' + token_regex for e in range(dim)]), 'dimensions_rep': ''.join(['/$%i' % (e + 2) for e in range(dim)]), 'tilematrixset': dim + 2, 'final': dim + 3, 'zoom': layer['grid_ref']['resolutions'].index(r) } ) if use_mapcache: f.write(""" MapCacheAlias %(mapcache_location)s "%(mapcache_config)s" """ % { 'mapcache_location': gene.config['mapcache']['location'], 'mapcache_config': os.path.abspath(gene.config['mapcache']['config_file']) }) f.close() def _validate_generate_openlayers(gene): error = False error = gene.validate(gene.config, 'config', 'openlayers', attribute_type=dict, default={}) or error error = gene.validate( gene.config['openlayers'], 'openlayers', 'srs', attribute_type=str, default='EPSG:21781' ) or error error = gene.validate( gene.config['openlayers'], 'openlayers', 'center_x', attribute_type=float, default=600000 ) or error error = gene.validate( gene.config['openlayers'], 'openlayers', 'center_y', attribute_type=float, default=200000 ) or error if error: exit(1) # pragma: no cover def _get_resource(ressource): path = os.path.join(os.path.dirname(__file__), ressource) f = open(path) data = f.read() f.close() return data def _generate_openlayers(gene): from tilecloud_chain.openlayers_html import openlayers_html from tilecloud_chain.openlayers_js import openlayers_js _validate_generate_openlayers(gene) cache = gene.caches[gene.options.cache] http_url = '' if 'http_url' in cache and cache['http_url']: if 'hosts' in cache and cache['hosts']: cc = copy(cache) cc['host'] = cache['hosts'][0] http_url = cache['http_url'] % cc else: http_url = cache['http_url'] % cache if 'http_urls' in cache and cache['http_urls']: http_url = cache['http_urls'][0] % cache if http_url and http_url[-1] != '/': http_url += '/' js = jinja2_template( openlayers_js, srs=gene.config['openlayers']['srs'], center_x=gene.config['openlayers']['center_x'], center_y=gene.config['openlayers']['center_y'], http_url=http_url + ('wmts/' if 'server' in gene.config else ''), layers=[ { 'name': name, 'grid': layer['type'] == 'mapnik' and layer['output_format'] == 'grid', 'maxExtent': layer['grid_ref']['bbox'], 'resolution': layer['resolution'] if layer['type'] == 'mapnik' and layer['output_format'] == 'grid' else None, } for name, layer in gene.layers.items() if layer['grid_ref']['srs'] == gene.config['openlayers']['srs'] ] ) _send(openlayers_html, 'index.html', 'text/html', cache) _send(js, 'wmts.js', 'application/javascript', cache) _send(_get_resource('OpenLayers.js'), 'OpenLayers.js', 'application/javascript', cache) _send(_get_resource('OpenLayers-style.css'), 'theme/default/style.css', 'text/css', cache) _send(_get_resource('layer-switcher-maximize.png'), 'img/layer-switcher-maximize.png', 'image/png', cache) _send(_get_resource('layer-switcher-minimize.png'), 'img/layer-switcher-minimize.png', 'image/png', cache) PKvB"+tilecloud_chain/layer-switcher-minimize.pngPNG  IHDRVΎWsBIT|dtEXtSoftwarewww.inkscape.org<nIDAT8c?5#- z$ $3 . Bˁ96 bP $Š @̏fG ' 5 >lhbS :DL;8#]ݵ20HIENDB`PKwIdCTSTTtilecloud_chain/mapnik_.py# -*- coding: utf-8 -*- import logging from tilecloud import Tile from tilecloud.store.mapnik_ import MapnikTileStore logger = logging.getLogger(__name__) class MapnikDropActionTileStore(MapnikTileStore): def __init__(self, store=None, queue_store=None, count=None, **kwargs): self.store = store self.queue_store = queue_store self.count = count MapnikTileStore.__init__(self, **kwargs) def get_one(self, tile): result = MapnikTileStore.get_one(self, tile) if result is None: if self.store is not None: if tile.tilecoord.n != 1: # pragma: no cover for tilecoord in tile.tilecoord: self.store.delete_one(Tile(tilecoord)) else: self.store.delete_one(tile) logger.info("The tile %s is dropped" % str(tile.tilecoord)) if hasattr(tile, 'metatile'): # pragma: no cover tile.metatile.elapsed_togenerate -= 1 if tile.metatile.elapsed_togenerate == 0 and self.queue_store is not None: self.queue_store.delete_one(tile.metatile) elif self.queue_store is not None: # pragma: no cover self.queue_store.delete_one(tile) if self.count: self.count() return result PKwH2`<<tilecloud_chain/amazon.py# -*- coding: utf-8 -*- import os import sys import logging import boto import re import socket import subprocess from os import path, environ from time import sleep from threading import Thread from boto import sns from datetime import timedelta from subprocess import Popen, PIPE from argparse import ArgumentParser from six.moves import reduce from tilecloud_chain import TileGeneration, add_comon_options, quote logger = logging.getLogger(__name__) def _get_path(): directory = path.dirname(sys.argv[0]) if len(directory) != 0: # pragma: no cover directory += '/' return directory def main(): parser = ArgumentParser( description='Used to generate the tiles from Amazon EC2, ' 'and get the SQS queue status', prog=sys.argv[0] ) add_comon_options(parser) parser.add_argument( '--deploy-config', default=None, dest="deploy_config", metavar="FILE", help='path to the deploy configuration file' ) parser.add_argument( '--status', default=False, action="store_true", help='display the SQS queue status and exit' ) parser.add_argument( '--disable-geodata', default=True, action="store_false", dest="geodata", help='disable geodata synchronisation' ) parser.add_argument( '--disable-code', default=True, action="store_false", dest="deploy_code", help='disable deploy application code' ) parser.add_argument( '--disable-database', default=True, action="store_false", dest="deploy_database", help='disable deploy database' ) parser.add_argument( '--disable-fillqueue', default=True, action="store_false", dest="fill_queue", help='disable queue filling' ) parser.add_argument( '--disable-tilesgen', default=True, action="store_false", dest="tiles_gen", help='disable tile generation' ) parser.add_argument( '--host', default=None, help='The host used to generate tiles' ) parser.add_argument( '--shutdown', default=False, action="store_true", help='Shut done the remote host after the task.' ) parser.add_argument( '--wait', default=False, action="store_true", help='Wait that all the tasks will finish.' ) parser.add_argument( '--local', default=False, action="store_true", help='Run the generation locally' ) options = parser.parse_args() gene = TileGeneration(options.config, options, layer_name=options.layer) if options.status: # pragma: no cover status(options, gene) sys.exit(0) if 'ec2' not in gene.config: # pragma: no cover print("EC2 not configured") sys.exit(1) if options.deploy_config is None: options.deploy_config = gene.config['ec2']['deploy_config'] if options.geodata: options.geodata = not gene.config['ec2']['disable_geodata'] if options.deploy_code: options.deploy_code = not gene.config['ec2']['disable_code'] if options.deploy_database: options.deploy_database = not gene.config['ec2']['disable_database'] if options.fill_queue: # pragma: no cover options.fill_queue = not gene.config['ec2']['disable_fillqueue'] if options.tiles_gen: # pragma: no cover options.tiles_gen = not gene.config['ec2']['disable_tilesgen'] # start aws if not options.host: # TODO not implemented yet host = aws_start(gene.config['ec2']['host_type']) # pragma: no cover else: host = options.host if not options.local and options.geodata and 'geodata_folder' in gene.config['ec2']: print("==== Sync geodata ====") ssh_options = '' if 'ssh_options' in gene.config['ec2']: # pragma: no cover ssh_options = gene.config['ec2']['ssh_options'] # sync geodata run_local([ 'rsync', '--delete', '-e', 'ssh ' + ssh_options, '-r', gene.config['ec2']['geodata_folder'], host + ':' + gene.config['ec2']['geodata_folder'] ]) if options.deploy_code and not options.local: print("==== Sync and build code ====") error = gene.validate(gene.config['ec2'], 'ec2', 'code_folder', required=True) if error: exit(1) # pragma: no cover cmd = ['rsync', '--delete', ] if 'ssh_options' in gene.config['ec2']: # pragma: no cover cmd += ['-e', 'ssh ' + gene.config['ec2']['ssh_options']] ssh_options = gene.config['ec2']['ssh_options'] project_dir = gene.config['ec2']['code_folder'] cmd += ['-r', '.', host + ':' + project_dir] run_local(cmd) for cmd in gene.config['ec2']['build_cmds']: run(options, cmd % environ, host, project_dir, gene) if 'apache_content' in gene.config['ec2'] and 'apache_config' in gene.config['ec2']: run( options, 'echo %s > %s' % ( gene.config['ec2']['apache_content'], gene.config['ec2']['apache_config'] ), host, project_dir, gene ) run(options, 'sudo apache2ctl graceful', host, project_dir, gene) # deploy if options.deploy_database and not options.local: _deploy(gene, host) if options.deploy_code or options.deploy_database \ or options.geodata and not options.local: # TODO not implemented yet create_snapshot(host, gene) if options.time: arguments = _get_arguments(options) arguments.extend(['--role', 'local']) arguments.extend(['--time', str(options.time)]) project_dir = None if options.local else gene.config['ec2']['code_folder'] processes = [] for i in range(gene.config['ec2']['number_process']): processes.append( run_remote_process( "%sgenerate_tiles %s" % ( _get_path(), ' '.join([str(a) for a in arguments]) ), host, project_dir, gene ) ) tiles_size = [] times = [] for p in processes: results = p.communicate() if results[1] != '': # pragma: no cover logger.debug('ERROR: %s' % results[1]) results = (re.sub(u'\n[^\n]*\r', u'\n', results[0]), ) results = (re.sub(u'^[^\n]*\r', u'', results[0]), ) for r in results[0].split('\n'): if r.startswith('time: '): times.append(int(r.replace('time: ', ''))) elif r.startswith('size: '): tiles_size.append(int(r.replace('size: ', ''))) if len(times) == 0: # pragma: no cover logger.error("Not enough data") sys.exit(1) mean_time = reduce( lambda x, y: x + y, [timedelta(microseconds=int(r)) for r in times], timedelta() ) / len(times) ** 2 mean_time_ms = mean_time.seconds * 1000 + mean_time.microseconds / 1000.0 mean_size = reduce(lambda x, y: x + y, [int(r) for r in tiles_size], 0) / len(tiles_size) mean_size_kb = mean_size / 1024.0 print('==== Time results ====') print('A tile is generated in: %0.3f [ms]' % mean_time_ms) print('Then mean generated tile size: %0.3f [kb]' % (mean_size_kb)) print('''config: cost: tileonly_generation_time: %0.3f tile_generation_time: %0.3f metatile_generation_time: 0 tile_size: %0.3f''' % (mean_time_ms, mean_time_ms, mean_size_kb)) if options.shutdown: # pragma: no cover run(options, 'sudo shutdown 0', host, project_dir, gene) sys.exit(0) if options.fill_queue and not options.local: # pragma: no cover print("==== Till queue ====") # TODO test arguments = _get_arguments(options) arguments.extend(['--role', 'master', '--quiet']) project_dir = gene.config['ec2']['code_folder'] run_remote_process( options, "%sgenerate_tiles %s" % ( _get_path(), ' '.join([str(a) for a in arguments]) ), host, project_dir, gene ) sleep(5) attributes = gene.get_sqs_queue().get_attributes() print( "\rTiles to generate: %s/%s" % ( attributes['ApproximateNumberOfMessages'], attributes['ApproximateNumberOfMessagesNotVisible'], ) ) if options.tiles_gen: # pragma: no cover print("==== Generate tiles ====") if options.wait and not options.local: print("") class Status(Thread): def run(self): # pragma: no cover while True: attributes = gene.get_sqs_queue().get_attributes() print( "\rTiles to generate/generating: %s/%s" % ( attributes['ApproximateNumberOfMessages'], attributes['ApproximateNumberOfMessagesNotVisible'], ) ) sleep(1) status_thread = Status() status_thread.setDaemon(True) status_thread.start() arguments = _get_arguments(options) arguments.extend(['--quiet']) if not options.local: arguments.extend(['--role', 'slave']) project_dir = None if options.local else gene.config['ec2']['code_folder'] threads = [] for i in range(gene.config['ec2']['number_process']): if options.local: threads.append(run_local_process( "%sgenerate_tiles --local-process-number %i %s" % ( _get_path(), i, ' '.join([str(a) for a in arguments]) ) )) else: run_remote_process( "%sgenerate_tiles %s" % ( _get_path(), ' '.join([str(a) for a in arguments]) ), host, project_dir, gene ) print('Tile generation started') if options.shutdown: run(options, 'sudo shutdown 0') if options.wait and options.local: while len(threads) > 0: threads = [t for t in threads if t.is_alive()] sleep(1) if 'sns' in gene.config: if 'region' in gene.config['sns']: connection = sns.connect_to_region(gene.config['sns']['region']) else: connection = boto.connect_sns() connection.publish( gene.config['sns']['topic'], """The tile generation is finish Host: %(host)s Command: %(cmd)s""" % { 'host': socket.getfqdn(), 'cmd': ' '.join([quote(arg) for arg in sys.argv]) }, "Tile generation controller" ) def _deploy(gene, host): print("==== Deploy database ====") deploy_cmd = 'deploy' if 'deploy_user' in gene.config['ec2']: deploy_cmd = 'sudo -u %s deploy' % gene.config['ec2']['deploy_user'] index = host.find('@') if index >= 0: # pragma: no cover host = host[index + 1:] run_local( '%s --remote --components=[databases] %s %s' % (deploy_cmd, gene.options.deploy_config, host) ) def _get_arguments(options): arguments = [ "--config", options.config, ] if options.cache is not None: arguments.extend(["--destination-cache", options.cache]) if options.layer is not None: arguments.extend(["--layer", options.layer]) if options.near is not None: arguments.append("--near") arguments.extend(options.near) elif options.bbox is not None: arguments.append("--bbox") arguments.extend(options.bbox) if options.zoom is not None: arguments.extend(["--zoom", ','.join([str(z) for z in options.zoom])]) if options.test is not None: arguments.extend(["--test", str(options.test)]) if not options.geom: arguments.append("--no-geom") return arguments def create_snapshot(host, gene): pass # TODO def aws_start(host_type): # pragma: no cover pass # TODO def run_local(cmd): if type(cmd) != list: cmd = cmd.split(' ') logger.debug('Run: %s.' % ' '.join([quote(c) for c in cmd])) result = Popen(cmd, stdout=PIPE, stderr=PIPE).communicate() if len(result[0]) != 0: # pragma: no cover logger.info(result[0]) if len(result[1]) != 0: logger.error(result[1]) return result def run_local_process(cmd): if type(cmd) != list: cmd = cmd.split(' ') logger.debug('Run: %s.' % ' '.join([quote(c) for c in cmd])) task = Run(cmd) task.start() return task def run_remote_process(remote_cmd, host, project_dir, gene): cmd = ['ssh'] if 'ssh_options' in gene.config['ec2']: # pragma: no cover cmd.extend(gene.config['ec2']['ssh_options'].split(' ')) if host is None: # pragma: no cover exit('host option is required.') cmd.append(host) env = '' if os.getenv('AWS_ACCESS_KEY_ID') and os.getenv('AWS_SECRET_ACCESS_KEY'): # pragma: no cover env = 'export AWS_ACCESS_KEY_ID=%(access_key)s;export AWS_SECRET_ACCESS_KEY=%(secret_key)s;' % { 'access_key': os.getenv('AWS_ACCESS_KEY_ID'), 'secret_key': os.getenv('AWS_SECRET_ACCESS_KEY'), } cmd.append( 'cd %(project_dir)s;' '%(env)s' '%(cmd)s' % { 'cmd': remote_cmd, 'env': env, 'project_dir': project_dir } ) logger.debug('Run: %s.' % ' '.join([quote(c) for c in cmd])) return Popen(cmd, stdout=PIPE, stderr=PIPE) class Run(Thread): def __init__(self, cmd): Thread.__init__(self) if type(cmd) != list: # pragma: no cover cmd = cmd.split(' ') self.cmd = cmd def run(self): subprocess.call(self.cmd) def run(options, cmd, host, project_dir, gene): if options.local: # pragma: no cover if type(cmd) != list: cmd = cmd.split(' ') subprocess.call(cmd) else: result = run_remote_process(cmd, host, project_dir, gene).communicate() if len(result[0]) != 0: logger.info(result[0]) if len(result[1]) != 0: # pragma: no cover logger.error(result[1]) def run_remote(remote_cmd, host, project_dir, gene): # pragma: no cover result = run_remote_process(remote_cmd, host, project_dir, gene).communicate() if len(result[0]) != 0: logger.info(result[0]) if len(result[1]) != 0: logger.error(result[1]) return result def status(options, gene): # pragma: no cover # get SQS status attributes = gene.get_sqs_queue().get_attributes() print( """Approximate number of tiles to generate: %s Approximate number of generating tiles: %s Last modification in tile queue: %s""" % ( attributes['ApproximateNumberOfMessages'], attributes['ApproximateNumberOfMessagesNotVisible'], attributes['LastModifiedTimestamp'] ) ) PKirGПDDtilecloud_chain/copy_.py# -*- coding: utf-8 -*- import sys import logging from argparse import ArgumentParser from tilecloud_chain import TileGeneration, DropEmpty, \ HashDropper, Count, add_comon_options from tilecloud_chain.format import size_format, duration_format logger = logging.getLogger(__name__) class Copy: count = None def copy(self, options, gene, layer, source, dest, task_name): # disable metatiles gene.layers[layer]['meta'] = False count_tiles_dropped = Count() gene.set_layer(layer, options) source_tilestore = gene.get_tilesstore(source) dest_tilestore = gene.get_tilesstore(dest) gene.init_tilecoords() gene.add_geom_filter() gene.add_logger() gene.get(source_tilestore, "Get the tiles") gene.ifilter(DropEmpty(gene)) # Discard tiles with certain content if 'empty_tile_detection' in gene.layer: empty_tile = gene.layer['empty_tile_detection'] gene.imap(HashDropper( empty_tile['size'], empty_tile['hash'], store=dest_tilestore, count=count_tiles_dropped, )) if options.process: gene.process(options.process) gene.ifilter(DropEmpty(gene)) self.count = gene.counter(size=True) gene.put(dest_tilestore, "Store the tiles") gene.add_error_filters() gene.consume() if not options.quiet: print( """The tile %s of layer '%s' is finish Nb %s tiles: %i Nb errored tiles: %i Nb dropped tiles: %i Total time: %s Total size: %s Time per tiles: %i ms Size per tile: %i o """ % ( task_name, gene.layer['name'], task_name, self.count.nb, count_tiles_dropped.nb, gene.error, duration_format(gene.duration), size_format(self.count.size), (gene.duration / self.count.nb * 1000).seconds if self.count.nb != 0 else 0, self.count.size / self.count.nb if self.count.nb != 0 else -1 ) ) def main(): parser = ArgumentParser( description='Used to copy the tiles from a cache to an other', prog=sys.argv[0] ) add_comon_options(parser, near=False, time=False, dimensions=True, cache=False) parser.add_argument( '--process', dest='process', metavar="NAME", help='The process name to do' ) parser.add_argument( 'source', metavar="SOURCE", help='The source cache' ) parser.add_argument( 'dest', metavar="DEST", help='The destination cache' ) options = parser.parse_args() gene = TileGeneration(options.config, options) if (options.layer): # pragma: no cover copy = Copy() copy.copy(options, gene, options.layer, options.source, options.dest, 'copy') else: for layer in gene.config['generation']['default_layers']: copy = Copy() copy.copy(options, gene, layer, options.source, options.dest, 'copy') def process(): parser = ArgumentParser( description='Used to copy the tiles from a cache to an other', prog=sys.argv[0] ) add_comon_options(parser, near=False, time=False, dimensions=True) parser.add_argument( 'process', metavar="PROCESS", help='The process name to do' ) options = parser.parse_args() gene = TileGeneration(options.config, options) if (options.layer): # pragma: no cover copy = Copy() copy.copy(options, gene, options.layer, options.cache, options.cache, 'process') else: for layer in gene.config['generation']['default_layers']: copy = Copy() copy.copy(options, gene, layer, options.cache, options.cache, 'process') PKwHyb7PPtilecloud_chain/server.py# -*- coding: utf-8 -*- # Copyright (c) 2013 by Stéphane Brunner # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # 3. Neither the name of Camptocamp nor the names of its contributors may # be used to endorse or promote products derived from this software # without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import os import sys import logging import requests import types import datetime import mimetypes from urllib import urlencode from urlparse import parse_qs from tilecloud import Tile, TileCoord from tilecloud.lib.s3 import S3Connection from tilecloud_chain import TileGeneration if sys.version_info.major >= 3: buffer = memoryview # pragma: no cover else: memoryview = buffer logger = logging.getLogger(__name__) class Server: def __init__(self, config_file): self.filters = {} self.max_zoom_seed = {} logger.info("Config file: '%s'" % config_file) self.tilegeneration = TileGeneration(config_file) if not self.tilegeneration.validate_apache_config(): # pragma: no cover raise "Apache configuration error" self.expires_hours = self.tilegeneration.config['apache']['expires'] self.static_allow_extension = self.tilegeneration.config['server']['static_allow_extension'] \ if 'static_allow_extension' in self.tilegeneration.config['server'] \ else ['jpeg', 'png', 'xml', 'js', 'html', 'css'] self.cache = self.tilegeneration.caches[ self.tilegeneration.config['server']['cache'] if 'cache' in self.tilegeneration.config['server'] else self.tilegeneration.config['generation']['default_cache'] ] if self.cache['type'] == 's3': # pragma: no cover s3bucket = S3Connection().bucket(self.cache['bucket']) def _get(self, path, **kwargs): global s3bucket try: s3key = s3bucket.key(os.path.join('%(folder)s' % self.cache, path)) responce = s3key.get() return responce.body, responce.headers['Content-Type'] except: s3bucket = S3Connection().bucket(self.cache['bucket']) s3key = s3bucket.key(os.path.join('%(folder)s' % self.cache, path)) responce = s3key.get() return responce.body, responce.headers['Content-Type'] else: folder = self.cache['folder'] or '' def _get(self, path, **kwargs): if path.split('.')[-1] not in self.static_allow_extension: # pragma: no cover return self.error(403, "Extension not allowed", **kwargs), None p = os.path.join(folder, path) if not os.path.isfile(p): # pragma: no cover return self.error(404, path + " not found", **kwargs), None with open(p, 'rb') as file: data = file.read() mime = mimetypes.guess_type(p) return data, mime[0] # get capabilities or other static files self._get = types.MethodType(_get, self) if not self.tilegeneration.validate_mapcache_config(): # pragma: no cover raise "Mapcache configuration error" mapcache_base = self.tilegeneration.config['server']['mapcache_base'] if \ 'mapcache_base' in self.tilegeneration.config['server'] else \ 'http://localhost/' self.mapcache_baseurl = mapcache_base + self.tilegeneration.config['mapcache']['location'] + '/wmts' self.mapcache_header = self.tilegeneration.config['server']['mapcache_headers'] if \ 'mapcache_headers' in self.tilegeneration.config['server'] else {} geoms_redirect = bool(self.tilegeneration.config['server']['geoms_redirect']) if \ 'geoms_redirect' in self.tilegeneration.config['server'] else False self.layers = self.tilegeneration.config['server']['layers'] if \ 'layers' in self.tilegeneration.config['server'] else \ self.tilegeneration.layers.keys() self.stores = {} for layer_name in self.layers: layer = self.tilegeneration.layers[layer_name] # build geoms redirect if geoms_redirect: self.filters[layer_name] = self.tilegeneration.get_geoms_filter( layer=layer, grid=layer['grid_ref'], geoms=self.tilegeneration.get_geoms( layer, extent=layer['bbox'] if 'bbox' in layer else layer['grid_ref']['bbox'], ), ) if 'min_resolution_seed' in layer: max_zoom_seed = -1 for zoom, resolution in enumerate(layer['grid_ref']['resolutions']): if resolution > layer['min_resolution_seed']: max_zoom_seed = zoom self.max_zoom_seed[layer_name] = max_zoom_seed else: self.max_zoom_seed[layer_name] = sys.maxint # build stores store_defs = [{ 'ref': [layer_name], 'dimensions': [], }] for dimension in layer['dimensions']: new_store_defs = [] for store_def in store_defs: for value in dimension['values']: new_store_defs.append({ 'ref': store_def['ref'] + [value], 'dimensions': store_def['dimensions'] + [(dimension['name'], value)], }) store_defs = new_store_defs for store_def in store_defs: self.stores['/'.join(store_def['ref'])] = \ self.tilegeneration.get_store(self.cache, layer, store_def['dimensions'], read_only=True) def __call__(self, environ, start_response): params = {} for key, value in parse_qs(environ['QUERY_STRING'], True).items(): params[key.upper()] = value[0] path = None if len(params) > 0 else environ['PATH_INFO'][1:].split('/') return self.serve(path, params, start_response=start_response) def serve(self, path, params, **kwargs): dimensions = [] if path is not None: if len(path) >= 1 and path[0] == 'static': body, mime = self._get('/'.join(path[1:]), **kwargs) if mime is not None: return self.responce(body, { 'Content-Type': mime, 'Expires': ( datetime.datetime.utcnow() + datetime.timedelta(hours=self.expires_hours) ).isoformat(), 'Cache-Control': "max-age=%i" % (3600 * self.expires_hours), 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET', }, **kwargs) else: # pragma: no cover return body elif len(path) >= 1 and path[0] != 'wmts': # pragma: no cover return self.error( 404, "Type '%s' don't exists, allows values: 'wmts' or 'static'" % path[0], **kwargs ) path = path[1:] # remove type if len(path) == 2 and path[0] == '1.0.0' and path[1].lower() == 'wmtscapabilities.xml': params['SERVICE'] = 'WMTS' params['VERSION'] = '1.0.0' params['REQUEST'] = 'GetCapabilities' elif len(path) < 7: return self.error(400, "Not enough path", **kwargs) else: params['SERVICE'] = 'WMTS' params['VERSION'] = path[0] params['LAYER'] = path[1] params['STYLE'] = path[2] if params['LAYER'] in self.layers: layer = self.tilegeneration.layers[params['LAYER']] else: return self.error(400, "Wrong Layer '%s'" % params['LAYER'], **kwargs) index = 3 dimensions = path[index:index + len(layer['dimensions'])] for dimension in layer['dimensions']: params[dimension['name'].upper()] = path[index] index += 1 last = path[-1].split('.') if len(path) < index + 4: # pragma: no cover return self.error(400, "Not enough path", **kwargs) params['TILEMATRIXSET'] = path[index] params['TILEMATRIX'] = path[index + 1] params['TILEROW'] = path[index + 2] if len(path) == index + 4: params['REQUEST'] = 'GetTile' params['TILECOL'] = last[0] if last[1] != layer['extension']: # pragma: no cover return self.error(400, "Wrong extention '%s'" % last[1], **kwargs) elif len(path) == index + 6: params['REQUEST'] = 'GetFeatureInfo' params['TILECOL'] = path[index + 3] params['I'] = path[index + 4] params['J'] = last[0] params['INFO_FORMAT'] = layer['info_formats'][0] else: # pragma: no cover return self.error(400, "Wrong path length", **kwargs) params['FORMAT'] = layer['mime_type'] else: if \ 'SERVICE' not in params or \ 'REQUEST' not in params or \ 'VERSION' not in params: return self.error(400, "Not all required parameters are present", **kwargs) if params['SERVICE'] != 'WMTS': return self.error(400, "Wrong Service '%s'" % params['SERVICE'], **kwargs) if params['VERSION'] != '1.0.0': return self.error(400, "Wrong Version '%s'" % params['VERSION'], **kwargs) if params['REQUEST'] == 'GetCapabilities': wmtscapabilities_file = self.cache['wmtscapabilities_file'] body, mime = self._get(wmtscapabilities_file, **kwargs) if mime is not None: return self.responce(body, headers={ 'Content-Type': "application/xml", 'Expires': ( datetime.datetime.utcnow() + datetime.timedelta(hours=self.expires_hours) ).isoformat(), 'Cache-Control': "max-age=%i" % (3600 * self.expires_hours), 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET', }, **kwargs) else: # pragma: no cover return body if \ 'FORMAT' not in params or \ 'LAYER' not in params or \ 'TILEMATRIXSET' not in params or \ 'TILEMATRIX' not in params or \ 'TILEROW' not in params or \ 'TILECOL' not in params: # pragma: no cover return self.error(400, "Not all required parameters are present", **kwargs) if path is None: if params['LAYER'] in self.layers: layer = self.tilegeneration.layers[params['LAYER']] else: return self.error(400, "Wrong Layer '%s'" % params['LAYER'], **kwargs) for dimension in layer['dimensions']: dimensions.append( params[dimension['name'].upper()] if dimension['name'].lower() in params else dimension['default'] ) if params['STYLE'] != layer['wmts_style']: return self.error(400, "Wrong Style '%s'" % params['STYLE'], **kwargs) if params['TILEMATRIXSET'] != layer['grid']: return self.error(400, "Wrong TileMatrixSet '%s'" % params['TILEMATRIXSET'], **kwargs) tile = Tile(TileCoord( # TODO fix for matrix_identifier = resolution int(params['TILEMATRIX']), int(params['TILECOL']), int(params['TILEROW']), )) if params['REQUEST'] == 'GetFeatureInfo': if \ 'I' not in params or \ 'J' not in params or \ 'INFO_FORMAT' not in params: # pragma: no cover return self.error(400, "Not all required parameters are present", **kwargs) if 'query_layers' in layer: return self.forward( layer['url'] + '?' + urlencode({ 'SERVICE': 'WMS', 'VERSION': '1.1.1', 'REQUEST': 'GetFeatureInfo', 'LAYERS': ','.join(layer['layers']), 'QUERY_LAYERS': ','.join(layer['query_layers']), 'STYLES': params['STYLE'], 'FORMAT': params['FORMAT'], 'INFO_FORMAT': params['INFO_FORMAT'], 'WIDTH': layer['grid_ref']['tile_size'], 'HEIGHT': layer['grid_ref']['tile_size'], 'SRS': layer['grid_ref']['srs'], 'BBOX': layer['grid_ref']['obj'].extent(tile.tilecoord), 'X': params['I'], 'Y': params['J'], }), no_cache=True, **kwargs ) else: # pragma: no cover return self.error(400, "Layer '%s' not queryable" % layer['name'], **kwargs) if params['REQUEST'] != 'GetTile': return self.error(400, "Wrong Request '%s'" % params['REQUEST'], **kwargs) if params['FORMAT'] != layer['mime_type']: return self.error(400, "Wrong Format '%s'" % params['FORMAT'], **kwargs) if tile.tilecoord.z > self.max_zoom_seed[layer['name']]: # pragma: no cover return self.forward( self.mapcache_baseurl + '?' + urlencode(params), headers=self.mapcache_header, **kwargs ) if layer['name'] in self.filters: layer_filter = self.filters[layer['name']] meta_size = layer['meta_size'] meta_tilecoord = TileCoord( # TODO fix for matrix_identifier = resolution tile.tilecoord.z, tile.tilecoord.x / meta_size * meta_size, tile.tilecoord.y / meta_size * meta_size, meta_size, ) if meta_size != 1 else tile.tilecoord if not layer_filter.filter_tilecoord(meta_tilecoord): # pragma: no cover return self.forward( self.mapcache_baseurl + '?' + urlencode(params), headers=self.mapcache_header, **kwargs ) store_ref = '/'.join([params['LAYER']] + dimensions) if store_ref in self.stores: # pragma: no cover store = self.stores[store_ref] else: # pragma: no cover return self.error( 400, "No store found for layer '%s' and dimensions %s" % ( layer['name'], ', '.join(dimensions) ), **kwargs ) tile = store.get_one(tile) if tile: return self.responce(tile.data, headers={ 'Content-Type': tile.content_type, 'Expires': ( datetime.datetime.utcnow() + datetime.timedelta(hours=self.expires_hours) ).isoformat(), 'Cache-Control': "max-age=%i" % (3600 * self.expires_hours), 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET', }, **kwargs) else: return self.error(204, **kwargs) def forward(self, url, headers={}, no_cache=False, **kwargs): if no_cache: headers['Cache-Control'] = 'no-cache' headers['Pragma'] = 'no-cache' responce = requests.get(url, headers=headers) if responce.status_code == 200: responce_headers = responce.headers.copy() if no_cache: responce_headers['Cache-Control'] = 'no-cache, no-store' responce_headers['Pragma'] = 'no-cache' else: # pragma: no cover responce_headers['Expires'] = ( datetime.datetime.utcnow() + datetime.timedelta(hours=self.expires_hours) ).isoformat() responce_headers['Cache-Control'] = "max-age=%i" % (3600 * self.expires_hours) responce_headers['Access-Control-Allow-Origin'] = '*' responce_headers['Access-Control-Allow-Methods'] = 'GET' responce_headers return self.responce(responce.content, headers=responce_headers, **kwargs) else: # pragma: no cover message = "The URL '%s' return '%i %s', content:\n%s" % ( url, responce.status_code, responce.reason, responce.text, ) logger.warning(message) return self.error(502, message=message, **kwargs) HTTP_MESSAGES = { 204: '204 No Content', 400: '400 Bad Request', 403: '403 Forbidden', 404: '404 Not Found', 502: '502 Bad Gateway', } def error(self, code, message='', start_response=None): start_response(self.HTTP_MESSAGES[code], []) return [message] def responce(self, data, headers={}, start_response=None): headers['Content-Length'] = str(len(data)) start_response('200 OK', headers.items()) return [data] def app_factory(global_config, configfile='tilegeneration/config.yaml', **local_conf): return Server(configfile) class PyramidView(Server): from pyramid.httpexceptions import HTTPNoContent, HTTPBadRequest, \ HTTPForbidden, HTTPNotFound, HTTPBadGateway HTTP_EXCEPTIONS = { 204: HTTPNoContent, 400: HTTPBadRequest, 403: HTTPForbidden, 404: HTTPNotFound, 502: HTTPBadGateway, } def __init__(self, request): self.request = request Server.__init__(self, request.registry.settings['tilegeneration_configfile']) def __call__(self): params = {} path = None if 'path' in self.request.matchdict: path = self.request.matchdict['path'] else: for param, value in self.request.params.items(): params[param.upper()] = value return self.serve(path, params) def error(self, code, message=''): raise self.HTTP_EXCEPTIONS[code](message) def responce(self, data, headers={}): self.request.response.headers = headers if type(data) == memoryview: self.request.response.body_file = data else: self.request.response.body = data return self.request.response PKDEB!tilecloud_chain/views/__init__.pyPKirGTT%tilecloud_chain/scaffolds/__init__.pyfrom pyramid.scaffolds.template import Template # pragma: no cover class Create(Template): # pragma: no cover _template_dir = 'create' summary = 'Template used to create a standalone TileCloud-chain project' def post(self, *args, **kargs): super(Create, self).post(*args, **kargs) print(""" Welcome to TileCloud chain. =========================== By default this scaffold use variable from Puppet facter. """) class Ec2(Template): # pragma: no cover _template_dir = 'ec2' summary = 'Template used to complete TileCloud-chain project with ec2 support' PKwIdC' >tilecloud_chain/scaffolds/create/tilegeneration/+dot+gitignoreconfig.yaml PKwHF;Etilecloud_chain/scaffolds/create/tilegeneration/config.yaml.mako_tmplgrids: # grid name, I just recommends to add the min resolution because it's common to not generate all the layers at the same resolution. swissgrid_05: # resolutions [required] resolutions: [1000, 500, 250, 100, 50, 20, 10, 5, 2, 1, 0.5] # bbox [required] bbox: [420000, 30000, 900000, 350000] # srs [required] srs: epsg:21781 caches: local: type: filesystem folder: /var/sig/tiles wmtscapabilities_file: ${wmtscapabilities_path} # for GetCapabilities http_url: https://%(host)s/tiles hosts: - wmts0. - wmts1. - wmts2. - wmts3. - wmts4. s3: type: s3 bucket: tiles folder: '' # for GetCapabilities http_url: https://%(host)s/%(bucket)s/%(folder)s hosts: - wmts0. # this defines some defaults values for all the layers layer_default: type: wms grid: swissgrid_05 # The minimum resolution to seed, useful to use with mapcache, optional. # min_resolution_seed: 1 # the URL of the WMS server to used url: http://localhost/${instanceid}/mapserv # Set the headers to get the right virtual host, and don't get any cached result headers: Host: ${host} Cache-Control: no-cache, no-store Pragma: no-cache # file name extension extension: png # the bbox there we want to generate tiles #bbox: [493000, 114000, 586000, 204000] # mime type used for the WMS request and the WMTS capabilities generation mime_type: image/png wmts_style: default # the WMTS dimensions definition [default to []] #dimensions: # - name: DATE # # the default value for the WMTS capabilities # default: 2012 # # the generated value # value: 2012 # # all the available values in the WMTS capabilities # values: [2012] # the meta tiles definition [default to off] meta: on # the meta tiles size [default to 8] meta_size: 8 # the meta tiles buffer [default to 128] meta_buffer: 128 # connexion an sql to get geometries (in column named geom) where we want to generate tiles # Warn: too complex result can slow down the application # connection: user=www-data password=www-data dbname= host=localhost # geoms: # - sql: AS geom FROM # size and hash used to detect empty tiles and metatiles [optional, default to None] empty_metatile_detection: size: 740 hash: 3237839c217b51b8a9644d596982f342f8041546 empty_tile_detection: size: 921 hash: 1e3da153be87a493c4c71198366485f290cad43c layers: plan: layers: plan ortho: layers: ortho extension: jpeg mime_type: image/jpeg # no buffer needed on rater sources meta_buffer: 0 empty_metatile_detection: size: 66163 hash: a9d16a1794586ef92129a2fb41a739451ed09914 empty_tile_detection: size: 1651 hash: 2892fea0a474228f5d66a534b0b5231d923696da generation: default_cache: local # used to allowed only a specific user to generate tiles (for rights issue) authorised_user: www-data # maximum allowed consecutive errors, after it exit [default to 10] maxconsecutive_errors: 10 apache: location: /${instanceid}/tiles mapcache: config_file: apache/mapcache.xml memcache_host: localhost memcache_port: 11211 process: optipng_test: - cmd: optipng -o7 -simulate %(in)s optipng: - cmd: optipng %(args)s -q -zc9 -zm8 -zs3 -f5 %(in)s arg: default: '-q' quiet: '-q' jpegoptim: - cmd: jpegoptim %(args)s --strip-all --all-normal -m 90 %(in)s arg: default: '-q' quiet: '-q' openlayers: # srs, center_x, center_y [required] srs: epsg:21781 center_x: 600000 center_y: 200000 PKwHTjÕ{l{l/tilecloud_chain-1.1.0.dist-info/DESCRIPTION.rstTileCloud Chain =============== .. image:: https://secure.travis-ci.org/sbrunner/tilecloud-chain.svg?branch=master .. image:: https://coveralls.io/repos/sbrunner/tilecloud-chain/badge.png?branch=master The goal of TileCloud Chain is to provide tools around tile generation on a chain like: Source: WMS, Mapnik. Optionally using an SQS queue, AWS host, SNS topic. Destination in WMTS layout, on S3, on Berkley DB (``bsddb``), on MBTiles, or on local filesystem. Features: - Generate tiles. - Drop empty tiles. - Drop tiles outside a geometry or a bbox. - Use MetaTiles - Generate GetCapabilities. - Generate OpenLayers example page. - Obtain the hash of an empty tile - In future, measure tile generation speed - Calculate cost and generation time. - In future, manage the AWS hosts that generate tiles. - Delete empty tiles. .. contents:: Table of contents ------ Get it ------ Requirements:: pg_config and a build environment. Install:: mkdir .build virtualenv .build/venv .build/venv/bin/pip install tilecloud-chain .build/venv/bin/pcreate -s tilecloud_chain . Edit your layers configuration in ``./tilegeneration/config.yaml``. `Default configuration file `_. --------- Configure --------- Configure grids --------------- The ``grid`` describes how the tiles are arranged. Especially on ``s3`` be careful to choose every of the grid settings before generating the tiles. If you change one of them you must regenerate all the tiles. The ``resolutions`` in [px/m] describes all the resolutions available for this layer. For a raster layer, have a look to the maximum resolution of the source files. It is not needed to generate tiles at smaller resolutions than the sources, it is preferable to use the OpenLayers client zoom. Note that you can add a resolution in the end without regenerating all the tiles. The ``bbox`` should match the resolution of the extent. **CAREFUL: you will have big issue if you use this parameter to generate the tile on a restricted area**: use the ``bbox`` on the layer instead. The ``srs`` specifies the code of the projection. The ``unit`` is the unit used by the projection. The ``tile_size`` is the tile size in [px], defaults to 256. The ``matrix_identifier`` is ``zoom`` by default and can also be set to ``resolution``. It specifies how the z index is build to store the tiles, for example, for the resolutions ``[2, 1, 0.5]`` the used values are ``[0, 1, 2]`` based on the zoom and ``[2, 1, 0_5]`` based on the resolution. The second has the advantage of allowing to add a new resolution without regenerating all the tiles, but it does not work with MapCache. Configure caches ---------------- The available tile caches are: ``s3``, ``bsddb``, ``mbtile`` and ``filesystem``. The best solution to store the tiles, ``s3``, ``mbtiles`` and ``bsddb``, have the advantage of using only one file per layer - style dimensions. To serve the ``mbtile`` and the ``bsddb`` see `Distribute the tiles`_. ``s3`` needs a ``bucket`` and a ``folder`` (defaults to ''). ``mbtiles``, ``bsddb`` and ``filesystem`` just need a ``folder``. On all the caches we can add some information to generate the URL where the tiles are available. This is needed to generate the capabilities. We can specify: * ``http_url`` direct url to the tiles root. * ``http_urls`` (array) urls to the tiles root. * ``http_url`` and ``hosts`` (array), where each value of ``hosts`` is used to replace ``%(host)s`` in ``http_url``. In all case ``http_url`` or ``http_urls`` can include all attributes of this cache as ``%(attribute)s``. MBTiles vs Berkley DB (``bsddb``) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * Read performance: similar, eventually the MBTiles is 10% faster. * Write performance: The Berkley DB is largely faster, about 10 times. * List the tiles: the MBTiles is largely faster but we usually don't need it. Configure layers ---------------- First of all, all the attributes in ``layer_default`` are copied in all the layers to define the default values. We have two ``type`` of layer: ``wms`` or ``mapnik``. To start the common attributes are: ``min_resolution_seed`` the minimum resolution that is seeded, other resolutions are served by MapCache. ``bbox`` used to limit the tiles generation. WMTS layout ~~~~~~~~~~~ To generate the file paths and the WMTS capabilities we need additional information: The ``mime_type`` of the tiles, it's also used by the WMS GetMap and to upload the tiles. The ``wmts_style`` defaults to 'default'. The ``extension`` is used to end the filename. The ``dimensions`` (defaults to []) is an array of objects that have a ``name``, a ``default`` value specified in the capabilities, a ``value`` to generate the tiles (it can be overwritten by an argument), and an array of ``values`` that contains all the possible values available in the capabilities. For example if you generate the tiles and capabilities with the following configuration: .. code:: yaml dimensions: - name: DATE default: 2012 value: 2012 values: [2012] then with the following configuration: .. code:: yaml dimensions: - name: DATE default: 2012 value: 2013 values: [2012, 2013] We will have two set of tiles ``2012`` and ``2013``, both accessible by the capabilities, and by default we will see the first set of tiles. Metatiles ~~~~~~~~~ The metatiles are activated by setting ``meta`` to ``on`` (by default it's ``off``). The metatiles are used for two things: first to generate multiple tiles with only one WMS query. By setting ``meta_size`` to 8 we will generate a square of 8 by 8 tiles in one shot. The second usage of metatiles is prevent cut label names: this is solved by getting a bigger image and cutting the borders. The ``meta_buffer`` should be set to a bigger value than half the size of the longest label. Configure hash ~~~~~~~~~~~~~~ We can filter tiles and metatiles by using an hash. The configuration of this hash is in the layer like this: .. code:: yaml empty_metatile_detection: size: 740 hash: 3237839c217b51b8a9644d596982f342f8041546 empty_tile_detection: size: 921 hash: 1e3da153be87a493c4c71198366485f290cad43c To easily generate this configuration we can use the following command:: .build/venv/bin/generate_tiles --get-hash -l Where ```` should refer to an empty tile/metatile. Generally it's a good idea to use z as the maximum zoom, x and y as 0. Configure geom/sql ~~~~~~~~~~~~~~~~~~ We can generate the tiles only on some geometries stored in PostGis. The configuration is in the layer like this: .. code:: yaml connection: user=www-data password=www-data dbname= host=localhost geoms: - sql: AS geom FROM
min_resolution: # included, optional, last win max_resolution: # included, optional, last win Example: .. code:: yaml connection: user=postgres password=postgres dbname=tests host=localhost geoms: - sql: the_geom AS geom FROM tests.polygon - sql: the_geom AS geom FROM tests.point min_resolution: 10 max_resolution: 20 It's preferable to use simple geometries, too complex geometries can slow down the generation. Legends ~~~~~~~ To be able to generate legends with ``.build/venv/bin/generate_controller --generate-legend-images`` you should have ``legend_mime`` and ``legend_extention`` in the layer config. for example: .. code:: yaml legend_mime: image/png legend_extention: png Then it will create a legend image per layer and per zoom level named ``.../1.0.0/{{layer}}/{{wmts_style}}/legend{{zoom}}.{{legend_extention}}`` only if she is deferent than the previous zoom level. If we have only one legend image it still stores in the file named ``legend0.{{legend_extention}}``. When we do ``.build/venv/bin/generate_controller --generate-wmts-capabilities`` we will at first parse the legend images to generate a layer config like this: .. code:: yaml legends: - mime_type: image/png href: http://host/tiles/layer/style/legend0.png min_resolution: 500 # optional, [m/px] max_resolution: 2000 # optional, [m/px] min_scale: # if define overwrite the min_resolution [m/m] max_scale: # if define overwrite the max_resolution [m/m] If you define a legends array in the layer configuration it is directly used to generate the capabilities. WMS layers ~~~~~~~~~~ The additional value needed by the WMS is the URL of the server and the ``layers``. The previously defined ``mime_type`` is also used in the WMS requests. To customise the request you also have the attributes ``params``, ``headers`` and ``generate_salt``. In ``params`` you can specify additional parameter of the WMS request, in ``headers`` you can modify the request headers. See the `Proxy/cache issue`_ for additional informations. Mapnik layers ~~~~~~~~~~~~~ We need to specify the ``mapfile`` path. With Mapnik we have the possibility to specify a ``data_buffer`` then we should set the unneeded ``meta_buffer`` to 0. And the ``output_format`` used for the Mapnik renderer, can be ``png``, ``png256``, ``jpeg``, ``grid`` (grid_renderer). ~~~~~~~~~~~~~~~~~~ Mapnik grid layers ~~~~~~~~~~~~~~~~~~ With Mapnik we can generate UTFGrid tiles (JSON format that describes the tiles present on a corresponding tile) by using the ``output_format`` 'grid', see also: https://github.com/mapnik/mapnik/wiki/MapnikRenderers#grid_renderer. Specific configuration: We have a specific way to ``drop_empty_utfgrid`` by using the ``on`` value. We should specify the pseudo pixel size [px] with the ``resolution``. And the ``layers_fields`` that we want to get the attributes. Object with the layer name as key and the values in an array as value. In fact the Mapnik documentation says that's working only for one layer. And don't forget to change the ``extension`` to ``json``, and the ``mime_type`` to ``application/utfgrid`` and the ``meta`` to ``off`` (not supported). Configuration example: .. code:: yaml grid: type: mapnik mapfile: style.mapnik output_format: grid extension: json mime_type: application/utfgrid drop_empty_utfgrid: on resolution: 4 meta: off data_buffer: 128 layers_fields: buildings: [name, street] Process ------- We can configure some tile commands to process the tiles. They can be automatically be called in the tile generation it we set the property ``post_process`` or ``pre_hash_post_process`` in the layer configuration. The process is a set of names processes, and each one has a list of commands declared like this: .. code:: yaml process: # root process config optipng: # the process command - cmd: optipng %(args)s -q -zc9 -zm8 -zs3 -f5 -o %(out)s %(in)s # the command line need_out: true # if false the command rewrite the input file, default to false arg: # argument used with the defferant log switches, all default to '' default: '-q' # the argument used by default quiet: '-q' # the arbument used in quiet mode verbose: '-v' # the argument used in verbose mode debug: '-log /tmp/optipng.log' # the argument user in debug mode The ``cmd`` can have the following optional argument: * ``args`` the argument configured in the `arg` section. * ``in``, ``out`` the input and output files. * ``x``, ``y``, ``z`` the tile coordinates. Configure Apache ---------------- To generate the Apache configuration we use the command:: .build/venv/bin/generate_controller --generate-apache-config The Apache configuration look like this (default values): .. code:: yaml apache: # Generated file config_file: apache/tiles.conf # Serve tiles location, default is /tiles location: /${instanceid}/tiles # Expires header in hours expires: 8 # Headers added to the ressponces headers: Cache-Control: max-age=864000, public If we use a proxy to access to the tiles we can specify a deferent URL to access to the tiles by adding the parameter ``tiles_url`` in the cache. Configure MapCache ------------------ For the last zoom levels we can use MapCache. To select the levels we generate the tiles an witch one we serve them using MapCache we have an option 'min_resolution_seed' in the layer configuration. The MapCache configuration look like this (default values): .. code:: yaml mapcache: # The generated file config_file: apache/mapcache.xml # The memcache host memcache_host: localhost # The memcache port memcache_port: 11211 # The mapcache location, default is /mapcache location: /${instanceid}/mapcache To generate the MapCache configuration we use the command:: .build/venv/bin/generate_controller --generate-mapcache-config Tiles error file ---------------- If we set a file path in config file: .. code:: yaml generation: error_file: The tiles that in error will be append to the file, ant the tiles can be regenerated with ``.build/venv/bin/generate_tiles --layer --tiles ``. The ```` can be ``/tmp/error_{layer}_{datetime:%Y-%m-%d_%H:%M:%S}`` to have one file per layer and per run. The tiles file looks like: .. code:: # [time] some comments z/x/y # [time] the error z/x/y:+m/+m # [time] the error The first line is just a comment, the second, is for an error on a tile, and the third is for an error on a metatile. Proxy/cache issue ----------------- In general we shouldn't generate tiles throw a proxy, to do that you should configure the layers as this: .. code:: yaml layers_name: url: http://localhost/wms headers: Host: the_host_name The idea is to get the WMS server on ``localhost`` and use the ``Host`` header to select the right Apache VirtualHost. To don't have cache we use the as default the headers: .. code:: yaml headers: Cache-Control: no-cache, no-store Pragma: no-cache And if you steal have issue you can add a ``SALT`` random argument by setting the layer parameter ``generate_salt`` to ``true``. Alternate mime type ------------------- By default TileCloud support only the ``image/jpeg`` and ``image/png`` mime type. ---------------- Amazon services ---------------- Authentication -------------- To be authenticated by Amazon you should set those environment variable before running a command:: export AWS_ACCESS_KEY_ID=... export AWS_SECRET_ACCESS_KEY=... Configure S3 ------------ The cache configuration is like this: .. code:: yaml s3: type: s3 # the s3 bucket name bucket: tiles # the used folder in the bucket [default to ''] folder: '' # for GetCapabilities http_url: https://%(host)s/%(bucket)s/%(folder)s hosts: - wmts0. The bucket should already exists. Configure SQS ------------- The configuration in layer is like this: .. code:: yaml sqs: # The region where the SQS queue is region: eu-west-1 # The SQS queue name, it should already exists queue: the_name The queue should be used only by one layer. To use the SQS queue we should first fill the queue:: .build/venv/bin/generate_tiles --role master --layer And then generate the tiles present in the SQS queue:: .build/venv/bin/generate_tiles --role slave --layer Configure SNS ------------- SNS can be used to send a message when the generation ends. The configuration is like this: .. code:: yaml sns: topic: arn:aws:sns:eu-west-1:your-account-id:tilecloud region: eu-west-1 The topic should already exists. Configure and explain EC2 ------------------------- The generation can be deported on an external host. This will deploy the code the database and the geodata to an external host, configure or build the application, configure apache, and run the tile generation. This work only with S3 and needs SQS. In a future version it will start the new EC2 host, join an ESB, run the tile generation, and do snapshot on the ESB. The configuration is like this: .. code:: yaml ec2: geodata_folder: /var/sig deploy_config: tilegeneration/deploy.cfg deploy_user: deploy code_folder: /var/www/vhost/project/private/project apache_config: /var/www/vhost/project/conf/tilegeneration.conf apache_content: Include /var/www/vhost/project/private/project/apache/\*.conf Amazon tool ----------- Amazon has a command line tool (`homepage `_). To use it, add in the ``setup.py``: * ``awscli`` as an ``install_requires``, * ``'aws = awscli.clidriver:main',`` in the ``console_scripts``. Than install it: .. code:: bash .build/venv/bin/pip install awscli And use it: .. code:: bash .build/venv/bin/aws help For example to delete many tiles do: .. code:: bash .build/venv/bin/aws s3 rm --recursive s3://your_bucket_name/folder --------------------------- Other related configuration --------------------------- Openlayers pink tiles --------------------- To avoid the OpenLayers red tiles on missing empty tiles we can add the following CSS rule: .. code:: css .olImageLoadError { display: none; } To completely hide the missing tiles, useful for a transparent layer, or for an opaque layer: .. code:: css .olImageLoadError { background-color: white; } -------------------- Distribute the tiles -------------------- There two ways to serve the tiles, with Apache configuration, or with an internal server. The advantage of the internal server are: * Can distribute Mbtiles or Berkley DB. * Return ``204 No Content`` HTTP code in place of ``404 Not Found`` (or ``403 Forbidden`` for s3). * Can be used in `KVP` mode. * Can have zone per layer where are the tiles, otherwise it redirect on mapcache. To generate the Apache configuration we use the command:: .build/venv/bin/generate_controller --generate-apache-config The server can be configure as it: .. code:: yaml server: layers: a_layer # Restrict to serve an certain number of layers [default to all] cache: mbtiles # The used cache [default use generation/default_cache] # the URL without location to MapCache, [default to http://localhost/] mapcache_base: http://localhost/ mapcache_headers: # headers, can be used to access to an other Apache vhost [default to {}] Host: localhost geoms_redirect: true # use the geoms to redirect to MapCache [defaut to false] # allowed extension in the static path (default value), not used for s3. static_allow_extension: [jpeg, png, xml, js, html, css] The minimal config is to enable it: .. code:: yaml server: {} You should also configure the ``http_url`` of the used `cache`, to something like ``https://%(host)s/${instanceid}/tiles`` or like ``https://%(host)s/${instanceid}/wsgi/tiles`` if you use the Pyramid view. Pyramid view ------------ To use the pyramid view use the following config: .. code:: python config.get_settings().update({ 'tilegeneration_configfile': '', }) config.add_route('tiles', '/tiles/\*path') config.add_view('tilecloud_chain.server:PyramidView', route_name='tiles') Internal WSGI server -------------------- in ``production.ini``:: [app:tiles] use = egg:tilecloud_chain#server configfile = %(here)s/tilegeneration/config.yaml with the apache configuration:: WSGIDaemonProcess tiles:${instanceid} display-name=%{GROUP} user=${modwsgi_user} WSGIScriptAlias /${instanceid}/tiles ${directory}/apache/wmts.wsgi WSGIProcessGroup tiles:${instanceid} WSGIApplicationGroup %{GLOBAL} -------- Commands -------- Available commands ------------------ * ``.build/venv/bin/generate_controller`` generate the annexe files like capabilities, legend, OpenLayers test page, MapCacke config, Apache config. * ``.build/venv/bin/generate_tiles`` generate the tiles. * ``.build/venv/bin/generate_copy`` copy the tiles from a cache to an other. * ``.build/venv/bin/generate_process`` prosses the tiles using a configured prosess. * ``.build/venv/bin/generate_cost`` estimate the cost. * ``.build/venv/bin/generate_amazon`` generate the tiles using EC2. * ``.build/venv/bin/import_expiretiles`` import the osm2pgsql expire-tiles file as geoms in the database. Each commands have a ``--help`` option to give a full arguments help. Generate tiles -------------- Generate all the tiles:: .build/venv/bin/generate_tiles Generate a specific layer:: .build/venv/bin/generate_tiles --layer Generate a specific zoom:: .build/venv/bin/generate_tiles --zoom 5 Generate a specific zoom range:: .build/venv/bin/generate_tiles --zoom 2-8 Generate a specific some zoom levels:: .build/venv/bin/generate_tiles --zoom 2,4,7 Generate tiles from an (error) tiles file:: .build/venv/bin/generate_tiles --layer --tiles Generate tiles on a bbox:: .build/venv/bin/generate_tiles --bbox Generate a tiles near a tile coordinate (useful for test):: .build/venv/bin/generate_tiles --near Generate a tiles in a deferent cache than the default one:: .build/venv/bin/generate_tiles --cache And don't forget to generate the WMTS Capabilities:: .build/venv/bin/generate_controller --capabilities OpenLayers test page -------------------- To generate a test page use:: .build/venv/bin/generate_controller --openlayers-test ------------ Explain cost ------------ Configuration (default values): .. code:: yaml cost: # [nb/month] request_per_layers: 10000000 # GeoData size [Go] esb_size: 100 cloudfront: download: 0.12, get: 0.009 ec2: usage: 0.17 esb: io: 260.0, storage: 0.11 esb_size: 100 request_per_layers: 10000000 s3: download: 0.12, get: 0.01, put: 0.01, storage: 0.125 sqs: request: 0.01 Layer configuration (default values): .. code:: yaml cost: metatile_generation_time: 30.0, tile_generation_time: 30.0, tile_size: 20.0, tileonly_generation_time: 60.0 The following commands can be used to know the time and cost to do generation:: .build/venv/bin/generate_controller --cost This suppose that you use a separate EC2 host to generate the tiles. Useful options -------------- ``--quiet`` or ``-q``: used to display only errors. ``--verbose`` or ``-v``: used to display info messages. ``--debug`` or ``-d``: used to display debug message, pleas use this option to report issue. With the debug mode we don't catch exceptions, and we don't log time messages. ``--test `` or ``-t ``: used to generate only ```` tiles, useful for test. The logging format is configurable in the``config.yaml`` - ``generation/log_format``, `See `_. ----------------- Important remarks ----------------- Especially on S3 the grid name, the layer name, the dimensions, can't be changed (understand if we want to change them we should regenerate all the tiles). By default we also can't insert a zoom level, if you think that you need it we can set the grid property ``matrix_identifier: resolution``, bit it don't work with MapCache. Please use the ``--debug`` to report issue. ------------ >From sources ------------ Build it: .. code:: bash git submodule update --recursive mkdir .build virtualenv --setuptools --no-site-packages .build/venv .build/venv/bin/pip install 'pip>=6' 'setuptools>=12' .build/venv/bin/pip install -e . .build/venv/bin/pip install -r dev-requirements.txt Changelog ========= ----------- Release 0.9 ----------- 1. Correct some error with slash. 2. Better error handling. 3. Be able to have one error file per layer. ----------- Release 0.8 ----------- 1. Correct some error with slash. 2. Add ``pre_hash_post_process`` and ``post_process``. 3. Add copy command. ----------- Release 0.7 ----------- 1. Support of deferent geoms per layers, requires configuration changes, old version: .. code:: yaml connection: user=www-data password=www-data dbname= host=localhost sql: AS geom FROM
to new version: .. code:: yaml connection: user=www-data password=www-data dbname= host=localhost geoms: - sql: AS geom FROM
More informations in the **Configure geom/sql** chapter. 2. Update from ``optparse`` to ``argparse``, and some argument refactoring, use ``--help`` to see the new version. 3. Add support of Blackbery DB (``bsddb``). 4. The tile ``server`` is completely rewrite, now it support all cache, ``REST`` and ``KVP`` interface, ``GetFeatureInfo`` request, and it can be used as a pyramid view or as a ``WSGI`` server. More informations in the **istribute the tiles** chapter. 5. Add three strategy to bypass the proxy/cache: Use the headers ``Cache-Control: no-cache, no-store``, ``Pragma: no-cache`` (default). Use localhost in the URL and the header ``Host: `` (recommended). Add a ``SALT`` random argument (if the above don't work). More informations in the **Proxy/cache issue** chapter. 6. Improve the dimensions usage by adding it ti the WMS requests, And add a ``--dimensions`` argument of ``generate_tiles`` to change the dimensions values. 7. Extract generate_cost and generate_amazon from generate_controler. 8. Now we can creates legends, see the **Legends** chapter. 9. Now the tiles generation display generation statistics at the ends. 10. The EC2 configuration is moved in a separate structure, see README for more informations. ----------- Release 0.6 ----------- 1. Now the apache configuration can be generated with ``.build/venv/bin/generate_controller --generate-apache-config``, it support ``filesystem`` ``cache`` and ``MapCache``. 2. Windows fixes. 3. Use console rewrite (\r) to log generated tiles coordinates. 4. Now if no layers is specified in ``generation:default_layers`` we generate all layers by default. 5. Now bbox to be floats. 6. New ``--get-bbox`` option to get the bbox of a tile. 7. Add coveralls support (https://coveralls.io/r/sbrunner/tilecloud-chain). 8. Add an config option ``generation:error_file`` and a command option ``--tiles`` to store and regenerate errored tiles. ----------- Release 0.5 ----------- 1. SQS config change: .. code:: yaml layers: layer_name: sqs: # The region where the SQS queue is region: eu-west-1 # The SQS queue name, it should already exists queue: the_name 2. Add debug option (``--debug``), please use it to report issue. 3. Now the ``sql`` request can return a set of geometries in a column names geom but the syntax change a little bit => `` AS geom FROM
`` PKwH0tilecloud_chain-1.1.0.dist-info/entry_points.txt[console_scripts] generate_amazon = tilecloud_chain.amazon:main generate_controller = tilecloud_chain.controller:main generate_copy = tilecloud_chain.copy_:main generate_cost = tilecloud_chain.cost:main generate_process = tilecloud_chain.copy_:process generate_tiles = tilecloud_chain.generate:main import_expiretiles = tilecloud_chain.expiretiles:main [paste.app_factory] server = tilecloud_chain.server:app_factory [pyramid.scaffold] tilecloud_chain = tilecloud_chain.scaffolds:Create tilecloud_chain_ec2 = tilecloud_chain.scaffolds:Ec2 PKwH_-tilecloud_chain-1.1.0.dist-info/metadata.json{"classifiers": ["Development Status :: 4 - Beta", "Environment :: Console", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Programming Language :: Python", "Topic :: Scientific/Engineering :: GIS"], "extensions": {"python.commands": {"wrap_console": {"generate_amazon": "tilecloud_chain.amazon:main", "generate_controller": "tilecloud_chain.controller:main", "generate_copy": "tilecloud_chain.copy_:main", "generate_cost": "tilecloud_chain.cost:main", "generate_process": "tilecloud_chain.copy_:process", "generate_tiles": "tilecloud_chain.generate:main", "import_expiretiles": "tilecloud_chain.expiretiles:main"}}, "python.details": {"contacts": [{"email": "stephane.brunner@camptocamp.com", "name": "St\u00e9phane Brunner", "role": "author"}], "document_names": {"description": "DESCRIPTION.rst"}, "project_urls": {"Home": "http://github.com/sbrunner/tilecloud-chain"}}, "python.exports": {"console_scripts": {"generate_amazon": "tilecloud_chain.amazon:main", "generate_controller": "tilecloud_chain.controller:main", "generate_copy": "tilecloud_chain.copy_:main", "generate_cost": "tilecloud_chain.cost:main", "generate_process": "tilecloud_chain.copy_:process", "generate_tiles": "tilecloud_chain.generate:main", "import_expiretiles": "tilecloud_chain.expiretiles:main"}, "paste.app_factory": {"server": "tilecloud_chain.server:app_factory"}, "pyramid.scaffold": {"tilecloud_chain": "tilecloud_chain.scaffolds:Create", "tilecloud_chain_ec2": "tilecloud_chain.scaffolds:Ec2"}}}, "extras": [], "generator": "bdist_wheel (0.26.0)", "keywords": ["gis", "tilecloud", "chain"], "license": "BSD", "metadata_version": "2.0", "name": "tilecloud-chain", "run_requires": [{"requires": ["Jinja2", "PyYAML", "Shapely", "boto (>=2.0)", "psycopg2", "pyramid", "requests", "simplejson", "six", "tilecloud (>=0.2dev-20130808)"]}], "summary": "Tools to generates tiles from WMS or Mapnik, to S3, Berkley DB, MBTiles, or local filesystem in WMTS layout using Amazon cloud services.", "version": "1.1.0"}PKwHK-tilecloud_chain-1.1.0.dist-info/top_level.txttilecloud_chain PKwH''\\%tilecloud_chain-1.1.0.dist-info/WHEELWheel-Version: 1.0 Generator: bdist_wheel (0.26.0) Root-Is-Purelib: true Tag: py2-none-any PKwH#zpp(tilecloud_chain-1.1.0.dist-info/METADATAMetadata-Version: 2.0 Name: tilecloud-chain Version: 1.1.0 Summary: Tools to generates tiles from WMS or Mapnik, to S3, Berkley DB, MBTiles, or local filesystem in WMTS layout using Amazon cloud services. Home-page: http://github.com/sbrunner/tilecloud-chain Author: Stéphane Brunner Author-email: stephane.brunner@camptocamp.com License: BSD Keywords: gis tilecloud chain Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Environment :: Console Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Programming Language :: Python Classifier: Topic :: Scientific/Engineering :: GIS Requires-Dist: Jinja2 Requires-Dist: PyYAML Requires-Dist: Shapely Requires-Dist: boto (>=2.0) Requires-Dist: psycopg2 Requires-Dist: pyramid Requires-Dist: requests Requires-Dist: simplejson Requires-Dist: six Requires-Dist: tilecloud (>=0.2dev-20130808) TileCloud Chain =============== .. image:: https://secure.travis-ci.org/sbrunner/tilecloud-chain.svg?branch=master .. image:: https://coveralls.io/repos/sbrunner/tilecloud-chain/badge.png?branch=master The goal of TileCloud Chain is to provide tools around tile generation on a chain like: Source: WMS, Mapnik. Optionally using an SQS queue, AWS host, SNS topic. Destination in WMTS layout, on S3, on Berkley DB (``bsddb``), on MBTiles, or on local filesystem. Features: - Generate tiles. - Drop empty tiles. - Drop tiles outside a geometry or a bbox. - Use MetaTiles - Generate GetCapabilities. - Generate OpenLayers example page. - Obtain the hash of an empty tile - In future, measure tile generation speed - Calculate cost and generation time. - In future, manage the AWS hosts that generate tiles. - Delete empty tiles. .. contents:: Table of contents ------ Get it ------ Requirements:: pg_config and a build environment. Install:: mkdir .build virtualenv .build/venv .build/venv/bin/pip install tilecloud-chain .build/venv/bin/pcreate -s tilecloud_chain . Edit your layers configuration in ``./tilegeneration/config.yaml``. `Default configuration file `_. --------- Configure --------- Configure grids --------------- The ``grid`` describes how the tiles are arranged. Especially on ``s3`` be careful to choose every of the grid settings before generating the tiles. If you change one of them you must regenerate all the tiles. The ``resolutions`` in [px/m] describes all the resolutions available for this layer. For a raster layer, have a look to the maximum resolution of the source files. It is not needed to generate tiles at smaller resolutions than the sources, it is preferable to use the OpenLayers client zoom. Note that you can add a resolution in the end without regenerating all the tiles. The ``bbox`` should match the resolution of the extent. **CAREFUL: you will have big issue if you use this parameter to generate the tile on a restricted area**: use the ``bbox`` on the layer instead. The ``srs`` specifies the code of the projection. The ``unit`` is the unit used by the projection. The ``tile_size`` is the tile size in [px], defaults to 256. The ``matrix_identifier`` is ``zoom`` by default and can also be set to ``resolution``. It specifies how the z index is build to store the tiles, for example, for the resolutions ``[2, 1, 0.5]`` the used values are ``[0, 1, 2]`` based on the zoom and ``[2, 1, 0_5]`` based on the resolution. The second has the advantage of allowing to add a new resolution without regenerating all the tiles, but it does not work with MapCache. Configure caches ---------------- The available tile caches are: ``s3``, ``bsddb``, ``mbtile`` and ``filesystem``. The best solution to store the tiles, ``s3``, ``mbtiles`` and ``bsddb``, have the advantage of using only one file per layer - style dimensions. To serve the ``mbtile`` and the ``bsddb`` see `Distribute the tiles`_. ``s3`` needs a ``bucket`` and a ``folder`` (defaults to ''). ``mbtiles``, ``bsddb`` and ``filesystem`` just need a ``folder``. On all the caches we can add some information to generate the URL where the tiles are available. This is needed to generate the capabilities. We can specify: * ``http_url`` direct url to the tiles root. * ``http_urls`` (array) urls to the tiles root. * ``http_url`` and ``hosts`` (array), where each value of ``hosts`` is used to replace ``%(host)s`` in ``http_url``. In all case ``http_url`` or ``http_urls`` can include all attributes of this cache as ``%(attribute)s``. MBTiles vs Berkley DB (``bsddb``) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * Read performance: similar, eventually the MBTiles is 10% faster. * Write performance: The Berkley DB is largely faster, about 10 times. * List the tiles: the MBTiles is largely faster but we usually don't need it. Configure layers ---------------- First of all, all the attributes in ``layer_default`` are copied in all the layers to define the default values. We have two ``type`` of layer: ``wms`` or ``mapnik``. To start the common attributes are: ``min_resolution_seed`` the minimum resolution that is seeded, other resolutions are served by MapCache. ``bbox`` used to limit the tiles generation. WMTS layout ~~~~~~~~~~~ To generate the file paths and the WMTS capabilities we need additional information: The ``mime_type`` of the tiles, it's also used by the WMS GetMap and to upload the tiles. The ``wmts_style`` defaults to 'default'. The ``extension`` is used to end the filename. The ``dimensions`` (defaults to []) is an array of objects that have a ``name``, a ``default`` value specified in the capabilities, a ``value`` to generate the tiles (it can be overwritten by an argument), and an array of ``values`` that contains all the possible values available in the capabilities. For example if you generate the tiles and capabilities with the following configuration: .. code:: yaml dimensions: - name: DATE default: 2012 value: 2012 values: [2012] then with the following configuration: .. code:: yaml dimensions: - name: DATE default: 2012 value: 2013 values: [2012, 2013] We will have two set of tiles ``2012`` and ``2013``, both accessible by the capabilities, and by default we will see the first set of tiles. Metatiles ~~~~~~~~~ The metatiles are activated by setting ``meta`` to ``on`` (by default it's ``off``). The metatiles are used for two things: first to generate multiple tiles with only one WMS query. By setting ``meta_size`` to 8 we will generate a square of 8 by 8 tiles in one shot. The second usage of metatiles is prevent cut label names: this is solved by getting a bigger image and cutting the borders. The ``meta_buffer`` should be set to a bigger value than half the size of the longest label. Configure hash ~~~~~~~~~~~~~~ We can filter tiles and metatiles by using an hash. The configuration of this hash is in the layer like this: .. code:: yaml empty_metatile_detection: size: 740 hash: 3237839c217b51b8a9644d596982f342f8041546 empty_tile_detection: size: 921 hash: 1e3da153be87a493c4c71198366485f290cad43c To easily generate this configuration we can use the following command:: .build/venv/bin/generate_tiles --get-hash -l Where ```` should refer to an empty tile/metatile. Generally it's a good idea to use z as the maximum zoom, x and y as 0. Configure geom/sql ~~~~~~~~~~~~~~~~~~ We can generate the tiles only on some geometries stored in PostGis. The configuration is in the layer like this: .. code:: yaml connection: user=www-data password=www-data dbname= host=localhost geoms: - sql: AS geom FROM
min_resolution: # included, optional, last win max_resolution: # included, optional, last win Example: .. code:: yaml connection: user=postgres password=postgres dbname=tests host=localhost geoms: - sql: the_geom AS geom FROM tests.polygon - sql: the_geom AS geom FROM tests.point min_resolution: 10 max_resolution: 20 It's preferable to use simple geometries, too complex geometries can slow down the generation. Legends ~~~~~~~ To be able to generate legends with ``.build/venv/bin/generate_controller --generate-legend-images`` you should have ``legend_mime`` and ``legend_extention`` in the layer config. for example: .. code:: yaml legend_mime: image/png legend_extention: png Then it will create a legend image per layer and per zoom level named ``.../1.0.0/{{layer}}/{{wmts_style}}/legend{{zoom}}.{{legend_extention}}`` only if she is deferent than the previous zoom level. If we have only one legend image it still stores in the file named ``legend0.{{legend_extention}}``. When we do ``.build/venv/bin/generate_controller --generate-wmts-capabilities`` we will at first parse the legend images to generate a layer config like this: .. code:: yaml legends: - mime_type: image/png href: http://host/tiles/layer/style/legend0.png min_resolution: 500 # optional, [m/px] max_resolution: 2000 # optional, [m/px] min_scale: # if define overwrite the min_resolution [m/m] max_scale: # if define overwrite the max_resolution [m/m] If you define a legends array in the layer configuration it is directly used to generate the capabilities. WMS layers ~~~~~~~~~~ The additional value needed by the WMS is the URL of the server and the ``layers``. The previously defined ``mime_type`` is also used in the WMS requests. To customise the request you also have the attributes ``params``, ``headers`` and ``generate_salt``. In ``params`` you can specify additional parameter of the WMS request, in ``headers`` you can modify the request headers. See the `Proxy/cache issue`_ for additional informations. Mapnik layers ~~~~~~~~~~~~~ We need to specify the ``mapfile`` path. With Mapnik we have the possibility to specify a ``data_buffer`` then we should set the unneeded ``meta_buffer`` to 0. And the ``output_format`` used for the Mapnik renderer, can be ``png``, ``png256``, ``jpeg``, ``grid`` (grid_renderer). ~~~~~~~~~~~~~~~~~~ Mapnik grid layers ~~~~~~~~~~~~~~~~~~ With Mapnik we can generate UTFGrid tiles (JSON format that describes the tiles present on a corresponding tile) by using the ``output_format`` 'grid', see also: https://github.com/mapnik/mapnik/wiki/MapnikRenderers#grid_renderer. Specific configuration: We have a specific way to ``drop_empty_utfgrid`` by using the ``on`` value. We should specify the pseudo pixel size [px] with the ``resolution``. And the ``layers_fields`` that we want to get the attributes. Object with the layer name as key and the values in an array as value. In fact the Mapnik documentation says that's working only for one layer. And don't forget to change the ``extension`` to ``json``, and the ``mime_type`` to ``application/utfgrid`` and the ``meta`` to ``off`` (not supported). Configuration example: .. code:: yaml grid: type: mapnik mapfile: style.mapnik output_format: grid extension: json mime_type: application/utfgrid drop_empty_utfgrid: on resolution: 4 meta: off data_buffer: 128 layers_fields: buildings: [name, street] Process ------- We can configure some tile commands to process the tiles. They can be automatically be called in the tile generation it we set the property ``post_process`` or ``pre_hash_post_process`` in the layer configuration. The process is a set of names processes, and each one has a list of commands declared like this: .. code:: yaml process: # root process config optipng: # the process command - cmd: optipng %(args)s -q -zc9 -zm8 -zs3 -f5 -o %(out)s %(in)s # the command line need_out: true # if false the command rewrite the input file, default to false arg: # argument used with the defferant log switches, all default to '' default: '-q' # the argument used by default quiet: '-q' # the arbument used in quiet mode verbose: '-v' # the argument used in verbose mode debug: '-log /tmp/optipng.log' # the argument user in debug mode The ``cmd`` can have the following optional argument: * ``args`` the argument configured in the `arg` section. * ``in``, ``out`` the input and output files. * ``x``, ``y``, ``z`` the tile coordinates. Configure Apache ---------------- To generate the Apache configuration we use the command:: .build/venv/bin/generate_controller --generate-apache-config The Apache configuration look like this (default values): .. code:: yaml apache: # Generated file config_file: apache/tiles.conf # Serve tiles location, default is /tiles location: /${instanceid}/tiles # Expires header in hours expires: 8 # Headers added to the ressponces headers: Cache-Control: max-age=864000, public If we use a proxy to access to the tiles we can specify a deferent URL to access to the tiles by adding the parameter ``tiles_url`` in the cache. Configure MapCache ------------------ For the last zoom levels we can use MapCache. To select the levels we generate the tiles an witch one we serve them using MapCache we have an option 'min_resolution_seed' in the layer configuration. The MapCache configuration look like this (default values): .. code:: yaml mapcache: # The generated file config_file: apache/mapcache.xml # The memcache host memcache_host: localhost # The memcache port memcache_port: 11211 # The mapcache location, default is /mapcache location: /${instanceid}/mapcache To generate the MapCache configuration we use the command:: .build/venv/bin/generate_controller --generate-mapcache-config Tiles error file ---------------- If we set a file path in config file: .. code:: yaml generation: error_file: The tiles that in error will be append to the file, ant the tiles can be regenerated with ``.build/venv/bin/generate_tiles --layer --tiles ``. The ```` can be ``/tmp/error_{layer}_{datetime:%Y-%m-%d_%H:%M:%S}`` to have one file per layer and per run. The tiles file looks like: .. code:: # [time] some comments z/x/y # [time] the error z/x/y:+m/+m # [time] the error The first line is just a comment, the second, is for an error on a tile, and the third is for an error on a metatile. Proxy/cache issue ----------------- In general we shouldn't generate tiles throw a proxy, to do that you should configure the layers as this: .. code:: yaml layers_name: url: http://localhost/wms headers: Host: the_host_name The idea is to get the WMS server on ``localhost`` and use the ``Host`` header to select the right Apache VirtualHost. To don't have cache we use the as default the headers: .. code:: yaml headers: Cache-Control: no-cache, no-store Pragma: no-cache And if you steal have issue you can add a ``SALT`` random argument by setting the layer parameter ``generate_salt`` to ``true``. Alternate mime type ------------------- By default TileCloud support only the ``image/jpeg`` and ``image/png`` mime type. ---------------- Amazon services ---------------- Authentication -------------- To be authenticated by Amazon you should set those environment variable before running a command:: export AWS_ACCESS_KEY_ID=... export AWS_SECRET_ACCESS_KEY=... Configure S3 ------------ The cache configuration is like this: .. code:: yaml s3: type: s3 # the s3 bucket name bucket: tiles # the used folder in the bucket [default to ''] folder: '' # for GetCapabilities http_url: https://%(host)s/%(bucket)s/%(folder)s hosts: - wmts0. The bucket should already exists. Configure SQS ------------- The configuration in layer is like this: .. code:: yaml sqs: # The region where the SQS queue is region: eu-west-1 # The SQS queue name, it should already exists queue: the_name The queue should be used only by one layer. To use the SQS queue we should first fill the queue:: .build/venv/bin/generate_tiles --role master --layer And then generate the tiles present in the SQS queue:: .build/venv/bin/generate_tiles --role slave --layer Configure SNS ------------- SNS can be used to send a message when the generation ends. The configuration is like this: .. code:: yaml sns: topic: arn:aws:sns:eu-west-1:your-account-id:tilecloud region: eu-west-1 The topic should already exists. Configure and explain EC2 ------------------------- The generation can be deported on an external host. This will deploy the code the database and the geodata to an external host, configure or build the application, configure apache, and run the tile generation. This work only with S3 and needs SQS. In a future version it will start the new EC2 host, join an ESB, run the tile generation, and do snapshot on the ESB. The configuration is like this: .. code:: yaml ec2: geodata_folder: /var/sig deploy_config: tilegeneration/deploy.cfg deploy_user: deploy code_folder: /var/www/vhost/project/private/project apache_config: /var/www/vhost/project/conf/tilegeneration.conf apache_content: Include /var/www/vhost/project/private/project/apache/\*.conf Amazon tool ----------- Amazon has a command line tool (`homepage `_). To use it, add in the ``setup.py``: * ``awscli`` as an ``install_requires``, * ``'aws = awscli.clidriver:main',`` in the ``console_scripts``. Than install it: .. code:: bash .build/venv/bin/pip install awscli And use it: .. code:: bash .build/venv/bin/aws help For example to delete many tiles do: .. code:: bash .build/venv/bin/aws s3 rm --recursive s3://your_bucket_name/folder --------------------------- Other related configuration --------------------------- Openlayers pink tiles --------------------- To avoid the OpenLayers red tiles on missing empty tiles we can add the following CSS rule: .. code:: css .olImageLoadError { display: none; } To completely hide the missing tiles, useful for a transparent layer, or for an opaque layer: .. code:: css .olImageLoadError { background-color: white; } -------------------- Distribute the tiles -------------------- There two ways to serve the tiles, with Apache configuration, or with an internal server. The advantage of the internal server are: * Can distribute Mbtiles or Berkley DB. * Return ``204 No Content`` HTTP code in place of ``404 Not Found`` (or ``403 Forbidden`` for s3). * Can be used in `KVP` mode. * Can have zone per layer where are the tiles, otherwise it redirect on mapcache. To generate the Apache configuration we use the command:: .build/venv/bin/generate_controller --generate-apache-config The server can be configure as it: .. code:: yaml server: layers: a_layer # Restrict to serve an certain number of layers [default to all] cache: mbtiles # The used cache [default use generation/default_cache] # the URL without location to MapCache, [default to http://localhost/] mapcache_base: http://localhost/ mapcache_headers: # headers, can be used to access to an other Apache vhost [default to {}] Host: localhost geoms_redirect: true # use the geoms to redirect to MapCache [defaut to false] # allowed extension in the static path (default value), not used for s3. static_allow_extension: [jpeg, png, xml, js, html, css] The minimal config is to enable it: .. code:: yaml server: {} You should also configure the ``http_url`` of the used `cache`, to something like ``https://%(host)s/${instanceid}/tiles`` or like ``https://%(host)s/${instanceid}/wsgi/tiles`` if you use the Pyramid view. Pyramid view ------------ To use the pyramid view use the following config: .. code:: python config.get_settings().update({ 'tilegeneration_configfile': '', }) config.add_route('tiles', '/tiles/\*path') config.add_view('tilecloud_chain.server:PyramidView', route_name='tiles') Internal WSGI server -------------------- in ``production.ini``:: [app:tiles] use = egg:tilecloud_chain#server configfile = %(here)s/tilegeneration/config.yaml with the apache configuration:: WSGIDaemonProcess tiles:${instanceid} display-name=%{GROUP} user=${modwsgi_user} WSGIScriptAlias /${instanceid}/tiles ${directory}/apache/wmts.wsgi WSGIProcessGroup tiles:${instanceid} WSGIApplicationGroup %{GLOBAL} -------- Commands -------- Available commands ------------------ * ``.build/venv/bin/generate_controller`` generate the annexe files like capabilities, legend, OpenLayers test page, MapCacke config, Apache config. * ``.build/venv/bin/generate_tiles`` generate the tiles. * ``.build/venv/bin/generate_copy`` copy the tiles from a cache to an other. * ``.build/venv/bin/generate_process`` prosses the tiles using a configured prosess. * ``.build/venv/bin/generate_cost`` estimate the cost. * ``.build/venv/bin/generate_amazon`` generate the tiles using EC2. * ``.build/venv/bin/import_expiretiles`` import the osm2pgsql expire-tiles file as geoms in the database. Each commands have a ``--help`` option to give a full arguments help. Generate tiles -------------- Generate all the tiles:: .build/venv/bin/generate_tiles Generate a specific layer:: .build/venv/bin/generate_tiles --layer Generate a specific zoom:: .build/venv/bin/generate_tiles --zoom 5 Generate a specific zoom range:: .build/venv/bin/generate_tiles --zoom 2-8 Generate a specific some zoom levels:: .build/venv/bin/generate_tiles --zoom 2,4,7 Generate tiles from an (error) tiles file:: .build/venv/bin/generate_tiles --layer --tiles Generate tiles on a bbox:: .build/venv/bin/generate_tiles --bbox Generate a tiles near a tile coordinate (useful for test):: .build/venv/bin/generate_tiles --near Generate a tiles in a deferent cache than the default one:: .build/venv/bin/generate_tiles --cache And don't forget to generate the WMTS Capabilities:: .build/venv/bin/generate_controller --capabilities OpenLayers test page -------------------- To generate a test page use:: .build/venv/bin/generate_controller --openlayers-test ------------ Explain cost ------------ Configuration (default values): .. code:: yaml cost: # [nb/month] request_per_layers: 10000000 # GeoData size [Go] esb_size: 100 cloudfront: download: 0.12, get: 0.009 ec2: usage: 0.17 esb: io: 260.0, storage: 0.11 esb_size: 100 request_per_layers: 10000000 s3: download: 0.12, get: 0.01, put: 0.01, storage: 0.125 sqs: request: 0.01 Layer configuration (default values): .. code:: yaml cost: metatile_generation_time: 30.0, tile_generation_time: 30.0, tile_size: 20.0, tileonly_generation_time: 60.0 The following commands can be used to know the time and cost to do generation:: .build/venv/bin/generate_controller --cost This suppose that you use a separate EC2 host to generate the tiles. Useful options -------------- ``--quiet`` or ``-q``: used to display only errors. ``--verbose`` or ``-v``: used to display info messages. ``--debug`` or ``-d``: used to display debug message, pleas use this option to report issue. With the debug mode we don't catch exceptions, and we don't log time messages. ``--test `` or ``-t ``: used to generate only ```` tiles, useful for test. The logging format is configurable in the``config.yaml`` - ``generation/log_format``, `See `_. ----------------- Important remarks ----------------- Especially on S3 the grid name, the layer name, the dimensions, can't be changed (understand if we want to change them we should regenerate all the tiles). By default we also can't insert a zoom level, if you think that you need it we can set the grid property ``matrix_identifier: resolution``, bit it don't work with MapCache. Please use the ``--debug`` to report issue. ------------ >From sources ------------ Build it: .. code:: bash git submodule update --recursive mkdir .build virtualenv --setuptools --no-site-packages .build/venv .build/venv/bin/pip install 'pip>=6' 'setuptools>=12' .build/venv/bin/pip install -e . .build/venv/bin/pip install -r dev-requirements.txt Changelog ========= ----------- Release 0.9 ----------- 1. Correct some error with slash. 2. Better error handling. 3. Be able to have one error file per layer. ----------- Release 0.8 ----------- 1. Correct some error with slash. 2. Add ``pre_hash_post_process`` and ``post_process``. 3. Add copy command. ----------- Release 0.7 ----------- 1. Support of deferent geoms per layers, requires configuration changes, old version: .. code:: yaml connection: user=www-data password=www-data dbname= host=localhost sql: AS geom FROM
to new version: .. code:: yaml connection: user=www-data password=www-data dbname= host=localhost geoms: - sql: AS geom FROM
More informations in the **Configure geom/sql** chapter. 2. Update from ``optparse`` to ``argparse``, and some argument refactoring, use ``--help`` to see the new version. 3. Add support of Blackbery DB (``bsddb``). 4. The tile ``server`` is completely rewrite, now it support all cache, ``REST`` and ``KVP`` interface, ``GetFeatureInfo`` request, and it can be used as a pyramid view or as a ``WSGI`` server. More informations in the **istribute the tiles** chapter. 5. Add three strategy to bypass the proxy/cache: Use the headers ``Cache-Control: no-cache, no-store``, ``Pragma: no-cache`` (default). Use localhost in the URL and the header ``Host: `` (recommended). Add a ``SALT`` random argument (if the above don't work). More informations in the **Proxy/cache issue** chapter. 6. Improve the dimensions usage by adding it ti the WMS requests, And add a ``--dimensions`` argument of ``generate_tiles`` to change the dimensions values. 7. Extract generate_cost and generate_amazon from generate_controler. 8. Now we can creates legends, see the **Legends** chapter. 9. Now the tiles generation display generation statistics at the ends. 10. The EC2 configuration is moved in a separate structure, see README for more informations. ----------- Release 0.6 ----------- 1. Now the apache configuration can be generated with ``.build/venv/bin/generate_controller --generate-apache-config``, it support ``filesystem`` ``cache`` and ``MapCache``. 2. Windows fixes. 3. Use console rewrite (\r) to log generated tiles coordinates. 4. Now if no layers is specified in ``generation:default_layers`` we generate all layers by default. 5. Now bbox to be floats. 6. New ``--get-bbox`` option to get the bbox of a tile. 7. Add coveralls support (https://coveralls.io/r/sbrunner/tilecloud-chain). 8. Add an config option ``generation:error_file`` and a command option ``--tiles`` to store and regenerate errored tiles. ----------- Release 0.5 ----------- 1. SQS config change: .. code:: yaml layers: layer_name: sqs: # The region where the SQS queue is region: eu-west-1 # The SQS queue name, it should already exists queue: the_name 2. Add debug option (``--debug``), please use it to report issue. 3. Now the ``sql`` request can return a set of geometries in a column names geom but the syntax change a little bit => `` AS geom FROM
`` PKwHQƳĆ &tilecloud_chain-1.1.0.dist-info/RECORDtilecloud_chain/OpenLayers-style.css,sha256=4e02b0SZCoLKztFvyTR2Ndqljsly1XTVZnGpp4SW42w,9948 tilecloud_chain/OpenLayers.js,sha256=zqSqKqAdJqAA9Al9o8q8kGO3yuzyDtVRYA-dEbGxgiw,226408 tilecloud_chain/__init__.py,sha256=YUV8QOyDE0RVmgXBD-oinv-84SESmVfEoiCE-F9Qvn8,63999 tilecloud_chain/amazon.py,sha256=MVZc4jp55k9dOKkcbpsCfDcSXeBsq0yInCDfXkSVg6s,15539 tilecloud_chain/controller.py,sha256=igSlrdA3H33V9OQLIsy47qnbOk-Lo6jAUVgGmkDBlQc,19265 tilecloud_chain/copy_.py,sha256=ldrNHFO7E0A8z8m-_UOg-KgPcApYvG9l18yDVMAbqCI,3908 tilecloud_chain/cost.py,sha256=V3-5-19Pw4CQWA7JfXjERcm6wkHzHQ1HRlhKNe8Idiw,11024 tilecloud_chain/expiretiles.py,sha256=OT6D8e4SBhhFRgv_Daa55WKA_hABkp5iKIHtYYu4HKw,4031 tilecloud_chain/format.py,sha256=O4GHqfHXBTgd-QXeCrlAUJCTsYB6XR6nnlJ704LMVko,603 tilecloud_chain/generate.py,sha256=MEDOAceEMHDaZOvvBgeadVS6p7j8WLi4wU3bBtgxEt0,17466 tilecloud_chain/layer-switcher-maximize.png,sha256=Ek4mF612vy5j5wDLxv8ggeIwPMrYxBOo7HHbGYsSG4M,405 tilecloud_chain/layer-switcher-minimize.png,sha256=_BXuoSGTuiA5u70-Z8T5htwYKqLQiu9cqDwMXWXvlJQ,220 tilecloud_chain/mapcache_config_template.py,sha256=E6hKhTr28PfJwCr-iLmwAA7jGSPBNApytv6ynRxNAuE,2871 tilecloud_chain/mapnik_.py,sha256=PaJRw0O4AE8ITChJO6JDh-prV7CGpmsRTg3cRukEqYs,1364 tilecloud_chain/openlayers_html.py,sha256=621L84mLIgegBnJU8PVn0MtFg_SI2Dal0URmOMtJOCw,779 tilecloud_chain/openlayers_js.py,sha256=kxYdqblzEQyrFywOGr3E2DU9803opR6BO7WzefECCxM,2043 tilecloud_chain/server.py,sha256=EhrdNNxy5nAu_0rjlleOyfeTY-GyDxMER1hzijtRp6Y,20494 tilecloud_chain/wmts_get_capabilities_template.py,sha256=M1kWphcCVoGMQu7XyFcTBnaMlfEaHTzN72drvq_Vgtg,5393 tilecloud_chain/scaffolds/__init__.py,sha256=yzPKf7G4FOdfCh0VRyFUXS6rAAq7CbjIW1tpiZN7Tfs,596 tilecloud_chain/scaffolds/create/tilegeneration/+dot+gitignore,sha256=UImFnHRRIHVARVIormo8YD1tl-cRWuGeMCyOtSq9-gg,12 tilecloud_chain/scaffolds/create/tilegeneration/config.yaml.mako_tmpl,sha256=0EX2QYpoXcr6Prque23xsJavstLt2php2-zCtOp-ZaM,4053 tilecloud_chain/views/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 tilecloud_chain-1.1.0.dist-info/DESCRIPTION.rst,sha256=eU7ZyivyHX71IEQ2i5xc4HcpgHVtwSBnFUEgTiL6OzU,27771 tilecloud_chain-1.1.0.dist-info/METADATA,sha256=jgYrjFsL5FUUooyTv9kMZ9enxgbF2cfaMp0HUug6AvM,28687 tilecloud_chain-1.1.0.dist-info/RECORD,, tilecloud_chain-1.1.0.dist-info/WHEEL,sha256=JTb7YztR8fkPg6aSjc571Q4eiVHCwmUDlX8PhuuqIIE,92 tilecloud_chain-1.1.0.dist-info/entry_points.txt,sha256=k7W_zGzSZMtrQBkNgiYOW1jnsu4fxKjKiK4ZbSGPVDk,542 tilecloud_chain-1.1.0.dist-info/metadata.json,sha256=LJjo_Hpck2DTROeOoDNb_PKzqh9A-kkzlFuTcMjMQCg,2020 tilecloud_chain-1.1.0.dist-info/top_level.txt,sha256=E6Xey_8T3wdI-GG3djnFNygA3V8J1sBIb5Y6mlIeyvQ,16 PKirGcktilecloud_chain/expiretiles.pyPK9CeE1tilecloud_chain/wmts_get_capabilities_template.pyPKvBߥ  "[%tilecloud_chain/openlayers_html.pyPKwHR"^(tilecloud_chain/__init__.pyPK:KD4ĉ "tilecloud_chain/openlayers_js.pyPKzkGhtht+tilecloud_chain/OpenLayers.jsPK}ElG7 7 +tilecloud_chain/mapcache_config_template.pyPKvBz+1&&$:tilecloud_chain/OpenLayers-style.cssPKvBb+Xtilecloud_chain/layer-switcher-maximize.pngPKirGݨ++6tilecloud_chain/cost.pyPKwH1W:D:D{tilecloud_chain/generate.pyPKwIdCf[[Ctilecloud_chain/format.pyPKwH챃AKAKFtilecloud_chain/controller.pyPKvB"+tilecloud_chain/layer-switcher-minimize.pngPKwIdCTSTT!tilecloud_chain/mapnik_.pyPKwH2`<<tilecloud_chain/amazon.pyPKirGПDDtilecloud_chain/copy_.pyPKwHyb7PPtilecloud_chain/server.pyPKDEB!V5tilecloud_chain/views/__init__.pyPKirGTT%5tilecloud_chain/scaffolds/__init__.pyPKwIdC' >,8tilecloud_chain/scaffolds/create/tilegeneration/+dot+gitignorePKwHF;E8tilecloud_chain/scaffolds/create/tilegeneration/config.yaml.mako_tmplPKwHTjÕ{l{l/Htilecloud_chain-1.1.0.dist-info/DESCRIPTION.rstPKwH0tilecloud_chain-1.1.0.dist-info/entry_points.txtPKwH_-tilecloud_chain-1.1.0.dist-info/metadata.jsonPKwHK-/tilecloud_chain-1.1.0.dist-info/top_level.txtPKwH''\\%tilecloud_chain-1.1.0.dist-info/WHEELPKwH#zpp()tilecloud_chain-1.1.0.dist-info/METADATAPKwHQƳĆ &~1tilecloud_chain-1.1.0.dist-info/RECORDPKe H<