mirror of
https://github.com/qgis/QGIS.git
synced 2025-12-15 00:07:25 -05:00
Merge pull request #54321 from alexbruy/processing-port-tile-xyz
[processing] port XYZ tiles algorithms
This commit is contained in:
commit
4401ac792b
@ -233,16 +233,6 @@ qgis:statisticsbycategories: >
|
||||
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:tilesxyzdirectory: >
|
||||
This algorithm generates raster XYZ tiles of map canvas content.
|
||||
|
||||
Tile images are saved as individual images in directory structure.
|
||||
|
||||
qgis:tilesxyzmbtiles: >
|
||||
This algorithm generates raster XYZ tiles of map canvas content.
|
||||
|
||||
Tile images are saved as a single file in the “MBTiles” format.
|
||||
|
||||
qgis:tininterpolation: >
|
||||
Generates a Triangulated Irregular Network (TIN) interpolation of a point vector layer.
|
||||
|
||||
|
||||
@ -74,7 +74,6 @@ from .SetVectorStyle import SetVectorStyle
|
||||
from .SpatialJoinSummary import SpatialJoinSummary
|
||||
from .StatisticsByCategories import StatisticsByCategories
|
||||
from .TextToFloat import TextToFloat
|
||||
from .TilesXYZ import TilesXYZAlgorithmDirectory, TilesXYZAlgorithmMBTiles
|
||||
from .TinInterpolation import TinInterpolation
|
||||
from .TopoColors import TopoColor
|
||||
from .UniqueValues import UniqueValues
|
||||
@ -140,8 +139,6 @@ class QgisAlgorithmProvider(QgsProcessingProvider):
|
||||
SpatialJoinSummary(),
|
||||
StatisticsByCategories(),
|
||||
TextToFloat(),
|
||||
TilesXYZAlgorithmDirectory(),
|
||||
TilesXYZAlgorithmMBTiles(),
|
||||
TinInterpolation(),
|
||||
TopoColor(),
|
||||
UniqueValues(),
|
||||
|
||||
@ -1,669 +0,0 @@
|
||||
"""
|
||||
***************************************************************************
|
||||
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'
|
||||
|
||||
import os
|
||||
import math
|
||||
import re
|
||||
from typing import List, Tuple
|
||||
import urllib.parse
|
||||
from uuid import uuid4
|
||||
|
||||
import sqlite3
|
||||
from osgeo 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,
|
||||
QgsProcessingParameterColor,
|
||||
QgsProcessingOutputFile,
|
||||
QgsProcessingParameterDefinition,
|
||||
QgsProcessingParameterFileDestination,
|
||||
QgsProcessingParameterFolderDestination,
|
||||
QgsGeometry,
|
||||
QgsRectangle,
|
||||
QgsMapSettings,
|
||||
QgsCoordinateTransform,
|
||||
QgsCoordinateReferenceSystem,
|
||||
QgsMapRendererCustomPainterJob,
|
||||
QgsLabelingEngineSettings,
|
||||
QgsApplication,
|
||||
QgsExpressionContextUtils,
|
||||
QgsProcessingAlgorithm)
|
||||
from processing.algs.qgis.QgisAlgorithm import QgisAlgorithm
|
||||
import threading
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from processing.core.ProcessingConfig import ProcessingConfig
|
||||
|
||||
|
||||
# TMS functions taken from https://alastaira.wordpress.com/2011/07/06/converting-tms-tile-coordinates-to-googlebingosm-tile-coordinates/ #spellok
|
||||
def tms(ytile: float, zoom: int) -> int:
|
||||
n = 2.0 ** zoom
|
||||
ytile = n - ytile - 1
|
||||
return int(ytile)
|
||||
|
||||
|
||||
# Math functions taken from https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames #spellok
|
||||
def deg2num(lat_deg: float, lon_deg: float, zoom: int) -> Tuple[int, int]:
|
||||
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)
|
||||
|
||||
|
||||
# Math functions taken from https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames #spellok
|
||||
def num2deg(xtile: int, ytile: int, zoom: int) -> Tuple[float, float]:
|
||||
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: int, y: int, z: int):
|
||||
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: List[Tuple[int, int, Tile]] = []
|
||||
|
||||
def add_tile(self, row: int, column: int, tile: Tile):
|
||||
self.tiles.append((row, column, tile))
|
||||
|
||||
def rows(self) -> int:
|
||||
return max([r for r, _, _ in self.tiles]) + 1
|
||||
|
||||
def columns(self) -> int:
|
||||
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_tile_based_extent(extent: Tuple[float, float, float, float], zoom: int) -> Tuple[int, int, int, int]:
|
||||
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)
|
||||
return left_tile, top_tile, right_tile, bottom_tile
|
||||
|
||||
|
||||
def get_metatiles(extent: Tuple[float, float, float, float], zoom: int, size=4):
|
||||
left_tile, top_tile, right_tile, bottom_tile = get_tile_based_extent(extent, zoom)
|
||||
|
||||
meta_row_max = int((bottom_tile - top_tile) / size) + 1
|
||||
meta_col_max = int((right_tile - left_tile) / size) + 1
|
||||
for meta_col in range(meta_col_max):
|
||||
for meta_row in range(meta_row_max):
|
||||
metatile = MetaTile()
|
||||
for i in range(size):
|
||||
for j in range(size):
|
||||
x = left_tile + meta_col * size + i
|
||||
y = top_tile + meta_row * size + j
|
||||
if x <= right_tile and y <= bottom_tile:
|
||||
metatile.add_tile(i, j, Tile(x, y, zoom))
|
||||
yield metatile
|
||||
|
||||
|
||||
def get_metatiles_count(extent: Tuple[float, float, float, float], zoom: int, size=4) -> int:
|
||||
left_tile, top_tile, right_tile, bottom_tile = get_tile_based_extent(extent, zoom)
|
||||
|
||||
meta_row_max = int((bottom_tile - top_tile) / size) + 1
|
||||
meta_col_max = int((right_tile - left_tile) / size) + 1
|
||||
return meta_row_max * meta_col_max
|
||||
|
||||
|
||||
class TilesXYZAlgorithmBase(QgisAlgorithm):
|
||||
EXTENT = 'EXTENT'
|
||||
ZOOM_MIN = 'ZOOM_MIN'
|
||||
ZOOM_MAX = 'ZOOM_MAX'
|
||||
DPI = 'DPI'
|
||||
BACKGROUND_COLOR = 'BACKGROUND_COLOR'
|
||||
ANTIALIAS = 'ANTIALIAS'
|
||||
TILE_FORMAT = 'TILE_FORMAT'
|
||||
QUALITY = 'QUALITY'
|
||||
METATILESIZE = 'METATILESIZE'
|
||||
|
||||
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.addParameter(QgsProcessingParameterColor(self.BACKGROUND_COLOR,
|
||||
self.tr('Background color'),
|
||||
defaultValue=QColor(Qt.transparent),
|
||||
optional=True))
|
||||
self.addParameter(QgsProcessingParameterBoolean(self.ANTIALIAS,
|
||||
self.tr('Enable antialiasing'),
|
||||
defaultValue=True))
|
||||
self.formats = ['PNG', 'JPG']
|
||||
self.addParameter(QgsProcessingParameterEnum(self.TILE_FORMAT,
|
||||
self.tr('Tile format'),
|
||||
self.formats,
|
||||
defaultValue=0))
|
||||
self.addParameter(QgsProcessingParameterNumber(self.QUALITY,
|
||||
self.tr('Quality (JPG only)'),
|
||||
minValue=1,
|
||||
maxValue=100,
|
||||
defaultValue=75))
|
||||
self.addParameter(QgsProcessingParameterNumber(self.METATILESIZE,
|
||||
self.tr('Metatile size'),
|
||||
minValue=1,
|
||||
maxValue=20,
|
||||
defaultValue=4))
|
||||
self.thread_nr_re = re.compile('[0-9]+$') # thread number regex
|
||||
|
||||
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 renderSingleMetatile(self, metatile: MetaTile):
|
||||
if self.feedback.isCanceled():
|
||||
return
|
||||
# Haven't found a better way to break than to make all the new threads return instantly
|
||||
|
||||
if "Dummy" in threading.current_thread().name or len(self.settingsDictionary) == 1: # single thread testing
|
||||
threadSpecificSettings = list(self.settingsDictionary.values())[0]
|
||||
else:
|
||||
thread_nr = self.thread_nr_re.search(threading.current_thread().name)[0] # terminating number only
|
||||
threadSpecificSettings = self.settingsDictionary[thread_nr]
|
||||
|
||||
size = QSize(self.tile_width * metatile.rows(), self.tile_height * metatile.columns())
|
||||
extent = QgsRectangle(*metatile.extent())
|
||||
threadSpecificSettings.setExtent(self.wgs_to_dest.transformBoundingBox(extent))
|
||||
threadSpecificSettings.setOutputSize(size)
|
||||
|
||||
# Append MapSettings scope in order to update map variables (e.g @map_scale) with new extent data
|
||||
exp_context = threadSpecificSettings.expressionContext()
|
||||
exp_context.appendScope(QgsExpressionContextUtils.mapSettingsScope(threadSpecificSettings))
|
||||
threadSpecificSettings.setExpressionContext(exp_context)
|
||||
|
||||
image = QImage(size, QImage.Format_ARGB32_Premultiplied)
|
||||
image.fill(self.color)
|
||||
dpm = round(threadSpecificSettings.outputDpi() / 25.4 * 1000)
|
||||
image.setDotsPerMeterX(dpm)
|
||||
image.setDotsPerMeterY(dpm)
|
||||
painter = QPainter(image)
|
||||
job = QgsMapRendererCustomPainterJob(threadSpecificSettings, 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:
|
||||
tileImage = image.copy(self.tile_width * r, self.tile_height * c, self.tile_width, self.tile_height)
|
||||
self.writer.write_tile(tile, tileImage)
|
||||
|
||||
# to stop thread sync issues
|
||||
with self.progressThreadLock:
|
||||
self.progress += 1
|
||||
self.feedback.setProgress(100 * (self.progress / self.totalMetatiles))
|
||||
self.feedback.setProgressText(self.tr('Generated: {progress}/{total} metatiles').format(progress=self.progress, total=self.totalMetatiles))
|
||||
|
||||
def generate(self, writer, parameters, context, feedback):
|
||||
self.feedback = feedback
|
||||
feedback.setProgress(1)
|
||||
|
||||
project = context.project()
|
||||
|
||||
# Transform extent to match project CRS
|
||||
extent_original = self.parameterAsExtent(parameters, self.EXTENT, context)
|
||||
extent_crs = self.parameterAsExtentCrs(parameters, self.EXTENT, context)
|
||||
extent_crs_to_project_crs = QgsCoordinateTransform(extent_crs, project.crs(), context.transformContext())
|
||||
extent = extent_crs_to_project_crs.transformBoundingBox(extent_original)
|
||||
|
||||
self.min_zoom = self.parameterAsInt(parameters, self.ZOOM_MIN, context)
|
||||
self.max_zoom = self.parameterAsInt(parameters, self.ZOOM_MAX, context)
|
||||
dpi = self.parameterAsInt(parameters, self.DPI, context)
|
||||
self.color = self.parameterAsColor(parameters, self.BACKGROUND_COLOR, context)
|
||||
self.antialias = self.parameterAsBoolean(parameters, self.ANTIALIAS, context)
|
||||
self.tile_format = self.formats[self.parameterAsEnum(parameters, self.TILE_FORMAT, context)]
|
||||
self.quality = self.parameterAsInt(parameters, self.QUALITY, context)
|
||||
self.metatilesize = self.parameterAsInt(parameters, self.METATILESIZE, context)
|
||||
self.maxThreads = context.maximumThreads()
|
||||
try:
|
||||
self.tile_width = self.parameterAsInt(parameters, self.TILE_WIDTH, context)
|
||||
self.tile_height = self.parameterAsInt(parameters, self.TILE_HEIGHT, context)
|
||||
except AttributeError:
|
||||
self.tile_width = 256
|
||||
self.tile_height = 256
|
||||
|
||||
wgs_crs = QgsCoordinateReferenceSystem('EPSG:4326')
|
||||
dest_crs = QgsCoordinateReferenceSystem('EPSG:3857')
|
||||
|
||||
self.src_to_wgs = QgsCoordinateTransform(project.crs(), wgs_crs, context.transformContext())
|
||||
self.wgs_to_dest = QgsCoordinateTransform(wgs_crs, dest_crs, context.transformContext())
|
||||
# without re-writing, we need a different settings for each thread to stop async errors
|
||||
# naming doesn't always line up, but the last number does
|
||||
self.settingsDictionary = {str(i): QgsMapSettings() for i in range(self.maxThreads)}
|
||||
for thread in self.settingsDictionary:
|
||||
self.settingsDictionary[thread].setOutputImageFormat(QImage.Format_ARGB32_Premultiplied)
|
||||
self.settingsDictionary[thread].setTransformContext(context.transformContext())
|
||||
self.settingsDictionary[thread].setDestinationCrs(dest_crs)
|
||||
self.settingsDictionary[thread].setLayers(self.layers)
|
||||
self.settingsDictionary[thread].setOutputDpi(dpi)
|
||||
if self.tile_format == 'PNG':
|
||||
self.settingsDictionary[thread].setBackgroundColor(self.color)
|
||||
self.settingsDictionary[thread].setFlag(QgsMapSettings.Antialiasing, self.antialias)
|
||||
|
||||
# disable partial labels (they would be cut at the edge of tiles)
|
||||
labeling_engine_settings = self.settingsDictionary[thread].labelingEngineSettings()
|
||||
labeling_engine_settings.setFlag(QgsLabelingEngineSettings.UsePartialCandidates, False)
|
||||
self.settingsDictionary[thread].setLabelingEngineSettings(labeling_engine_settings)
|
||||
|
||||
# Transfer context scopes to MapSettings
|
||||
self.settingsDictionary[thread].setExpressionContext(context.expressionContext())
|
||||
|
||||
self.wgs_extent = self.src_to_wgs.transformBoundingBox(extent)
|
||||
self.wgs_extent = [self.wgs_extent.xMinimum(), self.wgs_extent.yMinimum(), self.wgs_extent.xMaximum(),
|
||||
self.wgs_extent.yMaximum()]
|
||||
|
||||
metatiles_by_zoom = {}
|
||||
self.totalMetatiles = 0
|
||||
for zoom in range(self.min_zoom, self.max_zoom + 1):
|
||||
metatiles = get_metatiles(self.wgs_extent, zoom, self.metatilesize)
|
||||
metatiles_by_zoom[zoom] = metatiles
|
||||
self.totalMetatiles += get_metatiles_count(self.wgs_extent, zoom, self.metatilesize)
|
||||
|
||||
self.progress = 0
|
||||
|
||||
tile_params = {
|
||||
'format': self.tile_format,
|
||||
'quality': self.quality,
|
||||
'width': self.tile_width,
|
||||
'height': self.tile_height,
|
||||
'min_zoom': self.min_zoom,
|
||||
'max_zoom': self.max_zoom,
|
||||
'extent': self.wgs_extent,
|
||||
}
|
||||
writer.set_parameters(tile_params)
|
||||
self.writer = writer
|
||||
|
||||
self.progressThreadLock = threading.Lock()
|
||||
if self.maxThreads > 1:
|
||||
feedback.pushConsoleInfo(self.tr('Using {max_threads} CPU Threads:').format(max_threads=self.maxThreads))
|
||||
for zoom in range(self.min_zoom, self.max_zoom + 1):
|
||||
feedback.pushConsoleInfo(self.tr('Generating tiles for zoom level: {zoom}').format(zoom=zoom))
|
||||
with ThreadPoolExecutor(max_workers=self.maxThreads) as threadPool:
|
||||
for result in threadPool.map(self.renderSingleMetatile, metatiles_by_zoom[zoom]):
|
||||
# re-raise exceptions from threads
|
||||
_ = result
|
||||
else:
|
||||
feedback.pushConsoleInfo(self.tr('Using 1 CPU Thread:'))
|
||||
for zoom in range(self.min_zoom, self.max_zoom + 1):
|
||||
feedback.pushConsoleInfo(self.tr('Generating tiles for zoom level: {zoom}').format(zoom=zoom))
|
||||
for i, metatile in enumerate(metatiles_by_zoom[zoom]):
|
||||
self.renderSingleMetatile(metatile)
|
||||
|
||||
writer.close()
|
||||
|
||||
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)
|
||||
|
||||
|
||||
########################################################################
|
||||
# MBTiles
|
||||
########################################################################
|
||||
class MBTilesWriter:
|
||||
|
||||
def __init__(self, filename):
|
||||
base_dir = os.path.dirname(filename)
|
||||
os.makedirs(base_dir, exist_ok=True)
|
||||
self.filename = filename
|
||||
|
||||
def set_parameters(self, tile_params):
|
||||
self.extent = tile_params.get('extent')
|
||||
self.tile_width = tile_params.get('width', 256)
|
||||
self.tile_height = tile_params.get('height', 256)
|
||||
self.min_zoom = tile_params.get('min_zoom')
|
||||
self.max_zoom = tile_params.get('max_zoom')
|
||||
tile_format = tile_params['format']
|
||||
options = []
|
||||
if tile_format == 'JPG':
|
||||
tile_format = 'JPEG'
|
||||
options = ['QUALITY=%s' % tile_params.get('quality', 75)]
|
||||
driver = gdal.GetDriverByName('MBTiles')
|
||||
ds = driver.Create(self.filename, 1, 1, 1, options=['TILE_FORMAT=%s' % tile_format] + options)
|
||||
ds = None
|
||||
|
||||
# faster sqlite processing for parallel access https://stackoverflow.com/questions/15143871/simplest-way-to-retry-sqlite-query-if-db-is-locked
|
||||
self._execute_sqlite("PRAGMA journal_mode=WAL")
|
||||
|
||||
self._execute_sqlite(
|
||||
"INSERT INTO metadata(name, value) VALUES ('{}', '{}');".format('minzoom', self.min_zoom),
|
||||
"INSERT INTO metadata(name, value) VALUES ('{}', '{}');".format('maxzoom', self.max_zoom),
|
||||
# will be set properly after writing all tiles
|
||||
"INSERT INTO metadata(name, value) VALUES ('{}', '');".format('bounds')
|
||||
)
|
||||
self._zoom = None
|
||||
|
||||
def _execute_sqlite(self, *commands):
|
||||
# wait_timeout = default timeout is 5 seconds increase it for slower disk access and more Threads to 120 seconds
|
||||
# isolation_level = None Uses sqlite AutoCommit and disable phyton transaction management feature. https://docs.python.org/3/library/sqlite3.html#sqlite3-controlling-transactions
|
||||
conn = sqlite3.connect(self.filename, timeout=120, isolation_level=None)
|
||||
for cmd in commands:
|
||||
conn.execute(cmd)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def _init_zoom_layer(self, zoom):
|
||||
self._zoom_ds = None
|
||||
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]
|
||||
]
|
||||
|
||||
bounds = ','.join(map(str, zoom_extent))
|
||||
self._execute_sqlite(f"UPDATE metadata SET value='{bounds}' WHERE name='bounds'")
|
||||
|
||||
self._zoom_ds = gdal.OpenEx(self.filename, 1, open_options=['ZOOM_LEVEL=%s' % first_tile.z])
|
||||
self._first_tile = first_tile
|
||||
self._zoom = zoom
|
||||
|
||||
def write_tile(self, tile, image):
|
||||
if tile.z != self._zoom:
|
||||
self._init_zoom_layer(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._zoom_ds.WriteRaster(xoff, yoff, self.tile_width, self.tile_height, data)
|
||||
|
||||
def close(self):
|
||||
self._zoom_ds = None
|
||||
bounds = ','.join(map(str, self.extent))
|
||||
self._execute_sqlite(f"UPDATE metadata SET value='{bounds}' WHERE name='bounds'")
|
||||
# Set Journal Mode back to default
|
||||
self._execute_sqlite("PRAGMA journal_mode=DELETE")
|
||||
|
||||
|
||||
class TilesXYZAlgorithmMBTiles(TilesXYZAlgorithmBase):
|
||||
OUTPUT_FILE = 'OUTPUT_FILE'
|
||||
|
||||
def initAlgorithm(self, config=None):
|
||||
super().initAlgorithm()
|
||||
self.addParameter(QgsProcessingParameterFileDestination(self.OUTPUT_FILE,
|
||||
self.tr('Output file (for MBTiles)'),
|
||||
self.tr('MBTiles files (*.mbtiles)'),
|
||||
optional=True))
|
||||
|
||||
def name(self):
|
||||
return 'tilesxyzmbtiles'
|
||||
|
||||
def displayName(self):
|
||||
return self.tr('Generate XYZ tiles (MBTiles)')
|
||||
|
||||
def group(self):
|
||||
return self.tr('Raster tools')
|
||||
|
||||
def groupId(self):
|
||||
return 'rastertools'
|
||||
|
||||
def flags(self):
|
||||
return super().flags() | QgsProcessingAlgorithm.FlagRequiresProject
|
||||
|
||||
def processAlgorithm(self, parameters, context, feedback):
|
||||
output_file = self.parameterAsString(parameters, self.OUTPUT_FILE, context)
|
||||
if not output_file:
|
||||
raise QgsProcessingException(self.tr('You need to specify output filename.'))
|
||||
|
||||
writer = MBTilesWriter(output_file)
|
||||
self.generate(writer, parameters, context, feedback)
|
||||
|
||||
results = {'OUTPUT_FILE': output_file}
|
||||
return results
|
||||
|
||||
|
||||
########################################################################
|
||||
# Directory
|
||||
########################################################################
|
||||
LEAFLET_TEMPLATE = '''
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{tilesetname}</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.3/dist/leaflet.css"
|
||||
integrity="sha384-o/2yZuJZWGJ4s/adjxVW71R+EO/LyCwdQfP5UWSgX/w87iiTXuvDZaejd3TsN7mf"
|
||||
crossorigin=""/>
|
||||
<script src="https://unpkg.com/leaflet@1.9.3/dist/leaflet.js"
|
||||
integrity="sha384-okbbMvvx/qfQkmiQKfd5VifbKZ/W8p1qIsWvE1ROPUfHWsDcC8/BnHohF7vPg2T6"
|
||||
crossorigin=""></script>
|
||||
<style type="text/css">
|
||||
body {{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}}
|
||||
html, body, #map{{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="map"></div>
|
||||
<script>
|
||||
var map = L.map('map', {{ attributionControl: false }} ).setView([{centery}, {centerx}], {avgzoom});
|
||||
L.control.attribution( {{ prefix: false }} ).addTo( map );
|
||||
{osm}
|
||||
var tilesource_layer = L.tileLayer({tilesource}, {{
|
||||
minZoom: {minzoom},
|
||||
maxZoom: {maxzoom},
|
||||
tms: {tms},
|
||||
attribution: '{attribution}'
|
||||
}}).addTo(map);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
|
||||
OSM_TEMPLATE = '''
|
||||
var osm_layer = L.tileLayer('https://tile.openstreetmap.org/{{z}}/{{x}}/{{y}}.png', {{
|
||||
minZoom: {minzoom},
|
||||
maxZoom: {maxzoom},
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||
}}).addTo(map);
|
||||
'''
|
||||
|
||||
|
||||
class DirectoryWriter:
|
||||
|
||||
def __init__(self, folder, is_tms):
|
||||
self.folder = folder
|
||||
self.is_tms = is_tms
|
||||
|
||||
def set_parameters(self, tile_params):
|
||||
self.format = tile_params.get('format', 'PNG')
|
||||
self.quality = tile_params.get('quality', -1)
|
||||
|
||||
def write_tile(self, tile, image):
|
||||
directory = os.path.join(self.folder, str(tile.z), str(tile.x))
|
||||
os.makedirs(directory, exist_ok=True)
|
||||
ytile = tile.y
|
||||
if self.is_tms:
|
||||
ytile = tms(ytile, tile.z)
|
||||
path = os.path.join(directory, f'{ytile}.{self.format.lower()}')
|
||||
image.save(path, self.format, self.quality)
|
||||
return path
|
||||
|
||||
def close(self):
|
||||
pass
|
||||
|
||||
|
||||
class TilesXYZAlgorithmDirectory(TilesXYZAlgorithmBase):
|
||||
TMS_CONVENTION = 'TMS_CONVENTION'
|
||||
OUTPUT_DIRECTORY = 'OUTPUT_DIRECTORY'
|
||||
OUTPUT_HTML = 'OUTPUT_HTML'
|
||||
TILE_WIDTH = 'TILE_WIDTH'
|
||||
TILE_HEIGHT = 'TILE_HEIGHT'
|
||||
HTML_TITLE = 'HTML_TITLE'
|
||||
HTML_ATTRIBUTION = 'HTML_ATTRIBUTION'
|
||||
HTML_OSM = 'HTML_OSM'
|
||||
|
||||
def initAlgorithm(self, config=None):
|
||||
super().initAlgorithm()
|
||||
self.addParameter(QgsProcessingParameterNumber(self.TILE_WIDTH,
|
||||
self.tr('Tile width'),
|
||||
minValue=1,
|
||||
maxValue=4096,
|
||||
defaultValue=256))
|
||||
self.addParameter(QgsProcessingParameterNumber(self.TILE_HEIGHT,
|
||||
self.tr('Tile height'),
|
||||
minValue=1,
|
||||
maxValue=4096,
|
||||
defaultValue=256))
|
||||
self.addParameter(QgsProcessingParameterBoolean(self.TMS_CONVENTION,
|
||||
self.tr('Use inverted tile Y axis (TMS convention)'),
|
||||
defaultValue=False,
|
||||
optional=True))
|
||||
self.addParameter(QgsProcessingParameterFolderDestination(self.OUTPUT_DIRECTORY,
|
||||
self.tr('Output directory'),
|
||||
optional=True))
|
||||
self.addParameter(QgsProcessingParameterFileDestination(self.OUTPUT_HTML,
|
||||
self.tr('Output html (Leaflet)'),
|
||||
self.tr('HTML files (*.html)'),
|
||||
optional=True))
|
||||
html_title_param = QgsProcessingParameterString(self.HTML_TITLE,
|
||||
self.tr('Leaflet HTML output title'),
|
||||
optional=True,
|
||||
defaultValue='')
|
||||
html_attrib_param = QgsProcessingParameterString(self.HTML_ATTRIBUTION,
|
||||
self.tr('Leaflet HTML output attribution'),
|
||||
optional=True,
|
||||
defaultValue='')
|
||||
html_osm_param = QgsProcessingParameterBoolean(self.HTML_OSM,
|
||||
self.tr('Include OpenStreetMap basemap in Leaflet HTML output'),
|
||||
defaultValue=False,
|
||||
optional=True)
|
||||
for param in (html_title_param, html_attrib_param, html_osm_param):
|
||||
param.setFlags(param.flags() | QgsProcessingParameterDefinition.FlagAdvanced)
|
||||
self.addParameter(param)
|
||||
|
||||
def name(self):
|
||||
return 'tilesxyzdirectory'
|
||||
|
||||
def displayName(self):
|
||||
return self.tr('Generate XYZ tiles (Directory)')
|
||||
|
||||
def group(self):
|
||||
return self.tr('Raster tools')
|
||||
|
||||
def groupId(self):
|
||||
return 'rastertools'
|
||||
|
||||
def processAlgorithm(self, parameters, context, feedback):
|
||||
is_tms = self.parameterAsBoolean(parameters, self.TMS_CONVENTION, context)
|
||||
output_html = self.parameterAsString(parameters, self.OUTPUT_HTML, context)
|
||||
output_dir = self.parameterAsString(parameters, self.OUTPUT_DIRECTORY, context)
|
||||
html_title = self.parameterAsString(parameters, self.HTML_TITLE, context)
|
||||
html_attribution = self.parameterAsString(parameters, self.HTML_ATTRIBUTION, context)
|
||||
is_osm = self.parameterAsBoolean(parameters, self.HTML_OSM, context)
|
||||
if not output_dir:
|
||||
raise QgsProcessingException(self.tr('You need to specify output directory.'))
|
||||
|
||||
writer = DirectoryWriter(output_dir, is_tms)
|
||||
self.generate(writer, parameters, context, feedback)
|
||||
|
||||
results = {'OUTPUT_DIRECTORY': output_dir}
|
||||
|
||||
if output_html:
|
||||
output_dir_safe = urllib.parse.quote(output_dir.replace('\\', '/'))
|
||||
html_code = LEAFLET_TEMPLATE.format(
|
||||
tilesetname=html_title if html_title else "Leaflet Preview",
|
||||
centerx=self.wgs_extent[0] + (self.wgs_extent[2] - self.wgs_extent[0]) / 2,
|
||||
centery=self.wgs_extent[1] + (self.wgs_extent[3] - self.wgs_extent[1]) / 2,
|
||||
avgzoom=(self.max_zoom + self.min_zoom) / 2,
|
||||
tilesource=f"'file:///{output_dir_safe}/{{z}}/{{x}}/{{y}}.{self.tile_format.lower()}'",
|
||||
minzoom=self.min_zoom,
|
||||
maxzoom=self.max_zoom,
|
||||
tms='true' if is_tms else 'false',
|
||||
attribution=html_attribution if html_attribution else self.tr('Created by QGIS algorithm:') + ' ' + self.displayName(),
|
||||
osm=OSM_TEMPLATE.format(
|
||||
minzoom=self.min_zoom,
|
||||
maxzoom=self.max_zoom
|
||||
) if is_osm else ''
|
||||
)
|
||||
with open(output_html, "w") as fh:
|
||||
fh.write(html_code)
|
||||
results['OUTPUT_HTML'] = output_html
|
||||
|
||||
return results
|
||||
@ -237,6 +237,7 @@ set(QGIS_ANALYSIS_SRCS
|
||||
processing/qgsalgorithmvectorize.cpp
|
||||
processing/qgsalgorithmwedgebuffers.cpp
|
||||
processing/qgsalgorithmwritevectortiles.cpp
|
||||
processing/qgsalgorithmxyztiles.cpp
|
||||
processing/qgsalgorithmzonalhistogram.cpp
|
||||
processing/qgsalgorithmzonalstatistics.cpp
|
||||
processing/qgsalgorithmzonalstatisticsfeaturebased.cpp
|
||||
|
||||
536
src/analysis/processing/qgsalgorithmxyztiles.cpp
Normal file
536
src/analysis/processing/qgsalgorithmxyztiles.cpp
Normal file
@ -0,0 +1,536 @@
|
||||
/***************************************************************************
|
||||
qgsalgorithmxyztiles.cpp
|
||||
---------------------
|
||||
begin : August 2023
|
||||
copyright : (C) 2023 by Alexander Bruy
|
||||
email : alexander dot bruy at gmail dot com
|
||||
***************************************************************************/
|
||||
|
||||
/***************************************************************************
|
||||
* *
|
||||
* 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. *
|
||||
* *
|
||||
***************************************************************************/
|
||||
|
||||
#include "qgsalgorithmxyztiles.h"
|
||||
|
||||
#include <QBuffer>
|
||||
|
||||
#include "qgslayertree.h"
|
||||
#include "qgslayertreelayer.h"
|
||||
#include "qgsexpressioncontextutils.h"
|
||||
|
||||
///@cond PRIVATE
|
||||
|
||||
int tile2tms( const int y, const int zoom )
|
||||
{
|
||||
double n = std::pow( 2, zoom );
|
||||
return ( int )std::floor( n - y - 1 );
|
||||
}
|
||||
|
||||
int lon2tileX( const double lon, const int z )
|
||||
{
|
||||
return ( int )( std::floor( ( lon + 180.0 ) / 360.0 * ( 1 << z ) ) );
|
||||
}
|
||||
|
||||
int lat2tileY( const double lat, const int z )
|
||||
{
|
||||
double latRad = lat * M_PI / 180.0;
|
||||
return ( int )( std::floor( ( 1.0 - std::asinh( std::tan( latRad ) ) / M_PI ) / 2.0 * ( 1 << z ) ) );
|
||||
}
|
||||
|
||||
double tileX2lon( const int x, const int z )
|
||||
{
|
||||
return x / ( double )( 1 << z ) * 360.0 - 180 ;
|
||||
}
|
||||
|
||||
double tileY2lat( const int y, const int z )
|
||||
{
|
||||
double n = M_PI - 2.0 * M_PI * y / ( double )( 1 << z );
|
||||
return 180.0 / M_PI * std::atan( 0.5 * ( std::exp( n ) - std::exp( -n ) ) );
|
||||
}
|
||||
|
||||
void extent2TileXY( QgsRectangle extent, const int zoom, int &xMin, int &yMin, int &xMax, int &yMax )
|
||||
{
|
||||
xMin = lon2tileX( extent.xMinimum(), zoom );
|
||||
yMin = lat2tileY( extent.yMinimum(), zoom );
|
||||
xMax = lon2tileX( extent.xMaximum(), zoom );
|
||||
yMax = lat2tileY( extent.xMaximum(), zoom );
|
||||
}
|
||||
|
||||
QList< MetaTile > getMetatiles( const QgsRectangle extent, const int zoom, const int tileSize )
|
||||
{
|
||||
int minX = lon2tileX( extent.xMinimum(), zoom );
|
||||
int minY = lat2tileY( extent.yMaximum(), zoom );
|
||||
int maxX = lon2tileX( extent.xMaximum(), zoom );
|
||||
int maxY = lat2tileY( extent.yMinimum(), zoom );;
|
||||
|
||||
int i = 0;
|
||||
QMap< QString, MetaTile > tiles;
|
||||
for ( int x = minX; x <= maxX; x++ )
|
||||
{
|
||||
int j = 0;
|
||||
for ( int y = minY; y <= maxY; y++ )
|
||||
{
|
||||
QString key = QStringLiteral( "%1:%2" ).arg( ( int )( i / tileSize ) ).arg( ( int )( j / tileSize ) );
|
||||
MetaTile tile = tiles.value( key, MetaTile() );
|
||||
tile.addTile( i % tileSize, j % tileSize, Tile( x, y, zoom ) );
|
||||
tiles.insert( key, tile );
|
||||
j++;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
return tiles.values();
|
||||
}
|
||||
|
||||
////
|
||||
|
||||
QString QgsXyzTilesBaseAlgorithm::group() const
|
||||
{
|
||||
return QObject::tr( "Raster tools" );
|
||||
}
|
||||
|
||||
QString QgsXyzTilesBaseAlgorithm::groupId() const
|
||||
{
|
||||
return QStringLiteral( "rastertools" );
|
||||
}
|
||||
|
||||
QgsProcessingAlgorithm::Flags QgsXyzTilesBaseAlgorithm::flags() const
|
||||
{
|
||||
return QgsProcessingAlgorithm::flags() | QgsProcessingAlgorithm::FlagRequiresProject;
|
||||
}
|
||||
|
||||
void QgsXyzTilesBaseAlgorithm::createCommonParameters()
|
||||
{
|
||||
addParameter( new QgsProcessingParameterExtent( QStringLiteral( "EXTENT" ), QObject::tr( "Extent" ) ) );
|
||||
addParameter( new QgsProcessingParameterNumber( QStringLiteral( "ZOOM_MIN" ), QObject::tr( "Minimum zoom" ), QgsProcessingParameterNumber::Integer, 12, false, 0, 25 ) );
|
||||
addParameter( new QgsProcessingParameterNumber( QStringLiteral( "ZOOM_MAX" ), QObject::tr( "Maximum zoom" ), QgsProcessingParameterNumber::Integer, 12, false, 0, 25 ) );
|
||||
addParameter( new QgsProcessingParameterNumber( QStringLiteral( "DPI" ), QObject::tr( "DPI" ), QgsProcessingParameterNumber::Integer, 96, false, 48, 600 ) );
|
||||
addParameter( new QgsProcessingParameterColor( QStringLiteral( "BACKGROUND_COLOR" ), QObject::tr( "Background color" ), QColor( Qt::transparent ), true, true ) );
|
||||
addParameter( new QgsProcessingParameterBoolean( QStringLiteral( "ANTIALIAS" ), QObject::tr( "Enable antialiasing" ), true ) );
|
||||
addParameter( new QgsProcessingParameterEnum( QStringLiteral( "TILE_FORMAT" ), QObject::tr( "Tile format" ), QStringList() << QStringLiteral( "PNG" ) << QStringLiteral( "JPG" ), false, 0 ) );
|
||||
addParameter( new QgsProcessingParameterNumber( QStringLiteral( "QUALITY" ), QObject::tr( "Quality (JPG only)" ), QgsProcessingParameterNumber::Integer, 75, false, 1, 100 ) );
|
||||
addParameter( new QgsProcessingParameterNumber( QStringLiteral( "METATILESIZE" ), QObject::tr( "Metatile size" ), QgsProcessingParameterNumber::Integer, 4, false, 1, 20 ) );
|
||||
}
|
||||
|
||||
bool QgsXyzTilesBaseAlgorithm::prepareAlgorithm( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback *feedback )
|
||||
{
|
||||
Q_UNUSED( feedback );
|
||||
|
||||
QgsProject *project = context.project();
|
||||
|
||||
const QList< QgsLayerTreeLayer * > projectLayers = project->layerTreeRoot()->findLayers();
|
||||
QSet< QString > visibleLayers;
|
||||
for ( const QgsLayerTreeLayer *layer : projectLayers )
|
||||
{
|
||||
if ( layer->isVisible() )
|
||||
{
|
||||
visibleLayers << layer->layer()->id();
|
||||
}
|
||||
}
|
||||
|
||||
QList< QgsMapLayer * > renderLayers = project->layerTreeRoot()->layerOrder();
|
||||
for ( QgsMapLayer *layer : renderLayers )
|
||||
{
|
||||
if ( visibleLayers.contains( layer->id() ) )
|
||||
{
|
||||
QgsMapLayer *clonedLayer = layer->clone();
|
||||
clonedLayer->moveToThread( nullptr );
|
||||
mLayers << clonedLayer;
|
||||
}
|
||||
}
|
||||
|
||||
QgsRectangle extent = parameterAsExtent( parameters, QStringLiteral( "EXTENT" ), context );
|
||||
QgsCoordinateReferenceSystem extentCrs = parameterAsExtentCrs( parameters, QStringLiteral( "EXTENT" ), context );
|
||||
QgsCoordinateTransform ct( extentCrs, project->crs(), context.transformContext() );
|
||||
mExtent = ct.transformBoundingBox( extent );
|
||||
|
||||
mMinZoom = parameterAsInt( parameters, QStringLiteral( "ZOOM_MIN" ), context );
|
||||
mMaxZoom = parameterAsInt( parameters, QStringLiteral( "ZOOM_MAX" ), context );
|
||||
mDpi = parameterAsInt( parameters, QStringLiteral( "DPI" ), context );
|
||||
mBackgroundColor = parameterAsColor( parameters, QStringLiteral( "BACKGROUND_COLOR" ), context );
|
||||
mAntialias = parameterAsBool( parameters, QStringLiteral( "ANTIALIAS" ), context );
|
||||
mTileFormat = parameterAsEnum( parameters, QStringLiteral( "TILE_FORMAT" ), context ) ? QStringLiteral( "JPG" ) : QStringLiteral( "PNG" );
|
||||
mJpgQuality = parameterAsInt( parameters, QStringLiteral( "QUALITY" ), context );
|
||||
mMetaTileSize = parameterAsInt( parameters, QStringLiteral( "METATILESIZE" ), context );
|
||||
mThreadsNumber = context.maximumThreads();
|
||||
mTransformContext = context.transformContext();
|
||||
mFeedback = feedback;
|
||||
|
||||
mWgs84Crs = QgsCoordinateReferenceSystem( "EPSG:4326" );
|
||||
mMercatorCrs = QgsCoordinateReferenceSystem( "EPSG:3857" );
|
||||
mSrc2Wgs = QgsCoordinateTransform( project->crs(), mWgs84Crs, context.transformContext() );
|
||||
mWgs2Mercator = QgsCoordinateTransform( mWgs84Crs, mMercatorCrs, context.transformContext() );
|
||||
|
||||
mWgs84Extent = mSrc2Wgs.transformBoundingBox( mExtent );
|
||||
|
||||
if ( parameters.contains( QStringLiteral( "TILE_WIDTH" ) ) )
|
||||
{
|
||||
mTileWidth = parameterAsInt( parameters, QStringLiteral( "TILE_WIDTH" ), context );
|
||||
}
|
||||
|
||||
if ( parameters.contains( QStringLiteral( "TILE_HEIGHT" ) ) )
|
||||
{
|
||||
mTileHeight = parameterAsInt( parameters, QStringLiteral( "TILE_HEIGHT" ), context );
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void QgsXyzTilesBaseAlgorithm::startJobs()
|
||||
{
|
||||
while ( mRendererJobs.size() < mThreadsNumber )
|
||||
{
|
||||
MetaTile metaTile = mMetaTiles.takeFirst();
|
||||
|
||||
QgsMapSettings settings;
|
||||
settings.setExtent( mWgs2Mercator.transformBoundingBox( metaTile.extent() ) );
|
||||
settings.setOutputImageFormat( QImage::Format_ARGB32_Premultiplied );
|
||||
settings.setTransformContext( mTransformContext );
|
||||
settings.setDestinationCrs( mMercatorCrs );
|
||||
settings.setLayers( mLayers );
|
||||
settings.setOutputDpi( mDpi );
|
||||
if ( mTileFormat == QStringLiteral( "PNG" ) )
|
||||
{
|
||||
settings.setBackgroundColor( mBackgroundColor );
|
||||
}
|
||||
QSize size( mTileWidth * metaTile.rows, mTileHeight * metaTile.cols );
|
||||
settings.setOutputSize( size );
|
||||
|
||||
QgsLabelingEngineSettings labelingSettings = settings.labelingEngineSettings();
|
||||
labelingSettings.setFlag( Qgis::LabelingFlag::UsePartialCandidates, false );
|
||||
settings.setLabelingEngineSettings( labelingSettings );
|
||||
|
||||
QgsExpressionContext exprContext = settings.expressionContext();
|
||||
exprContext.appendScope( QgsExpressionContextUtils::mapSettingsScope( settings ) );
|
||||
settings.setExpressionContext( exprContext );
|
||||
|
||||
QgsMapRendererSequentialJob *job = new QgsMapRendererSequentialJob( settings );
|
||||
mRendererJobs.insert( job, metaTile );
|
||||
QObject::connect( job, &QgsMapRendererJob::finished, mFeedback, [ this, job ]() { processMetaTile( job ); } );
|
||||
job->start();
|
||||
}
|
||||
}
|
||||
|
||||
// Native XYZ tiles (directory) algorithm
|
||||
|
||||
QString QgsXyzTilesDirectoryAlgorithm::name() const
|
||||
{
|
||||
return QStringLiteral( "tilesxyzdirectory" );
|
||||
}
|
||||
|
||||
QString QgsXyzTilesDirectoryAlgorithm::displayName() const
|
||||
{
|
||||
return QObject::tr( "Generate XYZ tiles (Directory)" );
|
||||
}
|
||||
|
||||
QStringList QgsXyzTilesDirectoryAlgorithm::tags() const
|
||||
{
|
||||
return QObject::tr( "tiles,xyz,tms,directory" ).split( ',' );
|
||||
}
|
||||
|
||||
QString QgsXyzTilesDirectoryAlgorithm::shortHelpString() const
|
||||
{
|
||||
return QObject::tr( "Generates XYZ tiles of map canvas content and saves them as individual images in a directory." );
|
||||
}
|
||||
|
||||
QgsXyzTilesDirectoryAlgorithm *QgsXyzTilesDirectoryAlgorithm::createInstance() const
|
||||
{
|
||||
return new QgsXyzTilesDirectoryAlgorithm();
|
||||
}
|
||||
|
||||
void QgsXyzTilesDirectoryAlgorithm::initAlgorithm( const QVariantMap & )
|
||||
{
|
||||
createCommonParameters();
|
||||
addParameter( new QgsProcessingParameterNumber( QStringLiteral( "TILE_WIDTH" ), QObject::tr( "Tile width" ), QgsProcessingParameterNumber::Integer, 256, false, 1, 4096 ) );
|
||||
addParameter( new QgsProcessingParameterNumber( QStringLiteral( "TILE_HEIGHT" ), QObject::tr( "Tile height" ), QgsProcessingParameterNumber::Integer, 256, false, 1, 4096 ) );
|
||||
addParameter( new QgsProcessingParameterBoolean( QStringLiteral( "TMS_CONVENTION" ), QObject::tr( "Use inverted tile Y axis (TMS convention)" ), false, true ) );
|
||||
|
||||
std::unique_ptr< QgsProcessingParameterString > titleParam = std::make_unique< QgsProcessingParameterString >( QStringLiteral( "HTML_TITLE" ), QObject::tr( "Leaflet HTML output title" ), QVariant(), false, true );
|
||||
titleParam->setFlags( titleParam->flags() | QgsProcessingParameterDefinition::FlagAdvanced );
|
||||
addParameter( titleParam.release() );
|
||||
std::unique_ptr< QgsProcessingParameterString > attributionParam = std::make_unique< QgsProcessingParameterString >( QStringLiteral( "HTML_ATTRIBUTION" ), QObject::tr( "Leaflet HTML output attribution" ), QVariant(), false, true );
|
||||
attributionParam->setFlags( attributionParam->flags() | QgsProcessingParameterDefinition::FlagAdvanced );
|
||||
addParameter( attributionParam.release() );
|
||||
std::unique_ptr< QgsProcessingParameterBoolean > osmParam = std::make_unique< QgsProcessingParameterBoolean >( QStringLiteral( "HTML_OSM" ), QObject::tr( "Include OpenStreetMap basemap in Leaflet HTML output" ), false, true );
|
||||
osmParam->setFlags( osmParam->flags() | QgsProcessingParameterDefinition::FlagAdvanced );
|
||||
addParameter( osmParam.release() );
|
||||
|
||||
addParameter( new QgsProcessingParameterFolderDestination( QStringLiteral( "OUTPUT_DIRECTORY" ), QObject::tr( "Output directory" ) ) );
|
||||
addParameter( new QgsProcessingParameterFileDestination( QStringLiteral( "OUTPUT_HTML" ), QObject::tr( "Output html (Leaflet)" ), QObject::tr( "HTML files (*.html)" ), QVariant(), true ) );
|
||||
}
|
||||
|
||||
QVariantMap QgsXyzTilesDirectoryAlgorithm::processAlgorithm( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback *feedback )
|
||||
{
|
||||
const bool tms = parameterAsBoolean( parameters, QStringLiteral( "TMS_CONVENTION" ), context );
|
||||
const QString title = parameterAsString( parameters, QStringLiteral( "HTML_TITLE" ), context );
|
||||
const QString attribution = parameterAsString( parameters, QStringLiteral( "HTML_ATTRIBUTION" ), context );
|
||||
const bool useOsm = parameterAsBoolean( parameters, QStringLiteral( "HTML_OSM" ), context );
|
||||
QString outputDir = parameterAsString( parameters, QStringLiteral( "OUTPUT_DIRECTORY" ), context );
|
||||
const QString outputHtml = parameterAsString( parameters, QStringLiteral( "OUTPUT_HTML" ), context );
|
||||
|
||||
mOutputDir = outputDir;
|
||||
mTms = tms;
|
||||
|
||||
for ( int z = mMinZoom; z <= mMaxZoom; z++ )
|
||||
{
|
||||
if ( feedback->isCanceled() )
|
||||
break;
|
||||
|
||||
mMetaTiles += getMetatiles( mWgs84Extent, z, mMetaTileSize );
|
||||
}
|
||||
|
||||
for ( QgsMapLayer *layer : std::as_const( mLayers ) )
|
||||
{
|
||||
layer->moveToThread( QThread::currentThread() );
|
||||
}
|
||||
|
||||
mTotalTiles = mMetaTiles.size();
|
||||
|
||||
QEventLoop loop;
|
||||
// cppcheck-suppress danglingLifetime
|
||||
mEventLoop = &loop;
|
||||
startJobs();
|
||||
loop.exec();
|
||||
|
||||
qDeleteAll( mLayers );
|
||||
mLayers.clear();
|
||||
|
||||
QVariantMap results;
|
||||
results.insert( QStringLiteral( "OUTPUT_DIRECTORY" ), outputDir );
|
||||
|
||||
if ( !outputHtml.isEmpty() )
|
||||
{
|
||||
QString osm = QStringLiteral(
|
||||
"var osm_layer = L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png',"
|
||||
"{minZoom: %1, maxZoom: %2, attribution: '© <a href=\"https://www.openstreetmap.org/copyright\">OpenStreetMap</a> contributors'}).addTo(map);" )
|
||||
.arg( mMinZoom ).arg( mMaxZoom );
|
||||
|
||||
QString addOsm = useOsm ? osm : QString();
|
||||
QString tmsConvention = tms ? QStringLiteral( "true" ) : QStringLiteral( "false" );
|
||||
QString attr = attribution.isEmpty() ? QStringLiteral( "Created by QGIS" ) : attribution;
|
||||
QString tileSource = QStringLiteral( "'file:///%1/{z}/{x}/{y}.%2'" )
|
||||
.arg( outputDir.replace( "\\", "/" ).toHtmlEscaped() ).arg( mTileFormat.toLower() );
|
||||
|
||||
QString html = QStringLiteral(
|
||||
"<!DOCTYPE html><html><head><title>%1</title><meta charset=\"utf-8\"/>"
|
||||
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">"
|
||||
"<link rel=\"stylesheet\" href=\"https://unpkg.com/leaflet@1.9.3/dist/leaflet.css\""
|
||||
"integrity=\"sha384-o/2yZuJZWGJ4s/adjxVW71R+EO/LyCwdQfP5UWSgX/w87iiTXuvDZaejd3TsN7mf\""
|
||||
"crossorigin=\"\"/>"
|
||||
"<script src=\"https://unpkg.com/leaflet@1.9.3/dist/leaflet.js\""
|
||||
"integrity=\"sha384-okbbMvvx/qfQkmiQKfd5VifbKZ/W8p1qIsWvE1ROPUfHWsDcC8/BnHohF7vPg2T6\""
|
||||
"crossorigin=\"\"></script>"
|
||||
"<style type=\"text/css\">body {margin: 0;padding: 0;} html, body, #map{width: 100%;height: 100%;}</style></head>"
|
||||
"<body><div id=\"map\"></div><script>"
|
||||
"var map = L.map('map', {attributionControl: false}).setView([%2, %3], %4);"
|
||||
"L.control.attribution({prefix: false}).addTo(map);"
|
||||
"%5"
|
||||
"var tilesource_layer = L.tileLayer(%6, {minZoom: %7, maxZoom: %8, tms: %9, attribution: '%10'}).addTo(map);"
|
||||
"</script></body></html>"
|
||||
)
|
||||
.arg( title.isEmpty() ? QStringLiteral( "Leaflet preview" ) : title )
|
||||
.arg( mWgs84Extent.center().y() )
|
||||
.arg( mWgs84Extent.center().x() )
|
||||
.arg( ( mMaxZoom + mMinZoom ) / 2 )
|
||||
.arg( addOsm )
|
||||
.arg( tileSource )
|
||||
.arg( mMinZoom )
|
||||
.arg( mMaxZoom )
|
||||
.arg( tmsConvention )
|
||||
.arg( attr );
|
||||
|
||||
QFile htmlFile( outputHtml );
|
||||
if ( !htmlFile.open( QIODevice::WriteOnly | QIODevice::Truncate ) )
|
||||
{
|
||||
throw QgsProcessingException( QObject::tr( "Could not html file %1" ).arg( outputHtml ) );
|
||||
}
|
||||
QTextStream fout( &htmlFile );
|
||||
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
|
||||
fout.setCodec( "UTF-8" );
|
||||
#endif
|
||||
fout << html;
|
||||
|
||||
results.insert( QStringLiteral( "OUTPUT_HTML" ), outputHtml );
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
void QgsXyzTilesDirectoryAlgorithm::processMetaTile( QgsMapRendererSequentialJob *job )
|
||||
{
|
||||
MetaTile metaTile = mRendererJobs.value( job );
|
||||
QImage img = job->renderedImage();
|
||||
|
||||
QMap<QPair<int, int>, Tile>::const_iterator it = metaTile.tiles.constBegin();
|
||||
while ( it != metaTile.tiles.constEnd() )
|
||||
{
|
||||
QPair<int, int> tm = it.key();
|
||||
Tile tile = it.value();
|
||||
QImage tileImage = img.copy( mTileWidth * tm.first, mTileHeight * tm.second, mTileWidth, mTileHeight );
|
||||
QDir tileDir( QStringLiteral( "%1/%2/%3" ).arg( mOutputDir ).arg( tile.z ).arg( tile.x ) );
|
||||
tileDir.mkpath( tileDir.absolutePath() );
|
||||
int y = tile.y;
|
||||
if ( mTms )
|
||||
{
|
||||
y = tile2tms( y, tile.z );
|
||||
}
|
||||
tileImage.save( QStringLiteral( "%1/%2.%3" ).arg( tileDir.absolutePath() ).arg( y ).arg( mTileFormat.toLower() ), mTileFormat.toStdString().c_str(), mJpgQuality );
|
||||
++it;
|
||||
}
|
||||
|
||||
mRendererJobs.remove( job );
|
||||
job->deleteLater();
|
||||
|
||||
mFeedback->setProgress( 100.0 * ( mProcessedTiles++ ) / mTotalTiles );
|
||||
|
||||
if ( mFeedback->isCanceled() )
|
||||
{
|
||||
while ( mRendererJobs.size() > 0 )
|
||||
{
|
||||
QgsMapRendererSequentialJob *j = mRendererJobs.firstKey();
|
||||
j->cancel();
|
||||
mRendererJobs.remove( j );
|
||||
j->deleteLater();
|
||||
}
|
||||
mRendererJobs.clear();
|
||||
mEventLoop->exit();
|
||||
return;
|
||||
}
|
||||
|
||||
if ( mMetaTiles.size() > 0 )
|
||||
{
|
||||
startJobs();
|
||||
}
|
||||
else if ( mMetaTiles.size() == 0 && mRendererJobs.size() == 0 )
|
||||
{
|
||||
mEventLoop->exit();
|
||||
}
|
||||
}
|
||||
|
||||
// Native XYZ tiles (MBTiles) algorithm
|
||||
|
||||
QString QgsXyzTilesMbtilesAlgorithm::name() const
|
||||
{
|
||||
return QStringLiteral( "tilesxyzmbtiles" );
|
||||
}
|
||||
|
||||
QString QgsXyzTilesMbtilesAlgorithm::displayName() const
|
||||
{
|
||||
return QObject::tr( "Generate XYZ tiles (MBTiles)" );
|
||||
}
|
||||
|
||||
QStringList QgsXyzTilesMbtilesAlgorithm::tags() const
|
||||
{
|
||||
return QObject::tr( "tiles,xyz,tms,mbtiles" ).split( ',' );
|
||||
}
|
||||
|
||||
QString QgsXyzTilesMbtilesAlgorithm::shortHelpString() const
|
||||
{
|
||||
return QObject::tr( "Generates XYZ tiles of map canvas content and saves them as an MBTiles file." );
|
||||
}
|
||||
|
||||
QgsXyzTilesMbtilesAlgorithm *QgsXyzTilesMbtilesAlgorithm::createInstance() const
|
||||
{
|
||||
return new QgsXyzTilesMbtilesAlgorithm();
|
||||
}
|
||||
|
||||
void QgsXyzTilesMbtilesAlgorithm::initAlgorithm( const QVariantMap & )
|
||||
{
|
||||
createCommonParameters();
|
||||
addParameter( new QgsProcessingParameterFileDestination( QStringLiteral( "OUTPUT_FILE" ), QObject::tr( "Output" ), QObject::tr( "MBTiles files (*.mbtiles *.MBTILES)" ) ) );
|
||||
}
|
||||
|
||||
QVariantMap QgsXyzTilesMbtilesAlgorithm::processAlgorithm( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback *feedback )
|
||||
{
|
||||
const QString outputFile = parameterAsString( parameters, QStringLiteral( "OUTPUT_FILE" ), context );
|
||||
|
||||
mMbtilesWriter = std::make_unique<QgsMbTiles>( outputFile );
|
||||
if ( !mMbtilesWriter->create() )
|
||||
{
|
||||
throw QgsProcessingException( QObject::tr( "Failed to create MBTiles file %1" ).arg( outputFile ) );
|
||||
}
|
||||
mMbtilesWriter->setMetadataValue( "format", mTileFormat.toLower() );
|
||||
mMbtilesWriter->setMetadataValue( "name", QFileInfo( outputFile ).baseName() );
|
||||
mMbtilesWriter->setMetadataValue( "version", QStringLiteral( "1.1" ) );
|
||||
mMbtilesWriter->setMetadataValue( "type", QStringLiteral( "overlay" ) );
|
||||
mMbtilesWriter->setMetadataValue( "minzoom", QString::number( mMinZoom ) );
|
||||
mMbtilesWriter->setMetadataValue( "maxzoom", QString::number( mMaxZoom ) );
|
||||
QString boundsStr = QString( "%1,%2,%3,%4" )
|
||||
.arg( mWgs84Extent.xMinimum() ).arg( mWgs84Extent.yMinimum() )
|
||||
.arg( mWgs84Extent.xMaximum() ).arg( mWgs84Extent.yMaximum() );
|
||||
mMbtilesWriter->setMetadataValue( "bounds", boundsStr );
|
||||
|
||||
for ( int z = mMinZoom; z <= mMaxZoom; z++ )
|
||||
{
|
||||
if ( feedback->isCanceled() )
|
||||
break;
|
||||
|
||||
mMetaTiles += getMetatiles( mWgs84Extent, z, mMetaTileSize );
|
||||
}
|
||||
|
||||
mTotalTiles = mMetaTiles.size();
|
||||
|
||||
QEventLoop loop;
|
||||
// cppcheck-suppress danglingLifetime
|
||||
mEventLoop = &loop;
|
||||
startJobs();
|
||||
loop.exec();
|
||||
|
||||
QVariantMap results;
|
||||
results.insert( QStringLiteral( "OUTPUT_FILE" ), outputFile );
|
||||
return results;
|
||||
}
|
||||
|
||||
void QgsXyzTilesMbtilesAlgorithm::processMetaTile( QgsMapRendererSequentialJob *job )
|
||||
{
|
||||
MetaTile metaTile = mRendererJobs.value( job );
|
||||
QImage img = job->renderedImage();
|
||||
|
||||
QMap<QPair<int, int>, Tile>::const_iterator it = metaTile.tiles.constBegin();
|
||||
while ( it != metaTile.tiles.constEnd() )
|
||||
{
|
||||
QPair<int, int> tm = it.key();
|
||||
Tile tile = it.value();
|
||||
QImage tileImage = img.copy( mTileWidth * tm.first, mTileHeight * tm.second, mTileWidth, mTileHeight );
|
||||
QByteArray ba;
|
||||
QBuffer buffer( &ba );
|
||||
buffer.open( QIODevice::WriteOnly );
|
||||
tileImage.save( &buffer, mTileFormat.toStdString().c_str(), mJpgQuality );
|
||||
mMbtilesWriter->setTileData( tile.z, tile.x, tile2tms( tile.y, tile.z ), ba );
|
||||
++it;
|
||||
}
|
||||
|
||||
mRendererJobs.remove( job );
|
||||
job->deleteLater();
|
||||
|
||||
mFeedback->setProgress( 100.0 * ( mProcessedTiles++ ) / mTotalTiles );
|
||||
|
||||
if ( mFeedback->isCanceled() )
|
||||
{
|
||||
while ( mRendererJobs.size() > 0 )
|
||||
{
|
||||
QgsMapRendererSequentialJob *j = mRendererJobs.firstKey();
|
||||
j->cancel();
|
||||
mRendererJobs.remove( j );
|
||||
j->deleteLater();
|
||||
}
|
||||
mRendererJobs.clear();
|
||||
mEventLoop->exit();
|
||||
return;
|
||||
}
|
||||
|
||||
if ( mMetaTiles.size() > 0 )
|
||||
{
|
||||
startJobs();
|
||||
}
|
||||
else if ( mMetaTiles.size() == 0 && mRendererJobs.size() == 0 )
|
||||
{
|
||||
mEventLoop->exit();
|
||||
}
|
||||
}
|
||||
|
||||
///@endcond
|
||||
195
src/analysis/processing/qgsalgorithmxyztiles.h
Normal file
195
src/analysis/processing/qgsalgorithmxyztiles.h
Normal file
@ -0,0 +1,195 @@
|
||||
/***************************************************************************
|
||||
qgsalgorithmxyztiles.h
|
||||
---------------------
|
||||
begin : August 2023
|
||||
copyright : (C) 2023 by Alexander Bruy
|
||||
email : alexander dot bruy at gmail dot com
|
||||
***************************************************************************/
|
||||
|
||||
/***************************************************************************
|
||||
* *
|
||||
* 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. *
|
||||
* *
|
||||
***************************************************************************/
|
||||
|
||||
#ifndef QGSALGORITHMXYZTILES_H
|
||||
#define QGSALGORITHMXYZTILES_H
|
||||
|
||||
#define SIP_NO_FILE
|
||||
|
||||
#include "qgis_sip.h"
|
||||
#include "qgsprocessingalgorithm.h"
|
||||
|
||||
#include "qgsmaprenderersequentialjob.h"
|
||||
#include "qgsmbtiles.h"
|
||||
|
||||
///@cond PRIVATE
|
||||
|
||||
int tile2tms( const int y, const int zoom );
|
||||
int lon2tileX( const double lon, const int z );
|
||||
int lat2tileY( const double lat, const int z );
|
||||
double tileX2lon( const int x, const int z );
|
||||
double tileY2lat( const int y, const int z );
|
||||
void extent2TileXY( const QgsRectangle extent, const int zoom, int &xMin, int &yMin, int &xMax, int &yMax );
|
||||
|
||||
struct Tile
|
||||
{
|
||||
Tile( const int x, const int y, const int z )
|
||||
: x( x )
|
||||
, y( y )
|
||||
, z( z )
|
||||
{}
|
||||
|
||||
int x;
|
||||
int y;
|
||||
int z;
|
||||
};
|
||||
|
||||
struct MetaTile
|
||||
{
|
||||
MetaTile()
|
||||
: rows( 0 )
|
||||
, cols( 0 )
|
||||
{}
|
||||
|
||||
void addTile( const int row, const int col, Tile tileToAdd )
|
||||
{
|
||||
tiles.insert( QPair<int, int>( row, col ), tileToAdd );
|
||||
if ( row >= rows )
|
||||
{
|
||||
rows = row + 1;
|
||||
}
|
||||
if ( col >= cols )
|
||||
{
|
||||
cols = col + 1;
|
||||
}
|
||||
}
|
||||
|
||||
QgsRectangle extent()
|
||||
{
|
||||
const Tile first = tiles.first();
|
||||
const Tile last = tiles.last();
|
||||
return QgsRectangle( tileX2lon( first.x, first.z ), tileY2lat( last.y + 1, last.z ),
|
||||
tileX2lon( last.x + 1, last.z ), tileY2lat( first.y, first.z ) );
|
||||
}
|
||||
|
||||
QMap< QPair<int, int>, Tile > tiles;
|
||||
int rows;
|
||||
int cols;
|
||||
};
|
||||
QList< MetaTile > getMetatiles( const QgsRectangle extent, const int zoom, const int tileSize = 4 );
|
||||
|
||||
|
||||
/**
|
||||
* Base class for native XYZ tiles algorithms.
|
||||
*/
|
||||
class QgsXyzTilesBaseAlgorithm : public QgsProcessingAlgorithm
|
||||
{
|
||||
|
||||
public:
|
||||
|
||||
QString group() const override;
|
||||
QString groupId() const override;
|
||||
Flags flags() const override;
|
||||
|
||||
protected:
|
||||
|
||||
/**
|
||||
* Creates common parameters used in all algorithms
|
||||
*/
|
||||
void createCommonParameters();
|
||||
|
||||
bool prepareAlgorithm( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback *feedback ) override;
|
||||
|
||||
void startJobs();
|
||||
virtual void processMetaTile( QgsMapRendererSequentialJob *job ) = 0;
|
||||
|
||||
QgsRectangle mExtent;
|
||||
QColor mBackgroundColor;
|
||||
int mMinZoom = 12;
|
||||
int mMaxZoom = 12;
|
||||
int mDpi = 96;
|
||||
bool mAntialias = true;
|
||||
int mJpgQuality = 75;
|
||||
int mMetaTileSize = 4;
|
||||
int mThreadsNumber = 1;
|
||||
int mTileWidth = 256;
|
||||
int mTileHeight = 256;
|
||||
QString mTileFormat;
|
||||
QList< QgsMapLayer * > mLayers;
|
||||
QgsCoordinateReferenceSystem mWgs84Crs;
|
||||
QgsCoordinateReferenceSystem mMercatorCrs;
|
||||
QgsCoordinateTransform mSrc2Wgs;
|
||||
QgsCoordinateTransform mWgs2Mercator;
|
||||
QgsRectangle mWgs84Extent;
|
||||
QgsProcessingFeedback *mFeedback;
|
||||
long long mTotalTiles = 0;
|
||||
long long mProcessedTiles = 0;
|
||||
QgsCoordinateTransformContext mTransformContext;
|
||||
QEventLoop *mEventLoop;
|
||||
QList< MetaTile > mMetaTiles;
|
||||
QMap< QgsMapRendererSequentialJob *, MetaTile > mRendererJobs;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Native XYZ tiles (directory) algorithm.
|
||||
*/
|
||||
class QgsXyzTilesDirectoryAlgorithm : public QgsXyzTilesBaseAlgorithm
|
||||
{
|
||||
|
||||
public:
|
||||
|
||||
QgsXyzTilesDirectoryAlgorithm() = default;
|
||||
void initAlgorithm( const QVariantMap &configuration = QVariantMap() ) override;
|
||||
QString name() const override;
|
||||
QString displayName() const override;
|
||||
QStringList tags() const override;
|
||||
QString shortHelpString() const override;
|
||||
QgsXyzTilesDirectoryAlgorithm *createInstance() const override SIP_FACTORY;
|
||||
|
||||
protected:
|
||||
QVariantMap processAlgorithm( const QVariantMap ¶meters,
|
||||
QgsProcessingContext &context, QgsProcessingFeedback *feedback ) override;
|
||||
|
||||
void processMetaTile( QgsMapRendererSequentialJob *job ) override;
|
||||
|
||||
private:
|
||||
bool mTms;
|
||||
QString mOutputDir;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Native XYZ tiles (MBTiles) algorithm.
|
||||
*/
|
||||
class QgsXyzTilesMbtilesAlgorithm : public QgsXyzTilesBaseAlgorithm
|
||||
{
|
||||
|
||||
public:
|
||||
|
||||
QgsXyzTilesMbtilesAlgorithm() = default;
|
||||
void initAlgorithm( const QVariantMap &configuration = QVariantMap() ) override;
|
||||
QString name() const override;
|
||||
QString displayName() const override;
|
||||
QStringList tags() const override;
|
||||
QString shortHelpString() const override;
|
||||
QgsXyzTilesMbtilesAlgorithm *createInstance() const override SIP_FACTORY;
|
||||
|
||||
protected:
|
||||
QVariantMap processAlgorithm( const QVariantMap ¶meters,
|
||||
QgsProcessingContext &context, QgsProcessingFeedback *feedback ) override;
|
||||
|
||||
void processMetaTile( QgsMapRendererSequentialJob *job ) override;
|
||||
|
||||
private:
|
||||
std::unique_ptr<QgsMbTiles> mMbtilesWriter;
|
||||
|
||||
};
|
||||
|
||||
///@endcond PRIVATE
|
||||
|
||||
#endif // QGSALGORITHMXYZTILES_H
|
||||
@ -221,6 +221,7 @@
|
||||
#include "qgsalgorithmvectorize.h"
|
||||
#include "qgsalgorithmwedgebuffers.h"
|
||||
#include "qgsalgorithmwritevectortiles.h"
|
||||
#include "qgsalgorithmxyztiles.h"
|
||||
#include "qgsalgorithmzonalhistogram.h"
|
||||
#include "qgsalgorithmzonalstatistics.h"
|
||||
#include "qgsalgorithmzonalstatisticsfeaturebased.h"
|
||||
@ -519,6 +520,8 @@ void QgsNativeAlgorithms::loadAlgorithms()
|
||||
addAlgorithm( new QgsWedgeBuffersAlgorithm() );
|
||||
addAlgorithm( new QgsWriteVectorTilesXyzAlgorithm() );
|
||||
addAlgorithm( new QgsWriteVectorTilesMbtilesAlgorithm() );
|
||||
addAlgorithm( new QgsXyzTilesDirectoryAlgorithm() );
|
||||
addAlgorithm( new QgsXyzTilesMbtilesAlgorithm() );
|
||||
addAlgorithm( new QgsZonalHistogramAlgorithm() );
|
||||
addAlgorithm( new QgsZonalStatisticsAlgorithm() );
|
||||
addAlgorithm( new QgsZonalStatisticsFeatureBasedAlgorithm() );
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user