Skip to content

Commit ff10cf4

Browse files
committed
Add balance by area mode
This mode attempts to assign colors in such a way that the total area of features assigned to each color class is balanced. It helps avoid larger features causing a single color class to appear more dominant on a colored map.
1 parent ac5e292 commit ff10cf4

File tree

1 file changed

+45
-17
lines changed

1 file changed

+45
-17
lines changed

python/plugins/processing/algs/qgis/TopoColors.py

+45-17
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727

2828
import os
2929
import operator
30+
from enum import Enum
31+
3032
from collections import defaultdict, deque
3133

3234
from qgis.core import (QgsField,
@@ -37,19 +39,26 @@
3739
from qgis.PyQt.QtCore import (QVariant)
3840

3941
from processing.core.GeoAlgorithm import GeoAlgorithm
40-
from processing.core.parameters import ParameterVector
41-
from processing.core.parameters import ParameterNumber
42+
from processing.core.parameters import (ParameterVector,
43+
ParameterSelection,
44+
ParameterNumber)
4245
from processing.core.outputs import OutputVector
4346
from processing.tools import dataobjects, vector
4447

4548
pluginPath = os.path.split(os.path.split(os.path.dirname(__file__))[0])[0]
4649

50+
class BalanceMethod(Enum):
51+
BY_COUNT = 0
52+
BY_AREA = 1
53+
BY_DISTANCE = 2
4754

4855
class TopoColor(GeoAlgorithm):
4956
INPUT_LAYER = 'INPUT_LAYER'
5057
MIN_COLORS='MIN_COLORS'
58+
BALANCE='BALANCE'
5159
OUTPUT_LAYER = 'OUTPUT_LAYER'
5260

61+
5362
def defineCharacteristics(self):
5463
self.name, self.i18n_name = self.trAlgorithm('Topological coloring')
5564
self.group, self.i18n_group = self.trAlgorithm('Cartographic tools')
@@ -59,13 +68,21 @@ def defineCharacteristics(self):
5968
self.tr('Input layer'), [dataobjects.TYPE_VECTOR_POLYGON]))
6069
self.addParameter(ParameterNumber(self.MIN_COLORS,
6170
self.tr('Minimum number of colors'), 1, 1000, 4))
71+
balance_by = [self.tr('By feature count'),
72+
self.tr('By assigned area'),
73+
self.tr('By distance between colors')]
74+
self.addParameter(ParameterSelection(
75+
self.BALANCE,
76+
self.tr('Balance color assignment'),
77+
balance_by))
6278

6379
self.addOutput(OutputVector(self.OUTPUT_LAYER, self.tr('Colored'), datatype=[dataobjects.TYPE_VECTOR_POLYGON]))
6480

6581
def processAlgorithm(self, feedback):
6682
layer = dataobjects.getObjectFromUri(
6783
self.getParameterValue(self.INPUT_LAYER))
6884
min_colors = self.getParameterValue(self.MIN_COLORS)
85+
balance_by = BalanceMethod(self.getParameterValue(self.BALANCE))
6986

7087
fields = layer.fields()
7188
fields.append(QgsField('color_id', QVariant.Int))
@@ -76,26 +93,25 @@ def processAlgorithm(self, feedback):
7693
layer.wkbType(),
7794
layer.crs())
7895

79-
# use a deque so we can drop features as we write them
80-
# it's a bit friendlier on memory usage
81-
features = deque(f for f in vector.features(layer))
96+
features = {f.id():f for f in vector.features(layer)}
8297

8398
topology, id_graph = self.compute_graph(features, feedback)
84-
feature_colors = ColoringAlgorithm.balanced(topology,
85-
feedback,
99+
feature_colors = ColoringAlgorithm.balanced(features,
100+
balance=balance_by,
101+
graph=topology,
102+
feedback=feedback,
86103
min_colors=min_colors)
87104

88105
max_colors = max(feature_colors.values())
89106
feedback.pushInfo(self.tr('{} colors required').format(max_colors))
90107

91108
total = 20.0 / len(features)
92109
current = 0
93-
while features:
94-
input_feature = features.popleft()
110+
for feature_id, input_feature in features.items():
95111
output_feature = input_feature
96112
attributes = input_feature.attributes()
97-
if input_feature.id() in feature_colors:
98-
attributes.append(feature_colors[input_feature.id()])
113+
if feature_id in feature_colors:
114+
attributes.append(feature_colors[feature_id])
99115
else:
100116
attributes.append(NULL)
101117
output_feature.setAttributes(attributes)
@@ -115,7 +131,7 @@ def compute_graph(features, feedback, create_id_graph=False):
115131
id_graph = Graph(sort_graph=True)
116132

117133
# skip features without geometry
118-
features_with_geometry = dict((f.id(), f) for f in features if f.hasGeometry())
134+
features_with_geometry = { f_id: f for (f_id, f) in features.items() if f.hasGeometry() }
119135

120136
total = 70.0 / len(features_with_geometry)
121137
index = QgsSpatialIndex()
@@ -151,7 +167,7 @@ def compute_graph(features, feedback, create_id_graph=False):
151167
class ColoringAlgorithm:
152168

153169
@staticmethod
154-
def balanced(graph, feedback, min_colors = 4):
170+
def balanced(features, graph, feedback, balance=BalanceMethod.BY_COUNT, min_colors = 4):
155171
feature_colors = {}
156172
# start with minimum number of colors in pool
157173
color_pool = set(range(1, min_colors+1))
@@ -167,8 +183,10 @@ def balanced(graph, feedback, min_colors = 4):
167183
reverse=True)]
168184
# counts for each color already assigned
169185
color_counts = defaultdict(int)
186+
color_areas = defaultdict(float)
170187
for c in color_pool:
171188
color_counts[c] = 0
189+
color_areas[c] = 0
172190

173191
total = 10.0 / len(sorted_by_count)
174192
i = 0
@@ -183,16 +201,26 @@ def balanced(graph, feedback, min_colors = 4):
183201
# from the existing colors, work out which are available (ie non-adjacent)
184202
available_colors = color_pool.difference(adjacent_colors)
185203

204+
feature_color=-1
186205
if len(available_colors) == 0:
187206
# no existing colors available for this feature, so add new color to pool
188207
feature_color = len(color_pool) + 1
189208
color_pool.add(feature_color)
190209
else:
191-
# choose least used available color
192-
counts = [(c, v) for c, v in color_counts.items() if c in available_colors]
193-
feature_color = sorted(counts, key=operator.itemgetter(1))[0][0]
210+
if balance==BalanceMethod.BY_COUNT:
211+
# choose least used available color
212+
counts = [(c, v) for c, v in color_counts.items() if c in available_colors]
213+
feature_color = sorted(counts, key=operator.itemgetter(1))[0][0]
214+
color_counts[feature_color] += 1
215+
elif balance==BalanceMethod.BY_AREA:
216+
areas = [(c, v) for c, v in color_areas.items() if c in available_colors]
217+
feature_color = sorted(areas, key=operator.itemgetter(1))[0][0]
218+
color_areas[feature_color] += features[feature_id].geometry().area()
219+
#elif balance==BalanceMethod.BY_DISTANCE:
220+
221+
194222
feature_colors[feature_id] = feature_color
195-
color_counts[feature_color] += 1
223+
196224

197225
i += 1
198226
feedback.setProgress(70 + int(i * total))

0 commit comments

Comments
 (0)