diff --git a/python/plugins/processing/algs/help/qgis.yaml b/python/plugins/processing/algs/help/qgis.yaml index 27e62942006..b0ba6c4d12b 100644 --- a/python/plugins/processing/algs/help/qgis.yaml +++ b/python/plugins/processing/algs/help/qgis.yaml @@ -509,6 +509,13 @@ qgis:sumlinelengths: > qgis:texttofloat: > This algorithm modifies the type of a given attribute in a vector layer, converting a text attribute containing numeric strings into a numeric attribute. +qgis:tilesxyz: > + This algorithm generates raster XYZ tiles of map canvas content. + + Tile images can be saved as individual images in directory structure, or as single file in MBTiles format. + + Tile size is fixed to 256x256. + qgis:topologicalcoloring: > This algorithm assigns a color index to polygon features in such a way that no adjacent polygons share the same color index, whilst minimizing the number of colors required. diff --git a/python/plugins/processing/algs/qgis/QgisAlgorithmProvider.py b/python/plugins/processing/algs/qgis/QgisAlgorithmProvider.py index 32ddde1ebaa..8a6f5b31ab0 100644 --- a/python/plugins/processing/algs/qgis/QgisAlgorithmProvider.py +++ b/python/plugins/processing/algs/qgis/QgisAlgorithmProvider.py @@ -134,6 +134,7 @@ from .SpatialJoinSummary import SpatialJoinSummary from .StatisticsByCategories import StatisticsByCategories from .SumLines import SumLines from .TextToFloat import TextToFloat +from .TilesXYZ import TilesXYZ from .TinInterpolation import TinInterpolation from .TopoColors import TopoColor from .TruncateTable import TruncateTable @@ -244,6 +245,7 @@ class QgisAlgorithmProvider(QgsProcessingProvider): StatisticsByCategories(), SumLines(), TextToFloat(), + TilesXYZ(), TinInterpolation(), TopoColor(), TruncateTable(), diff --git a/python/plugins/processing/algs/qgis/TilesXYZ.py b/python/plugins/processing/algs/qgis/TilesXYZ.py new file mode 100644 index 00000000000..aa19e6dbf22 --- /dev/null +++ b/python/plugins/processing/algs/qgis/TilesXYZ.py @@ -0,0 +1,393 @@ +# -*- coding: utf-8 -*- + +""" +*************************************************************************** + TilesXYZ.py + --------------------- + Date : April 2019 + Copyright : (C) 2019 by Lutra Consulting Limited + Email : marcel.dancak@lutraconsulting.co.uk +*************************************************************************** +* * +* This program is free software; you can redistribute it and/or modify * +* it under the terms of the GNU General Public License as published by * +* the Free Software Foundation; either version 2 of the License, or * +* (at your option) any later version. * +* * +*************************************************************************** +""" + +__author__ = 'Marcel Dancak' +__date__ = 'April 2019' +__copyright__ = '(C) 2019 by Lutra Consulting Limited' + +# This will get replaced with a git SHA1 when you do a git archive + +__revision__ = '$Format:%H$' + +import os +import math +from uuid import uuid4 + +import ogr +import gdal +from qgis.PyQt.QtCore import QSize, Qt, QByteArray, QBuffer +from qgis.PyQt.QtGui import QColor, QImage, QPainter +from qgis.core import (QgsProcessingException, + QgsProcessingParameterEnum, + QgsProcessingParameterNumber, + QgsProcessingParameterBoolean, + QgsProcessingParameterString, + QgsProcessingParameterExtent, + QgsProcessingOutputFile, + QgsProcessingParameterFileDestination, + QgsProcessingParameterFolderDestination, + QgsGeometry, + QgsRectangle, + QgsMapSettings, + QgsCoordinateTransform, + QgsCoordinateReferenceSystem, + QgsMapRendererCustomPainterJob) +from processing.algs.qgis.QgisAlgorithm import QgisAlgorithm + + +# Math functions taken from https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames #spellok +def deg2num(lat_deg, lon_deg, zoom): + lat_rad = math.radians(lat_deg) + n = 2.0 ** zoom + xtile = int((lon_deg + 180.0) / 360.0 * n) + ytile = int((1.0 - math.log(math.tan(lat_rad) + (1 / math.cos(lat_rad))) / math.pi) / 2.0 * n) + return (xtile, ytile) + + +def num2deg(xtile, ytile, zoom): + n = 2.0 ** zoom + lon_deg = xtile / n * 360.0 - 180.0 + lat_rad = math.atan(math.sinh(math.pi * (1 - 2 * ytile / n))) + lat_deg = math.degrees(lat_rad) + return (lat_deg, lon_deg) + + +class Tile: + def __init__(self, x, y, z): + self.x = x + self.y = y + self.z = z + + def extent(self): + lat1, lon1 = num2deg(self.x, self.y, self.z) + lat2, lon2 = num2deg(self.x + 1, self.y + 1, self.z) + return [lon1, lat2, lon2, lat1] + + +class MetaTile: + def __init__(self): + # list of tuple(row index, column index, Tile) + self.tiles = [] + + def add_tile(self, row, column, tile): + self.tiles.append((row, column, tile)) + + def rows(self): + return max([r for r, _, _ in self.tiles]) + 1 + + def columns(self): + return max([c for _, c, _ in self.tiles]) + 1 + + def extent(self): + _, _, first = self.tiles[0] + _, _, last = self.tiles[-1] + lat1, lon1 = num2deg(first.x, first.y, first.z) + lat2, lon2 = num2deg(last.x + 1, last.y + 1, first.z) + return [lon1, lat2, lon2, lat1] + + +def get_metatiles(extent, zoom, size=4): + west_edge, south_edge, east_edge, north_edge = extent + left_tile, top_tile = deg2num(north_edge, west_edge, zoom) + right_tile, bottom_tile = deg2num(south_edge, east_edge, zoom) + + metatiles = {} + for i, x in enumerate(range(left_tile, right_tile + 1)): + for j, y in enumerate(range(top_tile, bottom_tile + 1)): + meta_key = '{}:{}'.format(int(i / size), int(j / size)) + if meta_key not in metatiles: + metatiles[meta_key] = MetaTile() + metatile = metatiles[meta_key] + metatile.add_tile(i % size, j % size, Tile(x, y, zoom)) + + return list(metatiles.values()) + + +class DirectoryWriter: + def __init__(self, folder, tile_params): + self.folder = folder + self.format = tile_params.get('format', 'PNG') + self.quality = tile_params.get('quality', -1) + + def writeTile(self, tile, image): + directory = os.path.join(self.folder, str(tile.z), str(tile.x)) + os.makedirs(directory, exist_ok=True) + path = os.path.join(directory, '{}.{}'.format(tile.y, self.format.lower())) + image.save(path, self.format, self.quality) + return path + + def close(self): + pass + + +class MBTilesWriter: + def __init__(self, filename, tile_params, extent, min_zoom, max_zoom): + base_dir = os.path.dirname(filename) + os.makedirs(base_dir, exist_ok=True) + self.filename = filename + self.extent = extent + self.tile_width = tile_params.get('width', 256) + self.tile_height = tile_params.get('height', 256) + tile_format = tile_params['format'] + if tile_format == 'JPG': + tile_format = 'JPEG' + options = ['QUALITY=%s' % tile_params.get('quality', 75)] + else: + options = ['ZLEVEL=8'] + driver = gdal.GetDriverByName('MBTiles') + ds = driver.Create(filename, 1, 1, 1, options=['TILE_FORMAT=%s' % tile_format] + options) + ds = None + sqlite_driver = ogr.GetDriverByName('SQLite') + ds = sqlite_driver.Open(filename, 1) + ds.ExecuteSQL("INSERT INTO metadata(name, value) VALUES ('{}', '{}');".format('minzoom', min_zoom)) + ds.ExecuteSQL("INSERT INTO metadata(name, value) VALUES ('{}', '{}');".format('maxzoom', max_zoom)) + # will be set properly after writing all tiles + ds.ExecuteSQL("INSERT INTO metadata(name, value) VALUES ('{}', '');".format('bounds')) + ds = None + self._zoom = None + + def _initZoomLayer(self, zoom): + west_edge, south_edge, east_edge, north_edge = self.extent + first_tile = Tile(*deg2num(north_edge, west_edge, zoom), zoom) + last_tile = Tile(*deg2num(south_edge, east_edge, zoom), zoom) + + first_tile_extent = first_tile.extent() + last_tile_extent = last_tile.extent() + zoom_extent = [ + first_tile_extent[0], + last_tile_extent[1], + last_tile_extent[2], + first_tile_extent[3] + ] + + sqlite_driver = ogr.GetDriverByName('SQLite') + ds = sqlite_driver.Open(self.filename, 1) + bounds = ','.join(map(str, zoom_extent)) + ds.ExecuteSQL("UPDATE metadata SET value='{}' WHERE name='bounds'".format(bounds)) + ds = None + + self._zoomDs = gdal.OpenEx(self.filename, 1, open_options=['ZOOM_LEVEL=%s' % first_tile.z]) + self._first_tile = first_tile + self._zoom = zoom + + def writeTile(self, tile, image): + if tile.z != self._zoom: + self._initZoomLayer(tile.z) + + data = QByteArray() + buff = QBuffer(data) + image.save(buff, 'PNG') + + mmap_name = '/vsimem/' + uuid4().hex + gdal.FileFromMemBuffer(mmap_name, data.data()) + gdal_dataset = gdal.Open(mmap_name) + data = gdal_dataset.ReadRaster(0, 0, self.tile_width, self.tile_height) + gdal_dataset = None + gdal.Unlink(mmap_name) + + xoff = (tile.x - self._first_tile.x) * self.tile_width + yoff = (tile.y - self._first_tile.y) * self.tile_height + self._zoomDs.WriteRaster(xoff, yoff, self.tile_width, self.tile_height, data) + + def close(self): + self._zoomDs = None + sqlite_driver = ogr.GetDriverByName('SQLite') + ds = sqlite_driver.Open(self.filename, 1) + bounds = ','.join(map(str, self.extent)) + ds.ExecuteSQL("UPDATE metadata SET value='{}' WHERE name='bounds'".format(bounds)) + ds = None + + +class TilesXYZ(QgisAlgorithm): + EXTENT = 'EXTENT' + ZOOM_MIN = 'ZOOM_MIN' + ZOOM_MAX = 'ZOOM_MAX' + TILE_FORMAT = 'TILE_FORMAT' + DPI = 'DPI' + OUTPUT_FORMAT = 'OUTPUT_FORMAT' + OUTPUT_DIRECTORY = 'OUTPUT_DIRECTORY' + OUTPUT_FILE = 'OUTPUT_FILE' + + def group(self): + return self.tr('Raster tools') + + def groupId(self): + return 'rastertools' + + def initAlgorithm(self, config=None): + self.addParameter(QgsProcessingParameterExtent(self.EXTENT, self.tr('Extent'))) + self.addParameter(QgsProcessingParameterNumber(self.ZOOM_MIN, + self.tr('Minimum zoom'), + minValue=0, + maxValue=25, + defaultValue=12)) + self.addParameter(QgsProcessingParameterNumber(self.ZOOM_MAX, + self.tr('Maximum zoom'), + minValue=0, + maxValue=25, + defaultValue=12)) + self.addParameter(QgsProcessingParameterNumber(self.DPI, + self.tr('DPI'), + minValue=48, + maxValue=600, + defaultValue=96)) + self.formats = ['PNG', 'JPG'] + self.addParameter(QgsProcessingParameterEnum(self.TILE_FORMAT, + self.tr('Tile format'), + self.formats, + defaultValue=0)) + self.outputs = ['Directory', 'MBTiles'] + self.addParameter(QgsProcessingParameterEnum(self.OUTPUT_FORMAT, + self.tr('Output format'), + self.outputs, + defaultValue=0)) + self.addParameter(QgsProcessingParameterFolderDestination(self.OUTPUT_DIRECTORY, + self.tr('Output directory'), + optional=True)) + self.addParameter(QgsProcessingParameterFileDestination(self.OUTPUT_FILE, + self.tr('Output file (for MBTiles)'), + self.tr('MBTiles files (*.mbtiles)'), + optional=True)) + + def name(self): + return 'tilesxyz' + + def displayName(self): + return self.tr('Generate XYZ tiles') + + def prepareAlgorithm(self, parameters, context, feedback): + project = context.project() + visible_layers = [item.layer() for item in project.layerTreeRoot().findLayers() if item.isVisible()] + self.layers = [l for l in project.layerTreeRoot().layerOrder() if l in visible_layers] + return True + + def processAlgorithm(self, parameters, context, feedback): + feedback.setProgress(1) + + extent = self.parameterAsExtent(parameters, self.EXTENT, context) + min_zoom = self.parameterAsInt(parameters, self.ZOOM_MIN, context) + max_zoom = self.parameterAsInt(parameters, self.ZOOM_MAX, context) + dpi = self.parameterAsInt(parameters, self.DPI, context) + tile_format = self.formats[self.parameterAsEnum(parameters, self.TILE_FORMAT, context)] + output_format = self.outputs[self.parameterAsEnum(parameters, self.OUTPUT_FORMAT, context)] + if output_format == 'Directory': + output_dir = self.parameterAsString(parameters, self.OUTPUT_DIRECTORY, context) + if not output_dir: + raise QgsProcessingException(self.tr('You need to specify output directory.')) + else: # MBTiles + output_file = self.parameterAsString(parameters, self.OUTPUT_FILE, context) + if not output_file: + raise QgsProcessingException(self.tr('You need to specify output filename.')) + tile_width = 256 + tile_height = 256 + + wgs_crs = QgsCoordinateReferenceSystem('EPSG:4326') + dest_crs = QgsCoordinateReferenceSystem('EPSG:3857') + + project = context.project() + src_to_wgs = QgsCoordinateTransform(project.crs(), wgs_crs, context.transformContext()) + wgs_to_dest = QgsCoordinateTransform(wgs_crs, dest_crs, context.transformContext()) + + settings = QgsMapSettings() + settings.setOutputImageFormat(QImage.Format_ARGB32_Premultiplied) + settings.setDestinationCrs(dest_crs) + settings.setLayers(self.layers) + settings.setOutputDpi(dpi) + + wgs_extent = src_to_wgs.transformBoundingBox(extent) + wgs_extent = [wgs_extent.xMinimum(), wgs_extent.yMinimum(), wgs_extent.xMaximum(), wgs_extent.yMaximum()] + + metatiles_by_zoom = {} + metatiles_count = 0 + for zoom in range(min_zoom, max_zoom + 1): + metatiles = get_metatiles(wgs_extent, zoom, 4) + metatiles_by_zoom[zoom] = metatiles + metatiles_count += len(metatiles) + + lab_buffer_px = 100 + progress = 0 + + tile_params = { + 'format': tile_format, + 'quality': 75, + 'width': tile_width, + 'height': tile_height + } + if output_format == 'Directory': + writer = DirectoryWriter(output_dir, tile_params) + else: + writer = MBTilesWriter(output_file, tile_params, wgs_extent, min_zoom, max_zoom) + + for zoom in range(min_zoom, max_zoom + 1): + feedback.pushConsoleInfo('Generating tiles for zoom level: %s' % zoom) + + for i, metatile in enumerate(metatiles_by_zoom[zoom]): + size = QSize(tile_width * metatile.rows(), tile_height * metatile.columns()) + extent = QgsRectangle(*metatile.extent()) + settings.setExtent(wgs_to_dest.transformBoundingBox(extent)) + settings.setOutputSize(size) + + label_area = QgsRectangle(settings.extent()) + lab_buffer = label_area.width() * (lab_buffer_px / size.width()) + label_area.set( + label_area.xMinimum() + lab_buffer, + label_area.yMinimum() + lab_buffer, + label_area.xMaximum() - lab_buffer, + label_area.yMaximum() - lab_buffer + ) + settings.setLabelBoundaryGeometry(QgsGeometry.fromRect(label_area)) + + image = QImage(size, QImage.Format_ARGB32_Premultiplied) + image.fill(Qt.transparent) + dpm = settings.outputDpi() / 25.4 * 1000 + image.setDotsPerMeterX(dpm) + image.setDotsPerMeterY(dpm) + painter = QPainter(image) + job = QgsMapRendererCustomPainterJob(settings, painter) + job.renderSynchronously() + painter.end() + + # For analysing metatiles (labels, etc.) + # metatile_dir = os.path.join(output_dir, str(zoom)) + # os.makedirs(metatile_dir, exist_ok=True) + # image.save(os.path.join(metatile_dir, 'metatile_%s.png' % i)) + + for r, c, tile in metatile.tiles: + tile_img = image.copy(tile_width * r, tile_height * c, tile_width, tile_height) + writer.writeTile(tile, tile_img) + + progress += 1 + feedback.setProgress(100 * (progress / metatiles_count)) + + writer.close() + + results = {} + if output_format == 'Directory': + results['OUTPUT_DIRECTORY'] = output_dir + else: # MBTiles + results['OUTPUT_FILE'] = output_file + return results + + def checkParameterValues(self, parameters, context): + min_zoom = self.parameterAsInt(parameters, self.ZOOM_MIN, context) + max_zoom = self.parameterAsInt(parameters, self.ZOOM_MAX, context) + if max_zoom < min_zoom: + return False, self.tr('Invalid zoom levels range.') + + return super().checkParameterValues(parameters, context) diff --git a/python/plugins/processing/tests/AlgorithmsTestBase.py b/python/plugins/processing/tests/AlgorithmsTestBase.py index 62d0df7f8a0..78d39491e57 100644 --- a/python/plugins/processing/tests/AlgorithmsTestBase.py +++ b/python/plugins/processing/tests/AlgorithmsTestBase.py @@ -86,7 +86,12 @@ class AlgorithmsTest(object): :param defs: A python dict containing a test algorithm definition """ self.vector_layer_params = {} - QgsProject.instance().removeAllMapLayers() + QgsProject.instance().clear() + + if 'project' in defs: + full_project_path = os.path.join(processingTestDataPath(), defs['project']) + project_read_success = QgsProject.instance().read(full_project_path) + self.assertTrue(project_read_success, 'Failed to load project file: ' + defs['project']) if 'project_crs' in defs: QgsProject.instance().setCrs(QgsCoordinateReferenceSystem(defs['project_crs'])) @@ -212,6 +217,9 @@ class AlgorithmsTest(object): basename = 'raster.tif' filepath = os.path.join(outdir, basename) return filepath + elif param['type'] == 'directory': + outdir = tempfile.mkdtemp() + return outdir raise KeyError("Unknown type '{}' specified for parameter".format(param['type'])) @@ -350,6 +358,11 @@ class AlgorithmsTest(object): result_filepath = results[id] self.assertFilesEqual(expected_filepath, result_filepath) + elif 'directory' == expected_result['type']: + expected_dirpath = self.filepath_from_param(expected_result) + result_dirpath = results[id] + + self.assertDirectoriesEqual(expected_dirpath, result_dirpath) elif 'regex' == expected_result['type']: with open(results[id], 'r') as file: data = file.read() diff --git a/python/plugins/processing/tests/README.md b/python/plugins/processing/tests/README.md index db46027f42e..7b884feb8ef 100644 --- a/python/plugins/processing/tests/README.md +++ b/python/plugins/processing/tests/README.md @@ -194,6 +194,25 @@ OUTPUT: - 'Feature Count: 6' ``` +#### Directories + +You can compare the content of an output directory by en expected result reference directory + +```yaml +OUTPUT_DIR: + name: expected/tiles_xyz/test_1 + type: directory +``` + +### Algorithm Context + +There are few more definitions that can modify context of the algorithm - these can be specified at top level of test: + +- `project` - will load a specified QGIS project file before running the algorithm. If not specified, algorithm will run with empty project +- `project_crs` - overrides the default project CRS - e.g. `EPSG:27700` +- `ellipsoid` - overrides the default project ellipsoid used for measurements - e.g. `GRS80` + + Running tests locally ------------------ ```bash diff --git a/python/plugins/processing/tests/testdata/expected/xyztiles/1/0/0.png b/python/plugins/processing/tests/testdata/expected/xyztiles/1/0/0.png new file mode 100644 index 00000000000..1f5a65a7fb1 Binary files /dev/null and b/python/plugins/processing/tests/testdata/expected/xyztiles/1/0/0.png differ diff --git a/python/plugins/processing/tests/testdata/expected/xyztiles/2/0/1.png b/python/plugins/processing/tests/testdata/expected/xyztiles/2/0/1.png new file mode 100644 index 00000000000..dc244915e53 Binary files /dev/null and b/python/plugins/processing/tests/testdata/expected/xyztiles/2/0/1.png differ diff --git a/python/plugins/processing/tests/testdata/expected/xyztiles/2/1/1.png b/python/plugins/processing/tests/testdata/expected/xyztiles/2/1/1.png new file mode 100644 index 00000000000..fb3e28c5545 Binary files /dev/null and b/python/plugins/processing/tests/testdata/expected/xyztiles/2/1/1.png differ diff --git a/python/plugins/processing/tests/testdata/expected/xyztiles/3/1/2.png b/python/plugins/processing/tests/testdata/expected/xyztiles/3/1/2.png new file mode 100644 index 00000000000..e256b4f5ce7 Binary files /dev/null and b/python/plugins/processing/tests/testdata/expected/xyztiles/3/1/2.png differ diff --git a/python/plugins/processing/tests/testdata/expected/xyztiles/3/1/3.png b/python/plugins/processing/tests/testdata/expected/xyztiles/3/1/3.png new file mode 100644 index 00000000000..8fc2550273a Binary files /dev/null and b/python/plugins/processing/tests/testdata/expected/xyztiles/3/1/3.png differ diff --git a/python/plugins/processing/tests/testdata/expected/xyztiles/3/2/2.png b/python/plugins/processing/tests/testdata/expected/xyztiles/3/2/2.png new file mode 100644 index 00000000000..c030145059c Binary files /dev/null and b/python/plugins/processing/tests/testdata/expected/xyztiles/3/2/2.png differ diff --git a/python/plugins/processing/tests/testdata/expected/xyztiles/3/2/3.png b/python/plugins/processing/tests/testdata/expected/xyztiles/3/2/3.png new file mode 100644 index 00000000000..31d5ea48180 Binary files /dev/null and b/python/plugins/processing/tests/testdata/expected/xyztiles/3/2/3.png differ diff --git a/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml b/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml index 4105e6d5245..b71d9298cb5 100755 --- a/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml +++ b/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml @@ -7524,5 +7524,21 @@ tests: name: expected/join_to_nearest_no_matches.gml type: vector + - name: Generate XYZ tiles + algorithm: qgis:tilesxyz + project: ../../../../../tests/testdata/xyztiles.qgs + project_crs: EPSG:3857 + params: + EXTENT: -12535000,-9883000,3360000,5349000 [EPSG:3857] + ZOOM_MIN: 1 + ZOOM_MAX: 3 + TILE_FORMAT: 0 # png + OUTPUT_FORMAT: 0 # directory + + results: + OUTPUT_DIRECTORY: + type: directory + name: expected/xyztiles + # See ../README.md for a description of the file format diff --git a/python/testing/__init__.py b/python/testing/__init__.py index 4f648db2b49..fbe5e1c9b68 100644 --- a/python/testing/__init__.py +++ b/python/testing/__init__.py @@ -29,6 +29,7 @@ import os import sys import difflib import functools +import filecmp from qgis.PyQt.QtCore import QVariant from qgis.core import QgsApplication, QgsFeatureRequest, NULL @@ -196,6 +197,20 @@ class TestCase(_TestCase): diff = list(diff) self.assertEqual(0, len(diff), ''.join(diff)) + def assertDirectoriesEqual(self, dirpath_expected, dirpath_result): + """ Checks whether both directories have the same content (recursively) and raises an assertion error if not. """ + dc = filecmp.dircmp(dirpath_expected, dirpath_result) + dc.report_full_closure() + + def _check_dirs_equal_recursive(dcmp): + self.assertEqual(dcmp.left_only, []) + self.assertEqual(dcmp.right_only, []) + self.assertEqual(dcmp.diff_files, []) + for sub_dcmp in dcmp.subdirs.values(): + _check_dirs_equal_recursive(sub_dcmp) + + _check_dirs_equal_recursive(dc) + def assertGeometriesEqual(self, geom0, geom1, geom0_id='geometry 1', geom1_id='geometry 2', precision=14, topo_equal_check=False): self.checkGeometriesEqual(geom0, geom1, geom0_id, geom1_id, use_asserts=True, precision=precision, topo_equal_check=topo_equal_check) diff --git a/tests/testdata/xyztiles.qgs b/tests/testdata/xyztiles.qgs new file mode 100644 index 00000000000..2aa9d433b4a --- /dev/null +++ b/tests/testdata/xyztiles.qgs @@ -0,0 +1,744 @@ + + + + + + + + + + +proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs + 3857 + 3857 + EPSG:3857 + WGS 84 / Pseudo-Mercator + merc + WGS84 + false + + + + + + + + + + + + polys20151123133114244 + lines20151123133101198 + + + + + + + + + + + meters + + -12244360.81901275180280209 + 3360185.32420947728678584 + -10173775.61578787304461002 + 5348862.70644816849380732 + + 0 + + + +proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs + 3857 + 3857 + EPSG:3857 + WGS 84 / Pseudo-Mercator + merc + WGS84 + false + + + 0 + + + + + + + + + + + + + + + + + -117.62319839219053108 + 23.20820580488508966 + -82.32264950769274492 + 46.18290982947509349 + + lines20151123133101198 + ./lines.shp + + + + Roads + + + +proj=longlat +datum=WGS84 +no_defs + 3452 + 4326 + EPSG:4326 + WGS 84 + longlat + WGS84 + true + + + + + + + + + + + + + + + + 0 + 0 + + + + + false + + + + + ogr + + + + + + + + + + + 1 + 1 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 0 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + ../src/quickgui/app/qgis-data + + 0 + generatedlayout + + + + COALESCE( "Name", '<NULL>' ) + + + + + -118.92286230599032137 + 24.50786971868489061 + -83.79001199101509201 + 46.72617265077044379 + + polys20151123133114244 + ./polys.shp + + + + Land + + + +proj=longlat +datum=WGS84 +no_defs + 3452 + 4326 + EPSG:4326 + WGS 84 + longlat + WGS84 + true + + + + + + + + + + + + + + + + 0 + 0 + + + + + false + + + + + ogr + + + + + + + + + + + 1 + 1 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 0 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + ../src/quickgui/app/qgis-data + + 0 + generatedlayout + + + + COALESCE( "Name", '<NULL>' ) + + + + + + + + + + 255 + + + + + 1 + true + + + + 20 + 1 + to vertex and segment + + to_vertex_and_segment + to_vertex_and_segment + to_vertex_and_segment + + + enabled + enabled + enabled + + + lines20151123133101198 + points20151123133104693 + polys20151123133114244 + + + 20.000000 + 20.000000 + 20.000000 + + + 1 + 1 + 1 + + current_layer + + + 255 + 255 + 255 + 255 + 0 + 255 + 255 + + + + + + false + + + + + + WGS84 + + + m2 + meters + + + 50 + 16 + 30 + false + 0 + false + false + true + 0 + + + false + + + true + 2 + MU + + + 3452 + +proj=longlat +datum=WGS84 +no_defs + EPSG:4326 + 1 + + + + + + + + + + + + + + + None + false + + + + + + conditions unknown + 90 + + + + 1 + + 8 + + false + + false + + + false + + + + + + + + false + + + + + false + + 5000 + + + + false + + + + + + + + + + + + + + + + + + + + + + + + + + + 2000-01-01T00:00:00 + + + +