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

Implement data defined min/max scales #10

Merged
merged 3 commits into from
Apr 1, 2022
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
74 changes: 70 additions & 4 deletions animation_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@
QgsRectangle,
QgsFeature,
QgsMapLayerUtils,
Qgis
Qgis,
QgsPropertyDefinition,
QgsPropertyCollection,
QgsExpressionContext
)

from .render_queue import RenderJob
Expand All @@ -50,6 +53,14 @@ class AnimationController(QObject):
normal_message = pyqtSignal(str)
verbose_message = pyqtSignal(str)

PROPERTY_MIN_SCALE = 1
PROPERTY_MAX_SCALE = 2

DYNAMIC_PROPERTIES = {
PROPERTY_MIN_SCALE: QgsPropertyDefinition('min_scale', 'Minimum scale', QgsPropertyDefinition.DoublePositive),
PROPERTY_MAX_SCALE: QgsPropertyDefinition('max_scale', 'Maximum scale', QgsPropertyDefinition.DoublePositive),
}

@staticmethod
def create_fixed_extent_controller(
map_settings: QgsMapSettings,
Expand Down Expand Up @@ -91,6 +102,10 @@ def create_moving_extent_controller(
if not feature_layer:
raise InvalidAnimationParametersException("No animation layer set")

context = map_settings.expressionContext()
context.appendScope(feature_layer.createExpressionContextScope())
map_settings.setExpressionContext(context)

controller = AnimationController(mode, map_settings)
controller.feature_layer = feature_layer
controller.total_feature_count = feature_layer.featureCount()
Expand All @@ -117,6 +132,10 @@ def __init__(self, map_mode: MapMode, map_settings: QgsMapSettings):
self.map_settings: QgsMapSettings = map_settings
self.map_mode: MapMode = map_mode

self.expression_context = self.map_settings.expressionContext()

self.data_defined_properties = QgsPropertyCollection()

self.feature_layer: Optional[QgsVectorLayer] = None
self.layer_to_map_transform: Optional[QgsCoordinateTransform] = None
self.total_feature_count: int = 0
Expand All @@ -128,6 +147,9 @@ def __init__(self, map_mode: MapMode, map_settings: QgsMapSettings):
self.max_scale: float = 0
self.min_scale: float = 0

self._evaluated_min_scale = None
self._evaluated_max_scale = None

self.pan_easing: Optional[QEasingCurve] = None
self.zoom_easing: Optional[QEasingCurve] = None

Expand All @@ -141,6 +163,7 @@ def __init__(self, map_mode: MapMode, map_settings: QgsMapSettings):
self.previous_feature: Optional[QgsFeature] = None

self.reuse_cache: bool = False
self.flying_up = False

def create_job_for_frame(self, frame: int) -> Optional[RenderJob]:
"""
Expand Down Expand Up @@ -183,8 +206,36 @@ def create_fixed_extent_job(self) -> Iterator[RenderJob]:
yield job

def create_moving_extent_job(self) -> Iterator[RenderJob]:
self._evaluated_min_scale = self.min_scale

self.set_to_scale(self.min_scale)
for feature in self.feature_layer.getFeatures():
if self.previous_feature is None:
# first feature, need to evaluate the starting scale
context = QgsExpressionContext(self.expression_context)
context.setFeature(feature)
self.map_settings.setExpressionContext(context)

self._evaluated_max_scale = self.max_scale
if self.data_defined_properties.hasActiveProperties():
self._evaluated_max_scale, _ = self.data_defined_properties.valueAsDouble(
AnimationController.PROPERTY_MAX_SCALE, context, self.max_scale)

context = QgsExpressionContext(self.expression_context)
context.setFeature(feature)

scope = QgsExpressionContextScope()
scope.setVariable('from_feature', self.previous_feature, True)
scope.setVariable('to_feature', feature, True)
context.appendScope(scope)

self.map_settings.setExpressionContext(context)

# update min scale as soon as we are ready to move to the next feature
self._evaluated_min_scale = self.min_scale
if self.data_defined_properties.hasActiveProperties():
self._evaluated_min_scale, _ = self.data_defined_properties.valueAsDouble(AnimationController.PROPERTY_MIN_SCALE, context, self.min_scale)

if self.previous_feature is not None:
for job in self.fly_feature_to_feature(
self.previous_feature, feature
Expand Down Expand Up @@ -267,7 +318,7 @@ def dwell_at_feature(self, feature) -> Iterator[RenderJob]:

center = self.layer_to_map_transform.transform(center)
self.set_extent_center(center.x(), center.y())
self.set_to_scale(self.max_scale)
self.set_to_scale(self._evaluated_max_scale)
# Change CRS if needed
if self.map_mode == MapMode.SPHERE:
definition = """ +proj=ortho \
Expand Down Expand Up @@ -351,18 +402,33 @@ def fly_feature_to_feature(
zoom_factor = self.zoom_easing.valueForProgress(
progress_fraction * 2
)
self.flying_up = True
else:
# flying down
# take progress from 0.5 -> 1.0 and scale to 1 ->0
# before apply easing

if self.flying_up:
# update max scale at the half way point
context = QgsExpressionContext(self.expression_context)
context.setFeature(end_feature)
self.map_settings.setExpressionContext(context)

self._evaluated_max_scale = self.max_scale
if self.data_defined_properties.hasActiveProperties():
self._evaluated_max_scale, _ = self.data_defined_properties.valueAsDouble(
AnimationController.PROPERTY_MAX_SCALE, context, self.max_scale)

self.flying_up = False

zoom_factor = self.zoom_easing.valueForProgress(
(1 - progress_fraction) * 2
)

zoom_factor = self.zoom_easing.valueForProgress(zoom_factor)
scale = (
self.min_scale - self.max_scale
) * zoom_factor + self.max_scale
self._evaluated_min_scale - self._evaluated_max_scale
) * zoom_factor + self._evaluated_max_scale
self.set_to_scale(scale)

# Change CRS if needed
Expand Down
94 changes: 93 additions & 1 deletion animation_workbench.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,26 @@
QGridLayout,
QVBoxLayout,
)
from qgis.PyQt.QtXml import (
QDomDocument,
QDomElement
)
from qgis.core import (
QgsPointXY,
QgsExpressionContextUtils,
QgsProject,
QgsMapLayerProxyModel,
QgsReferencedRectangle,
QgsApplication,
QgsExpressionContextGenerator,
QgsPropertyCollection,
QgsExpressionContext,
QgsVectorLayer
)
from qgis.gui import (
QgsExtentWidget,
QgsPropertyOverrideButton
)
from qgis.gui import QgsExtentWidget

from .settings import set_setting, setting
from .utilities import get_ui_class, resources_path
Expand All @@ -50,6 +61,24 @@
FORM_CLASS = get_ui_class("animation_workbench_base.ui")


class DialogExpressionContextGenerator(QgsExpressionContextGenerator):

def __init__(self):
super().__init__()
self.layer = None

def set_layer(self, layer: QgsVectorLayer):
self.layer = layer

def createExpressionContext(self) -> QgsExpressionContext:
context = QgsExpressionContext()
context.appendScope(QgsExpressionContextUtils.globalScope())
context.appendScope(QgsExpressionContextUtils.projectScope(QgsProject.instance()))
if self.layer:
context.appendScope(self.layer.createExpressionContextScope())
return context


class AnimationWorkbench(QDialog, FORM_CLASS):
"""Dialog implementation class Animation Workbench class."""

Expand All @@ -68,6 +97,8 @@ def __init__(self, parent=None, iface=None, render_queue=None):
QDialog.__init__(self, parent)
self.setupUi(self)

self.expression_context_generator = DialogExpressionContextGenerator()

self.extent_group_box = QgsExtentWidget(
None, QgsExtentWidget.ExpandedStyle
)
Expand All @@ -82,6 +113,8 @@ def __init__(self, parent=None, iface=None, render_queue=None):
self.parent = parent
self.iface = iface

self.data_defined_properties = QgsPropertyCollection()

self.extent_group_box.setMapCanvas(self.iface.mapCanvas())
self.scale_range.setMapCanvas(self.iface.mapCanvas())

Expand Down Expand Up @@ -126,6 +159,7 @@ def __init__(self, parent=None, iface=None, render_queue=None):
| QgsMapLayerProxyModel.LineLayer
| QgsMapLayerProxyModel.PolygonLayer
)
self.layer_combo.layerChanged.connect(self._layer_changed)

prev_layer_id, ok = QgsProject.instance().readEntry(
"animation", "layer_id"
Expand All @@ -135,6 +169,15 @@ def __init__(self, parent=None, iface=None, render_queue=None):
if layer:
self.layer_combo.setLayer(layer)

prev_data_defined_properties_xml, _ = QgsProject.instance().readEntry(
"animation", "data_defined_properties"
)
if prev_data_defined_properties_xml:
doc = QDomDocument()
doc.setContent(prev_data_defined_properties_xml.encode())
elem = doc.firstChildElement("data_defined_properties")
self.data_defined_properties.readXml(elem, AnimationController.DYNAMIC_PROPERTIES)

self.extent_group_box.setOutputCrs(QgsProject.instance().crs())
self.extent_group_box.setOutputExtentFromUser(
self.iface.mapCanvas().extent(), QgsProject.instance().crs()
Expand Down Expand Up @@ -349,6 +392,9 @@ def __init__(self, parent=None, iface=None, render_queue=None):
self.show_preview_for_frame
)

self.register_data_defined_button(self.scale_min_dd_btn, AnimationController.PROPERTY_MIN_SCALE)
self.register_data_defined_button(self.scale_max_dd_btn, AnimationController.PROPERTY_MAX_SCALE)

def close(self):
self.save_state()
self.reject()
Expand All @@ -357,6 +403,44 @@ def closeEvent(self, event):
self.save_state()
self.reject()

def _layer_changed(self, layer):
"""
Triggered when the layer is changed
"""
self.expression_context_generator.set_layer(layer)

buttons = self.findChildren(QgsPropertyOverrideButton)
for button in buttons:
button.setVectorLayer(layer)

def register_data_defined_button(self, button, property_key: int):
"""
Registers a new data defined button, linked to the given property key (see values in AnimationController)
"""
button.init(property_key, self.data_defined_properties, AnimationController.DYNAMIC_PROPERTIES, None, False)
button.changed.connect(self._update_property)
button.registerExpressionContextGenerator(self.expression_context_generator)
button.setVectorLayer(self.layer_combo.currentLayer())

def _update_property(self):
"""
Triggered when a property override button value is changed
"""
button = self.sender()
self.data_defined_properties.setProperty(button.propertyKey(), button.toProperty())

def update_data_defined_button(self, button):
"""
Updates the current state of a property override button to reflect the current
property value
"""
if button.propertyKey() < 0:
return

button.blockSignals(True)
button.setToProperty(self.data_defined_properties.property(button.propertyKey()))
button.blockSignals(False)

def show_message(self, message):
self.output_log_text_edit.append(message)

Expand Down Expand Up @@ -508,6 +592,13 @@ def save_state(self):
)
else:
QgsProject.instance().removeEntry("animation", "layer_id")
temp_doc = QDomDocument()
dd_elem = temp_doc.createElement('data_defined_properties')
self.data_defined_properties.writeXml(dd_elem, AnimationController.DYNAMIC_PROPERTIES)
temp_doc.appendChild(dd_elem)
QgsProject.instance().writeEntry(
"animation", "data_defined_properties", temp_doc.toString()
)

# Prevent the slot being called twize
@pyqtSlot()
Expand Down Expand Up @@ -637,6 +728,7 @@ def create_controller(self) -> Optional[AnimationController]:
self.output_log_text_edit.append(f"Processing halted: {e}")
return None

controller.data_defined_properties=QgsPropertyCollection(self.data_defined_properties)
return controller

def processing_completed(self, success: bool):
Expand Down
Loading