Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Don't try to upload empty vector layers, support xyz layers #51

Merged
merged 3 commits into from
Nov 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion felt/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
AuthState,
ObjectType,
LayerExportResult,
UsageType
UsageType,
LayerSupport
)
from .auth import OAuthWorkflow # noqa
from .map import Map # noqa
Expand Down
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
20 changes: 20 additions & 0 deletions felt/core/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,26 @@ class LayerExportResult(Enum):
Canceled = auto()


class LayerSupport(Enum):
"""
Reasons why a layer is not supported
"""
Supported = auto()
NotImplementedProvider = auto()
NotImplementedLayerType = auto()
EmptyLayer = auto()

def should_report(self) -> bool:
"""
Returns True if the layer support should be reported to Felt
usage API
"""
return self not in (
LayerSupport.Supported,
LayerSupport.EmptyLayer
)


class UsageType(Enum):
"""
Usage types for reporting plugin usage
Expand Down
99 changes: 86 additions & 13 deletions 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,10 +63,15 @@
QgsRasterRange
)

from .enums import LayerExportResult
from .layer_style import LayerStyle
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 @@ -108,20 +116,85 @@ def __del__(self):
self.temp_dir.cleanup()

@staticmethod
def can_export_layer(layer: QgsMapLayer) -> bool:
def can_export_layer(layer: QgsMapLayer) \
-> Tuple[LayerSupport, str]:
"""
Returns True if a layer can be exported
Returns True if a layer can be exported, and an explanatory
string if not
"""
if isinstance(layer, QgsVectorLayer):
return True
# Vector layers must have some features
if layer.featureCount() == 0:
return LayerSupport.EmptyLayer, 'Layer is empty'

return LayerSupport.Supported, ''

if isinstance(layer, QgsRasterLayer):
return layer.providerType() in (
'gdal',
'virtualraster'
if layer.providerType() in (
'gdal',
'virtualraster'
):
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(
layer.providerType())
)

return False
return (
LayerSupport.NotImplementedLayerType,
'{} layers are not yet supported'.format(
layer.__class__.__name__
)
)

@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 Expand Up @@ -188,8 +261,8 @@ def generate_file_name(self, suffix: str) -> str:
"""
Generates a temporary file name with the given suffix
"""
return (Path(str(self.temp_dir.name)) / ('qgis_export_' +
(uuid.uuid4().hex + suffix))).as_posix()
file_name = 'qgis_export_' + uuid.uuid4().hex + suffix
return (Path(str(self.temp_dir.name)) / file_name).as_posix()

def export_layer_for_felt(
self,
Expand Down Expand Up @@ -296,7 +369,7 @@ def export_vector_layer(
{
'type': Logger.PACKAGING_VECTOR,
'error': 'Error packaging layer: {}'.format(error_message)
}
}
)

raise LayerPackagingException(error_message)
Expand Down
85 changes: 60 additions & 25 deletions felt/core/map_uploader.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
from pathlib import Path
from typing import (
Optional,
List
List,
Tuple
)

from qgis.PyQt.QtCore import (
Expand Down Expand Up @@ -53,6 +54,7 @@
from .map_utils import MapUtils
from .multi_step_feedback import MultiStepFeedback
from .s3_upload_parameters import S3UploadParameters
from .enums import LayerSupport


class MapUploaderTask(QgsTask):
Expand All @@ -71,7 +73,7 @@ def __init__(self,
)
project = project or QgsProject.instance()

self.unsupported_layers = []
self.unsupported_layers: List[Tuple[str, str]] = []
if layers:
self.current_map_crs = QgsCoordinateReferenceSystem('EPSG:4326')
self.current_map_extent = QgsMapLayerUtils.combinedExtent(
Expand Down Expand Up @@ -101,7 +103,8 @@ def __init__(self,

self.layers = [
layer.clone() for layer in visible_layers if
LayerExporter.can_export_layer(layer)
LayerExporter.can_export_layer(layer)[
0] == LayerSupport.Supported
]

self._build_unsupported_layer_details(project, visible_layers)
Expand Down Expand Up @@ -149,15 +152,21 @@ def _build_unsupported_layer_details(self,
these to users and to Felt
"""
unsupported_layer_type_count = defaultdict(int)
unsupported_layer_names = set()
for layer in layers:
if LayerExporter.can_export_layer(layer):
support, reason = LayerExporter.can_export_layer(layer)
if not support.should_report():
continue

self.unsupported_layers.append(layer.name())
unsupported_layer_names.add(layer.name())
self.unsupported_layers.append((layer.name(), reason))
if layer.type() == QgsMapLayer.PluginLayer:
id_string = layer.pluginLayerType()
else:
id_string = str(layer.__class__.__name__)
id_string = '{}:{}'.format(
layer.__class__.__name__,
layer.providerType()
)

unsupported_layer_type_count[id_string] = (
unsupported_layer_type_count[id_string] + 1)
Expand All @@ -169,8 +178,8 @@ def _build_unsupported_layer_details(self,
for layer_tree_layer in project.layerTreeRoot().findLayers():
if layer_tree_layer.isVisible() and \
not layer_tree_layer.layer() and \
not layer_tree_layer.name() in self.unsupported_layers:
self.unsupported_layers.append(layer_tree_layer.name())
not layer_tree_layer.name() in unsupported_layer_names:
self.unsupported_layers.append((layer_tree_layer.name(), ''))

def default_map_title(self) -> str:
"""
Expand Down Expand Up @@ -199,7 +208,13 @@ def warning_message(self) -> Optional[str]:

msg = '<p>' + self.tr('The following layers are not supported '
'and won\'t be uploaded:') + '</p><ul><li>'
msg += '</li><li>'.join(self.unsupported_layers)

for layer_name, reason in self.unsupported_layers:
if reason:
msg += '<li>{}: {}</li>'.format(layer_name, reason)
else:
msg += '<li>{}</li>'.format(layer_name)

msg += '</ul>'
return msg

Expand Down Expand Up @@ -293,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
Loading
Loading