Skip to content

Commit

Permalink
Convert graduated renderers
Browse files Browse the repository at this point in the history
  • Loading branch information
nyalldawson committed May 2, 2024
1 parent 9a630c4 commit 88d5689
Show file tree
Hide file tree
Showing 2 changed files with 184 additions and 37 deletions.
146 changes: 109 additions & 37 deletions felt/core/fsl_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@
QgsFeatureRenderer,
QgsSingleSymbolRenderer,
QgsNullSymbolRenderer,
QgsCategorizedSymbolRenderer
QgsCategorizedSymbolRenderer,
QgsGraduatedSymbolRenderer
)


Expand Down Expand Up @@ -84,7 +85,8 @@ def vector_renderer_to_fsl(renderer: QgsFeatureRenderer,
QgsSingleSymbolRenderer: FslConverter.single_renderer_to_fsl,
QgsCategorizedSymbolRenderer:
FslConverter.categorized_renderer_to_fsl,
# QgsGraduatedSymbolRenderer
QgsGraduatedSymbolRenderer:
FslConverter.graduated_renderer_to_fsl,
# QgsRuleBasedRenderer
QgsNullSymbolRenderer: FslConverter.null_renderer_to_fsl,

Expand Down Expand Up @@ -149,6 +151,62 @@ def null_renderer_to_fsl(renderer: QgsNullSymbolRenderer,
"type": "simple"
}

@staticmethod
def create_varying_style_from_list(
styles: List[List[Dict[str, object]]]) -> List[Dict[str, object]]:
"""
Given a list of individual styles, try to create a single
varying style from them
"""
if len(styles) < 1:
return []

result = []
# upgrade all properties in first symbol to lists
first_symbol = styles[0]
for layer in first_symbol:
list_dict = {}
for key, value in layer.items():
list_dict[key] = [value]
result.append(list_dict)

for symbol in styles[1:]:
for layer_idx, target_layer in enumerate(result):
if layer_idx >= len(symbol):
source_layer = None
else:
source_layer = symbol[layer_idx]

for key, value in target_layer.items():
# if property doesn't exist in this layer, copy from first
# symbol
if source_layer and key in source_layer:
value.append(source_layer[key])
else:
value.append(value[0])

return FslConverter.simplify_style(result)

@staticmethod
def simplify_style(style: List[Dict[str, object]]) \
-> List[Dict[str, object]]:
"""
Tries to simplify a style, by collapsing lists of the same
value to a single value
"""
# re-collapse single value lists to single values
cleaned_style = []
for layer in style:
cleaned_layer = {}
for key, value in layer.items():
if (isinstance(value, list) and
all(v == value[0] for v in value)):
cleaned_layer[key] = value[0]
else:
cleaned_layer[key] = value
cleaned_style.append(cleaned_layer)
return cleaned_style

@staticmethod
def categorized_renderer_to_fsl(
renderer: QgsCategorizedSymbolRenderer,
Expand Down Expand Up @@ -185,40 +243,9 @@ def categorized_renderer_to_fsl(
if not all_symbols:
return None

style = []
# upgrade all properties in first symbol to lists
first_symbol = all_symbols[0]
for layer in first_symbol:
list_dict = {}
for property, value in layer.items():
list_dict[property] = [value]
style.append(list_dict)

for symbol in all_symbols[1:]:
for layer_idx, target_layer in enumerate(style):
if layer_idx >= len(symbol):
source_layer = None
else:
source_layer = symbol[layer_idx]

for property, value in target_layer.items():
# if property doesn't exist in this layer, copy from first
# symbol
if source_layer and property in source_layer:
value.append(source_layer[property])
else:
value.append(value[0])

# re-collapse single value lists to single values
cleaned_style = []
for layer in style:
cleaned_layer = {}
for key, value in layer.items():
if all(v == value[0] for v in value):
cleaned_layer[key] = value[0]
else:
cleaned_layer[key] = value
cleaned_style.append(cleaned_layer)
style = FslConverter.create_varying_style_from_list(
all_symbols
)

return {
"config": {
Expand All @@ -229,10 +256,55 @@ def categorized_renderer_to_fsl(
"legend": {
"displayName": legend_text
},
"style": cleaned_style,
"style": style,
"type": "categorical"
}

@staticmethod
def graduated_renderer_to_fsl(
renderer: QgsGraduatedSymbolRenderer,
context: ConversionContext,
layer_opacity: float = 1) \
-> Optional[Dict[str, object]]:
"""
Converts a QGIS categorized symbol renderer to an FSL definition
"""
if not renderer.ranges():
return None

converted_symbols = []
range_breaks = []
legend_text = {}
for idx, _range in enumerate(renderer.ranges()):
converted_symbol = FslConverter.symbol_to_fsl(_range.symbol(),
context,
layer_opacity)
if converted_symbol:
converted_symbols.append(converted_symbol)
legend_text[str(idx)] = _range.label()
if idx == 0:
range_breaks.append(_range.lowerValue())
range_breaks.append(_range.upperValue())

if not converted_symbols:
return None

style = FslConverter.create_varying_style_from_list(
converted_symbols
)

return {
"config": {
"numericAttribute": renderer.classAttribute(),
"steps": range_breaks
},
"legend": {
"displayName": legend_text
},
"style": style,
"type": "numeric"
}

@staticmethod
def symbol_to_fsl(symbol: QgsSymbol,
context: ConversionContext,
Expand Down
75 changes: 75 additions & 0 deletions felt/test/test_fsl_conversion.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
QgsSingleSymbolRenderer,
QgsCategorizedSymbolRenderer,
QgsRendererCategory,
QgsGraduatedSymbolRenderer,
QgsRendererRange,
)

from .utilities import get_qgis_app
Expand Down Expand Up @@ -1354,6 +1356,79 @@ def test_categorized_renderer(self):
'type': 'categorical'}
)

def test_graduated_renderer(self):
"""
Test converting graduated renderers
"""
conversion_context = ConversionContext()

line = QgsSimpleLineSymbolLayer(color=QColor(255, 0, 0))
line_symbol = QgsLineSymbol()
line_symbol.changeSymbolLayer(0, line.clone())

line.setColor(QColor(255, 255, 0))
line.setWidth(5)
line_symbol.appendSymbolLayer(line.clone())

line_symbol2 = QgsLineSymbol()
line.setColor(QColor(255, 0, 255))
line.setWidth(6)
line_symbol2.changeSymbolLayer(0, line.clone())

line_symbol3 = QgsLineSymbol()
line.setColor(QColor(0, 255, 255))
line.setWidth(7)
line_symbol3.changeSymbolLayer(0, line.clone())

ranges = [
QgsRendererRange(1, 2, line_symbol.clone(), 'first range'),
QgsRendererRange(2, 3, line_symbol2.clone(), 'second range'),
QgsRendererRange(3, 4, line_symbol3.clone(), 'third range'),
]

renderer = QgsGraduatedSymbolRenderer('my_field',
ranges)
self.assertEqual(
FslConverter.vector_renderer_to_fsl(renderer, conversion_context),
{'config': {'numericAttribute': 'my_field',
'steps': [1.0, 2.0, 3.0, 4.0]},
'legend': {'displayName': {'0': 'first range',
'1': 'second range',
'2': 'third range'}},
'style': [{'color': ['rgb(255, 0, 0)', 'rgb(255, 0, 255)',
'rgb(0, 255, 255)'],
'lineCap': 'square',
'lineJoin': 'bevel',
'size': [1, 23, 26]},
{'color': 'rgb(255, 255, 0)',
'lineCap': 'square',
'lineJoin': 'bevel',
'size': 19}],
'type': 'numeric'}
)

self.assertEqual(
FslConverter.vector_renderer_to_fsl(renderer, conversion_context,
layer_opacity=0.5),
{'config': {'numericAttribute': 'my_field',
'steps': [1.0, 2.0, 3.0, 4.0]},
'legend': {'displayName': {'0': 'first range',
'1': 'second range',
'2': 'third range'}},
'style': [{'color': ['rgb(255, 0, 0)', 'rgb(255, 0, 255)',
'rgb(0, 255, 255)'],
'lineCap': 'square',
'lineJoin': 'bevel',
'opacity': 0.5,
'size': [1, 23, 26]},
{'color': 'rgb(255, 255, 0)',
'lineCap': 'square',
'lineJoin': 'bevel',
'opacity': 0.5,
'size': 19}],
'type': 'numeric'}
)


if __name__ == "__main__":
suite = unittest.makeSuite(FslConversionTest)
Expand Down

0 comments on commit 88d5689

Please sign in to comment.