QGIS API Documentation 4.3.0-Master (9dce731a7a1)
Loading...
Searching...
No Matches
qgsdatadefinedutils.h
Go to the documentation of this file.
1/***************************************************************************
2 qgsdatadefinedutils.h
3 -----------------
4 begin : October 2026
5 copyright : (C) Nyall Dawson
6 email : nyall dot dawson at gmail dot com
7 ***************************************************************************
8 * *
9 * This program is free software; you can redistribute it and/or modify *
10 * it under the terms of the GNU General Public License as published by *
11 * the Free Software Foundation; either version 2 of the License, or *
12 * (at your option) any later version. *
13 * *
14 ***************************************************************************/
15
16#ifndef QGSDATADEFINEDUTILS_H
17#define QGSDATADEFINEDUTILS_H
18
19#include <algorithm>
20#include <array>
21#include <optional>
22
23#include "qgis_core.h"
24
25#define SIP_NO_FILE
26
28template<typename V> struct DataDefinedDecoderEntry
29{
30 QStringView first;
31 V second;
32};
34
44template<typename V, size_t N> class QgsDataDefinedStringDecoder
45{
46 public:
50 constexpr explicit QgsDataDefinedStringDecoder( const DataDefinedDecoderEntry<V> ( &values )[N] )
51 {
52 for ( size_t i = 0; i < N; ++i )
53 {
54 const QStringView key = values[i].first;
55
56 // compile-time assumption checks
57 if ( key.isEmpty() )
58 throw "QgsDataDefinedStringDecoder: keys cannot be empty";
59
60 if ( key[0].unicode() < u'a' || key[0].unicode() > u'z' )
61 throw "QgsDataDefinedStringDecoder: keys must start with a lowercase letter";
62
63 if ( key.size() > MAX_KEY_SIZE )
64 throw "QgsDataDefinedStringDecoder: keys must be less than MAX_KEY_SIZE characters length";
65
66 for ( int j = 0; j < key.size(); ++j )
67 {
68 if ( key[j].unicode() >= u'A' && key[j].unicode() <= u'Z' )
69 throw "QgsDataDefinedStringDecoder: keys must be lowercase";
70
71 if ( shouldIgnoreCharacter( key[j].unicode() ) )
72 throw "QgsDataDefinedStringDecoder: keys must not contain spaces, hyphens, or underscores";
73 }
74
75 mLookupTable[i] = values[i];
76 }
77
78 // sort entries alphabetically
79 std::sort( mLookupTable.begin(), mLookupTable.end(), []( const DataDefinedDecoderEntry<V> &a, const DataDefinedDecoderEntry<V> &b ) { return compareStringViews( a.first, b.first ) < 0; } );
80
81 // now our entries are sorted, lets make sure they are unique too...
82 for ( size_t i = 1; i < N; ++i )
83 {
84 if ( compareStringViews( mLookupTable[i - 1].first, mLookupTable[i].first ) == 0 )
85 throw "QgsDataDefinedStringDecoder: keys must be unique";
86 }
87
88 // this function may be called MANY MANY thousands of times per map render, and for some data-defined
89 // enums there are many keys to lookup. This adds up! We can optimize this a bit by pre-allocating
90 // the keys into alphabetical partitions, so that we only have to compare candidate values to keys
91 // with matching first letters
92 for ( size_t i = 0; i < N; ++i )
93 {
94 // work out what partition this key falls into - the partition index is just the alphabetical index of the first letter
95 const int partitionIndex = getPartitionIndexForChar( mLookupTable[i].first[0].unicode() );
96 if ( partitionIndex < 0 )
97 throw "Something went wrong -- partition index < 0";
98 if ( partitionIndex >= 26 )
99 throw "Something went wrong -- partition index >= 26";
100
101 if ( mEntryPartitions[partitionIndex].count == 0 )
102 {
103 mEntryPartitions[partitionIndex].offset = static_cast<uint16_t>( i );
104 }
105 mEntryPartitions[partitionIndex].count++;
106 }
107 }
108
112 [[nodiscard]] std::optional<V> lookup( QStringView string ) const
113 {
114 if ( string.isEmpty() )
115 return {};
116
117 // clean up the candidate string -- remove whitespace and other unwanted characters
118 std::array<char16_t, MAX_KEY_SIZE> cleanedString;
119 int cleanedStringLength = 0;
120 for ( qsizetype i = 0; i < string.size(); ++i )
121 {
122 char16_t unicode = string[i].unicode();
123 if ( shouldIgnoreCharacter( unicode ) )
124 continue;
125
126 // strings with more than MAX_KEY_SIZE non-ignored characters cannot possibly match anything,
127 // cos we have compile-time assertion that all valid keys are < this length...
128 if ( cleanedStringLength >= MAX_KEY_SIZE )
129 return {};
130
131 static constexpr int UPPER_TO_LOWER_CHAR_OFFSET = 32;
132 if ( unicode >= u'A' && unicode <= u'Z' )
133 unicode += UPPER_TO_LOWER_CHAR_OFFSET;
134
135 cleanedString[cleanedStringLength++] = unicode;
136 }
137
138 if ( cleanedStringLength == 0 )
139 return {};
140
141 // find the corresponding partition
142 const int partitionIndex = getPartitionIndexForChar( cleanedString[0] );
143 if ( partitionIndex < 0 )
144 {
145 // doesn't start with an a-z character, so cannot possibly match any key (since we have
146 // compile-time requirement that keys start with a-z)
147 return {};
148 }
149
150 const Partition &partition = mEntryPartitions[partitionIndex];
151 if ( partition.count == 0 )
152 return {}; // no keys match the start character of the string
153
154 const DataDefinedDecoderEntry<V> *partitionData = mLookupTable.data() + partition.offset;
155 const QStringView cleanedStringView( cleanedString.data(), cleanedStringLength );
156 for ( uint16_t i = 0; i < partition.count; ++i )
157 {
158 if ( partitionData[i].first.size() == cleanedStringLength && partitionData[i].first.compare( cleanedStringView ) == 0 )
159 {
160 return partitionData[i].second;
161 }
162 }
163
164 return {};
165 }
166
167 private:
168 static constexpr int MAX_KEY_SIZE = 32;
169 std::array<DataDefinedDecoderEntry<V>, N> mLookupTable {};
170
171 // we have one partition per a-z character, allowing us to jump immediately to the
172 // candidates with the matching first letter
173 struct Partition
174 {
175 uint16_t offset = 0;
176 uint16_t count = 0;
177 };
178 std::array<Partition, 26> mEntryPartitions {};
179
180 // dang, can't use Qt's QStringView comparison methods, they aren't constexpr :(
181 static constexpr int compareStringViews( QStringView a, QStringView b ) noexcept
182 {
183 for ( qsizetype i = 0; i < std::min( a.size(), b.size() ); ++i )
184 {
185 const char16_t aUnicode = a[i].unicode();
186 const char16_t bUnicode = b[i].unicode();
187 if ( aUnicode < bUnicode )
188 return -1;
189 if ( aUnicode > bUnicode )
190 return 1;
191 }
192 if ( a.size() < b.size() )
193 return -1;
194 if ( a.size() > b.size() )
195 return 1;
196 return 0;
197 }
198
199 static constexpr int getPartitionIndexForChar( char16_t c )
200 {
201 // work out what partition this character falls into - the partition index is just the alphabetical index of the character
202 if ( c >= u'a' && c <= u'z' )
203 {
204 return c - u'a';
205 }
206 else if ( c >= u'A' && c <= u'Z' )
207 {
208 return c - u'A';
209 }
210
211 return -1;
212 }
213
214 static constexpr bool shouldIgnoreCharacter( char16_t unicode ) noexcept
215 {
216 // when matching strings to keys, these characters will be completely ignored.
217 // this gives us tolerant matching, so e.g any of "top-right", "top right", "top_right" will match to the "topright" key
218 return unicode == u' ' || unicode == u'-' || unicode == u'_';
219 }
220};
221
256template<typename V, size_t N> constexpr QgsDataDefinedStringDecoder<V, N> makeDataDefinedStringDecoder( const DataDefinedDecoderEntry<V> ( &values )[N] )
257{
258 return QgsDataDefinedStringDecoder<V, N>( values );
259}
260
261#endif // QGSDATADEFINEDUTILS_H
A template based class for efficiently decoding a string value to a corresponding enum value.
constexpr QgsDataDefinedStringDecoder(const DataDefinedDecoderEntry< V >(&values)[N])
Constructor for QgsDataDefinedStringDecoder.
std::optional< V > lookup(QStringView string) const
Looks up a string in the lookup table, returning the corresponding enum value if found.
As part of the API refactoring and improvements which landed in the Processing API was substantially reworked from the x version This was done in order to allow much of the underlying Processing framework to be ported into c
constexpr QgsDataDefinedStringDecoder< V, N > makeDataDefinedStringDecoder(const DataDefinedDecoderEntry< V >(&values)[N])
Creates a QgsDataDefinedStringDecoder from a hardcoded lookup table.