Skip to content

Commit

Permalink
Add support for (some) XYZ tile layers
Browse files Browse the repository at this point in the history
Take care to catch unsuported {q} token in layer uris
  • Loading branch information
nyalldawson committed Nov 21, 2023
1 parent 6b68ed0 commit 2bd4748
Show file tree
Hide file tree
Showing 4 changed files with 180 additions and 17 deletions.
34 changes: 34 additions & 0 deletions felt/core/api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ class FeltApiClient:
CREATE_MAP_ENDPOINT = '/maps'
CREATE_LAYER_ENDPOINT = '/maps/{}/layers'
FINISH_LAYER_ENDPOINT = '/maps/{}/layers/{}/finish_upload'
URL_IMPORT_ENDPOINT = '/maps/{}/layers/url_import'
USAGE_ENDPOINT = '/internal/reports'
RECENT_MAPS_ENDPOINT = '/maps/recent'

Expand Down Expand Up @@ -187,6 +188,39 @@ def create_map(self,

# pylint: enable=unused-argument

def url_import_to_map(self,
map_id: str,
name: str,
layer_url: str,
blocking: bool = False,
feedback: Optional[QgsFeedback] = None) \
-> Union[QNetworkReply, QgsNetworkReplyContent]:
"""
Prepares a layer upload
"""
request = self._build_request(
self.URL_IMPORT_ENDPOINT.format(map_id),
{'Content-Type': 'application/json'}
)

request_params = {
'name': name,
'layer_url': layer_url
}

json_data = json.dumps(request_params)
if blocking:
return QgsNetworkAccessManager.instance().blockingPost(
request,
json_data.encode(),
feedback=feedback
)

return QgsNetworkAccessManager.instance().post(
request,
json_data.encode()
)

def prepare_layer_upload(self,
map_id: str,
name: str,
Expand Down
54 changes: 53 additions & 1 deletion felt/core/layer_exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@
# This will get replaced with a git SHA1 when you do a git archive
__revision__ = '$Format:%H$'

import json
import math
import tempfile
import uuid
import zipfile
import math
from dataclasses import dataclass
from pathlib import Path
from typing import (
Dict,
Optional,
List,
Tuple
Expand All @@ -34,6 +36,7 @@
)

from qgis.core import (
QgsDataSourceUri,
QgsFeedback,
QgsMapLayer,
QgsVectorLayer,
Expand All @@ -60,13 +63,15 @@
QgsRasterRange
)

from .api_client import API_CLIENT
from .enums import (
LayerExportResult,
LayerSupport
)
from .exceptions import LayerPackagingException
from .layer_style import LayerStyle
from .logger import Logger
from .map import Map


@dataclass
Expand Down Expand Up @@ -131,6 +136,19 @@ def can_export_layer(layer: QgsMapLayer) \
):
return LayerSupport.Supported, ''

if layer.providerType() == 'wms':
ds = QgsDataSourceUri()
ds.setEncodedUri(layer.source())
if ds.param('type') == 'xyz':
url = ds.param('url')
if '{q}' in url:
return (
LayerSupport.NotImplementedProvider,
'{q} token in XYZ tile layers not supported'
)

return LayerSupport.Supported, ''

return (
LayerSupport.NotImplementedProvider,
'{} raster layers are not yet supported'.format(
Expand All @@ -144,6 +162,40 @@ def can_export_layer(layer: QgsMapLayer) \
)
)

@staticmethod
def layer_import_url(layer: QgsMapLayer) -> Optional[str]:
"""
Returns the layer URL if the URL import method should be used to add
a layer
"""
if isinstance(layer, QgsRasterLayer):
if layer.providerType() == 'wms':
ds = QgsDataSourceUri()
ds.setEncodedUri(layer.source())
if ds.param('type') == 'xyz':
url = ds.param('url')
if '{q}' not in url:
return url

return None

@staticmethod
def import_from_url(layer: QgsMapLayer, target_map: Map,
feedback: Optional[QgsFeedback] = None) -> Dict:
"""
Imports a layer from URI to the given map
"""
layer_url = LayerExporter.layer_import_url(layer)

reply = API_CLIENT.url_import_to_map(
map_id=target_map.id,
name=layer.name(),
layer_url=layer_url,
blocking=True,
feedback=feedback
)
return json.loads(reply.content().data().decode())

@staticmethod
def representative_layer_style(layer: QgsVectorLayer) -> LayerStyle:
"""
Expand Down
52 changes: 36 additions & 16 deletions felt/core/map_uploader.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,27 +308,47 @@ def run(self):
if self.isCanceled():
return False

self.status_changed.emit(
self.tr('Exporting {}').format(layer.name())
)
try:
result = exporter.export_layer_for_felt(
if LayerExporter.layer_import_url(layer):
result = LayerExporter.import_from_url(
layer,
multi_step_feedback
)
except LayerPackagingException as e:
self.associated_map,
multi_step_feedback)

if 'errors' in result:
self.error_string = self.tr(
'Error occurred while exporting layer {}: {}').format(
layer.name(),
result['errors'][0]['detail']
)
self.status_changed.emit(self.error_string)

return False

layer.moveToThread(None)
self.error_string = self.tr(
'Error occurred while exporting layer {}: {}').format(
layer.name(),
e
else:

self.status_changed.emit(
self.tr('Exporting {}').format(layer.name())
)
self.status_changed.emit(self.error_string)
try:
result = exporter.export_layer_for_felt(
layer,
multi_step_feedback
)
except LayerPackagingException as e:
layer.moveToThread(None)
self.error_string = self.tr(
'Error occurred while exporting layer {}: {}').format(
layer.name(),
e
)
self.status_changed.emit(self.error_string)

return False
return False

layer.moveToThread(None)
to_upload[layer] = result

layer.moveToThread(None)
to_upload[layer] = result
multi_step_feedback.step_finished()

if self.isCanceled():
Expand Down
57 changes: 57 additions & 0 deletions felt/test/test_layer_exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,66 @@ def test_can_export_layer(self):
'&zmax=19&zmin=0',
'test', 'wms')
support, reason = LayerExporter.can_export_layer(layer)
self.assertEqual(support, LayerSupport.Supported)

layer = QgsRasterLayer(
'http-header:referer=&type=xyz&url=http://ecn.t3.tiles.'
'virtualearth.net/tiles/a%7Bq%7D.jpeg?g%3D1&zmax=18&zmin=0',
'test', 'wms')
support, reason = LayerExporter.can_export_layer(layer)
self.assertEqual(support, LayerSupport.NotImplementedProvider)
self.assertEqual(reason, '{q} token in XYZ tile layers not supported')

layer = QgsRasterLayer(
'crs=EPSG:4326&dpiMode=7&format=image/png&layers='
'wofs_summary_clear&styles&'
'tilePixelRatio=0&url=https://ows.dea.ga.gov.au/',
'test', 'wms')
support, reason = LayerExporter.can_export_layer(layer)
self.assertEqual(support, LayerSupport.NotImplementedProvider)
self.assertEqual(reason, 'wms raster layers are not yet supported')

def test_use_url_import_method(self):
"""
Test determining if layers should use the URL import method
"""
file = str(TEST_DATA_PATH / 'points.gpkg')
layer = QgsVectorLayer(file, 'test')
self.assertTrue(layer.isValid())
self.assertFalse(
LayerExporter.layer_import_url(layer))

file = str(TEST_DATA_PATH / 'dem.tif')
layer = QgsRasterLayer(file, 'test')
self.assertTrue(layer.isValid())
self.assertFalse(
LayerExporter.layer_import_url(layer))

layer = QgsRasterLayer(
'crs=EPSG:3857&format&type=xyz&url='
'https://tile.openstreetmap.org/%7Bz%7D/%7Bx%7D/%7By%7D.png'
'&zmax=19&zmin=0',
'test', 'wms')
self.assertEqual(
LayerExporter.layer_import_url(layer),
'https://tile.openstreetmap.org/{z}/{x}/{y}.png'
)

layer = QgsRasterLayer(
'http-header:referer=&type=xyz&url=http://ecn.t3.tiles.'
'virtualearth.net/tiles/a%7Bq%7D.jpeg?g%3D1&zmax=18&zmin=0',
'test', 'wms')
self.assertFalse(
LayerExporter.layer_import_url(layer))

layer = QgsRasterLayer(
'crs=EPSG:4326&dpiMode=7&format=image/png&layers='
'wofs_summary_clear&styles&tilePixelRatio=0'
'&url=https://ows.dea.ga.gov.au/',
'test', 'wms')
self.assertFalse(
LayerExporter.layer_import_url(layer))

def test_file_name(self):
"""
Test building temporary file names
Expand Down

0 comments on commit 2bd4748

Please sign in to comment.