QGIS API Documentation 4.3.0-Master (c8cdfe19d7b)
Loading...
Searching...
No Matches
qgsesriarcadeconverter.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgsesriarcadeconverter.h
3 --------------------
4 begin : October 2026
5 copyright : (C) 2026 by 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 ***************************************************************************/
16
17#include "qgsesriarcadescript.h"
18#include "qgsexception.h"
19#include "qgsexpression.h"
20
21#include <QString>
22
23using namespace Qt::StringLiterals;
24
26class ArcadeToExpressionVisitor : public QgsEsriArcadeRecursiveVisitor
27{
28 public:
30
31 static const QString NEXT_BLOCK_MARKER;
32
33 QString mCurrentPart;
34 bool mCurrentPartIsLiteral = false;
35 bool mCurrentPartIsSimple = false;
36
37 void visit( const QgsEsriArcadeNode &node )
38 {
39 if ( auto block = dynamic_cast< const QgsEsriArcadeBlockStatement * >( &node ) )
40 {
41 visit( *block );
42 }
43 else if ( auto varDeclare = dynamic_cast< const QgsEsriArcadeVariableDeclarationStatement * >( &node ) )
44 {
45 visit( *varDeclare );
46 }
47 else if ( auto varAssign = dynamic_cast< const QgsEsriArcadeAssignmentStatement * >( &node ) )
48 {
49 visit( *varAssign );
50 }
51 else if ( auto returnStatement = dynamic_cast< const QgsEsriArcadeReturnStatement * >( &node ) )
52 {
53 visit( *returnStatement );
54 }
55 else if ( auto functionDef = dynamic_cast< const QgsEsriArcadeFunctionDeclarationStatement * >( &node ) )
56 {
57 visit( *functionDef );
58 }
59 else if ( auto expression = dynamic_cast< const QgsEsriArcadeExpressionStatement * >( &node ) )
60 {
61 visit( *expression );
62 }
63 else if ( auto literal = dynamic_cast< const QgsEsriArcadeLiteralExpression * >( &node ) )
64 {
65 visit( *literal );
66 }
67 else if ( auto variableReference = dynamic_cast< const QgsEsriArcadeVariableReferenceExpression * >( &node ) )
68 {
69 visit( *variableReference );
70 }
71 else if ( auto profileVariableReference = dynamic_cast< const QgsEsriArcadeProfileVariableReferenceExpression * >( &node ) )
72 {
73 visit( *profileVariableReference );
74 }
75 else if ( auto memberAccess = dynamic_cast< const QgsEsriArcadeMemberAccessExpression * >( &node ) )
76 {
77 visit( *memberAccess );
78 }
79 else if ( auto binaryOperator = dynamic_cast< const QgsEsriArcadeBinaryOperatorExpression * >( &node ) )
80 {
81 visit( *binaryOperator );
82 }
83 else if ( auto unaryOperator = dynamic_cast< const QgsEsriArcadeUnaryOperatorExpression * >( &node ) )
84 {
85 visit( *unaryOperator );
86 }
87 else if ( auto functionCall = dynamic_cast< const QgsEsriArcadeFunctionCallExpression * >( &node ) )
88 {
89 visit( *functionCall );
90 }
91 else if ( auto index = dynamic_cast< const QgsEsriArcadeIndexExpression * >( &node ) )
92 {
93 visit( *index );
94 }
95 else if ( auto arrayLiteral = dynamic_cast< const QgsEsriArcadeArrayLiteralExpression * >( &node ) )
96 {
97 visit( *arrayLiteral );
98 }
99 else if ( auto dictionaryLiteral = dynamic_cast< const QgsEsriArcadeDictionaryLiteralExpression * >( &node ) )
100 {
101 visit( *dictionaryLiteral );
102 }
103 else if ( auto ifStatement = dynamic_cast< const QgsEsriArcadeIfStatement * >( &node ) )
104 {
105 visit( *ifStatement );
106 }
107 else if ( auto forStatement = dynamic_cast< const QgsEsriArcadeForStatement * >( &node ) )
108 {
109 visit( *forStatement );
110 }
111 else if ( auto forInStatement = dynamic_cast< const QgsEsriArcadeForInStatement * >( &node ) )
112 {
113 visit( *forInStatement );
114 }
115 else if ( auto whileStatement = dynamic_cast< const QgsEsriArcadeWhileStatement * >( &node ) )
116 {
117 visit( *whileStatement );
118 }
119 else if ( auto breakStatement = dynamic_cast< const QgsEsriArcadeBreakStatement * >( &node ) )
120 {
121 visit( *breakStatement );
122 }
123 else if ( auto continueStatement = dynamic_cast< const QgsEsriArcadeContinueStatement * >( &node ) )
124 {
125 visit( *continueStatement );
126 }
127 else
128 {
129 throw QgsNotSupportedException( QObject::tr( "An unexpected node was encountered while converting to a QGIS expression." ) );
130 }
131 }
132
133 void visit( const QgsEsriArcadeBlockStatement &node ) override
134 {
135 if ( node.statements().empty() )
136 {
138 throw QgsNotSupportedException( QObject::tr( "An empty script block was encountered while converting to a QGIS expression." ) );
139 }
140 else if ( node.statements().size() > 1 )
141 {
143 throw QgsNotSupportedException( QObject::tr( "Scripts containing multiple statements cannot be converted to QGIS expressions." ) );
144 }
145
146 QgsEsriArcadeStatement *firstStatement = node.statements().at( 0 );
147 if ( !firstStatement )
148 {
150 throw QgsNotSupportedException( QObject::tr( "An empty script block was encountered while converting to a QGIS expression." ) );
151 }
152 visit( *firstStatement );
153 }
154
155 void visit( const QgsEsriArcadeVariableDeclarationStatement & ) override
156 {
158 throw QgsNotSupportedException( QObject::tr( "Scripts containing variable declarations cannot be converted to QGIS expressions." ) );
159 }
160
161 void visit( const QgsEsriArcadeAssignmentStatement & ) override
162 {
164 throw QgsNotSupportedException( QObject::tr( "Scripts containing variable assignments cannot be converted to QGIS expressions." ) );
165 }
166
167 void visit( const QgsEsriArcadeFunctionDeclarationStatement & ) override
168 {
170 throw QgsNotSupportedException( QObject::tr( "Scripts containing function definitions cannot be converted to QGIS expressions." ) );
171 }
172
173 void visit( const QgsEsriArcadeReturnStatement &node ) override
174 {
175 if ( QgsEsriArcadeExpression *expression = node.expression() )
176 {
177 visit( *expression );
178 }
179 else
180 {
181 mCurrentPart = u"NULL"_s;
182 mCurrentPartIsLiteral = false;
183 mCurrentPartIsSimple = true;
184 }
185 }
186
187 void visit( const QgsEsriArcadeExpressionStatement &node ) override
188 {
189 if ( QgsEsriArcadeExpression *expression = node.expression() )
190 {
191 visit( *expression );
192 }
193 else
194 {
195 mCurrentPart = u"NULL"_s;
196 mCurrentPartIsLiteral = false;
197 mCurrentPartIsSimple = true;
198 }
199 }
200
201 void visit( const QgsEsriArcadeLiteralExpression &node ) override
202 {
203 const QVariant value = node.value();
204 if ( value.toString() == "\n" )
205 {
206 mCurrentPart = u"'\\n'"_s;
207 mCurrentPartIsLiteral = false;
208 }
209 else if ( value.type() == QVariant::String )
210 {
211 mCurrentPart = QgsExpression::quotedString( value.toString() );
212 mCurrentPartIsLiteral = true;
213 }
214 else if ( QgsVariantUtils::isNull( value ) )
215 {
216 mCurrentPart = u"NULL"_s;
217 mCurrentPartIsLiteral = false;
218 }
219 else
220 {
221 mCurrentPart = value.toString();
222 mCurrentPartIsLiteral = true;
223 }
224 mCurrentPartIsSimple = true;
225 }
226
227 void visit( const QgsEsriArcadeVariableReferenceExpression &node ) override
228 {
229 const QString variableName = node.name();
230 if ( variableName.isEmpty() )
231 {
233 throw QgsNotSupportedException( QObject::tr( "An empty variable reference was encountered while converting to a QGIS expression." ) );
234 }
235 mCurrentPart = u"@%1"_s.arg( variableName );
236 mCurrentPartIsLiteral = false;
237 mCurrentPartIsSimple = true;
238 }
239
240 void visit( const QgsEsriArcadeProfileVariableReferenceExpression &node ) override
241 {
242 const QString variableName = node.name();
243 if ( variableName.compare( "$feature"_L1, Qt::CaseInsensitive ) == 0 )
244 {
245 mCurrentPart = u"@feature"_s;
246 mCurrentPartIsLiteral = false;
247 mCurrentPartIsSimple = true;
248 }
249 else if ( variableName.compare( "$view"_L1, Qt::CaseInsensitive ) == 0 )
250 {
251 // this will get caught in QgsEsriArcadeMemberAccessExpression visitor and translated to @map_ variables
252 mCurrentPart = variableName;
253 mCurrentPartIsLiteral = false;
254 mCurrentPartIsSimple = true;
255 }
256 else
257 {
259 throw QgsNotSupportedException( QObject::tr( "Converting the %1 profile variable is not supported" ).arg( variableName ) );
260 }
261 }
262
263 void visit( const QgsEsriArcadeMemberAccessExpression &node ) override
264 {
265 if ( QgsEsriArcadeExpression *container = node.containerExpression() )
266 {
267 visit( *container );
268 }
269 else
270 {
272 throw QgsNotSupportedException( QObject::tr( "Member access to an empty container was encountered while converting to a QGIS expression." ) );
273 }
274
275 if ( mCurrentPart.compare( "@feature"_L1, Qt::CaseInsensitive ) == 0 )
276 {
277 mCurrentPart = QgsExpression::quotedColumnRef( node.memberName() );
278 mCurrentPartIsLiteral = false;
279 mCurrentPartIsSimple = true;
280 }
281 else if ( mCurrentPart.compare( "$view"_L1, Qt::CaseInsensitive ) == 0 )
282 {
283 static const QMap< QString, QString > sVIEW_VARS {
284 { u"scale"_s, u"@map_scale"_s },
285 };
286 auto it = sVIEW_VARS.constFind( node.memberName().toLower() );
287 if ( it != sVIEW_VARS.constEnd() )
288 {
289 mCurrentPart = it.value();
290 mCurrentPartIsLiteral = false;
291 mCurrentPartIsSimple = true;
292 }
293 else
294 {
296 throw QgsNotSupportedException( QObject::tr( "Converting the %1 view member is not supported" ).arg( node.memberName() ) );
297 }
298 }
299 else
300 {
302 throw QgsNotSupportedException( QObject::tr( "Converting the member access operator in this expression is not supported" ) );
303 }
304 }
305
306 void visit( const QgsEsriArcadeBinaryOperatorExpression &node ) override
307 {
308 if ( !node.leftOperand() || !node.rightOperand() )
309 {
311 throw QgsNotSupportedException( QObject::tr( "A binary operator with missing operands was encountered while converting to a QGIS expression." ) );
312 }
313
314 bool isChainableOp = false;
315 switch ( node.binaryOperator() )
316 {
320 isChainableOp = true;
321 break;
322
339 break;
340 }
341
342 bool leftChainable = false;
343 bool rightChainable = false;
344 if ( isChainableOp )
345 {
346 if ( dynamic_cast< QgsEsriArcadeBinaryOperatorExpression * >( node.leftOperand() )
347 && dynamic_cast< QgsEsriArcadeBinaryOperatorExpression * >( node.leftOperand() )->binaryOperator() == node.binaryOperator() )
348 {
349 leftChainable = true;
350 }
351 if ( dynamic_cast< QgsEsriArcadeBinaryOperatorExpression * >( node.rightOperand() )
352 && dynamic_cast< QgsEsriArcadeBinaryOperatorExpression * >( node.rightOperand() )->binaryOperator() == node.binaryOperator() )
353 {
354 rightChainable = true;
355 }
356 }
357
358 visit( *node.leftOperand() );
359 QString newOpLeftPart = mCurrentPart;
360 const bool leftIsSimple = mCurrentPartIsSimple;
361
362 visit( *node.rightOperand() );
363 QString newOpRightChunk = mCurrentPart;
364 const bool rightIsSimple = mCurrentPartIsSimple;
365
366 // special case, testing against null
367 if ( newOpRightChunk.compare( "NULL"_L1, Qt::CaseInsensitive ) == 0 )
368 {
370 {
371 if ( !leftIsSimple )
372 {
373 newOpLeftPart = u"(%1)"_s.arg( newOpLeftPart );
374 }
375 mCurrentPart = u"%1 %2 NULL"_s.arg( newOpLeftPart, node.binaryOperator() == QgsEsriArcadeBinaryOperatorExpression::BinaryOperator::Equal ? u"IS"_s : u"IS NOT"_s );
376 mCurrentPartIsLiteral = false;
377 mCurrentPartIsSimple = false;
378 return;
379 }
380 }
381
382 static const QMap< QgsEsriArcadeBinaryOperatorExpression::BinaryOperator, QString > sBINARY_OPERATOR_MAP {
396 };
397
398 mCurrentPartIsSimple = false;
399 auto it = sBINARY_OPERATOR_MAP.constFind( node.binaryOperator() );
400 if ( it != sBINARY_OPERATOR_MAP.constEnd() )
401 {
402 if ( !leftIsSimple && !leftChainable )
403 {
404 newOpLeftPart = u"(%1)"_s.arg( newOpLeftPart );
405 }
406 if ( !rightIsSimple && !rightChainable )
407 {
408 newOpRightChunk = u"(%1)"_s.arg( newOpRightChunk );
409 }
410 mCurrentPart = u"%1 %2 %3"_s.arg( newOpLeftPart, it.value(), newOpRightChunk );
411 mCurrentPartIsLiteral = false;
412 mCurrentPartIsSimple = false;
413 }
414 else
415 {
417 throw QgsNotSupportedException( QObject::tr( "Converting the %1 operator is not supported" ).arg( node.text() ) );
418 }
419 }
420
421 void visit( const QgsEsriArcadeUnaryOperatorExpression &node ) override
422 {
423 if ( !node.operand() )
424 {
426 throw QgsNotSupportedException( QObject::tr( "A unary operator with missing operand was encountered while converting to a QGIS expression." ) );
427 }
428
429 visit( *node.operand() );
430
431 static const QMap< QgsEsriArcadeUnaryOperatorExpression::UnaryOperator, QString > sUNARY_OPERATOR_MAP {
435 };
436
437 auto it = sUNARY_OPERATOR_MAP.constFind( node.unaryOperator() );
438 if ( it != sUNARY_OPERATOR_MAP.constEnd() )
439 {
440 QString part = mCurrentPart;
441 if ( !mCurrentPartIsSimple )
442 {
443 part = u"(%1)"_s.arg( part );
444 }
445 mCurrentPart = u"%1%2"_s.arg( it.value(), part );
446 mCurrentPartIsLiteral = false;
447 mCurrentPartIsSimple = false;
448 }
449 else
450 {
452 throw QgsNotSupportedException( QObject::tr( "Converting the %1 operator is not supported" ).arg( node.text() ) );
453 }
454 }
455
456 void visit( const QgsEsriArcadeFunctionCallExpression &node ) override
457 {
458 const QString functionName = node.functionName().toLower();
459 if ( functionName.isEmpty() )
460 {
462 throw QgsNotSupportedException( QObject::tr( "An empty function name was encountered while converting to a QGIS expression." ) );
463 }
464 QStringList convertedArguments;
465 QList< bool > convertedArgumentsAreSimple;
466 QList< bool > convertedArgumentsAreLiteral;
467
468 for ( QgsEsriArcadeExpression *arg : node.arguments() )
469 {
470 if ( !arg )
471 {
473 throw QgsNotSupportedException( QObject::tr( "An empty function argument encountered while converting to a QGIS expression." ) );
474 }
475 visit( *arg );
476 convertedArguments.append( mCurrentPart );
477 convertedArgumentsAreSimple.append( mCurrentPartIsSimple );
478 convertedArgumentsAreLiteral.append( mCurrentPartIsLiteral );
479 }
480
481 if ( functionName.compare( "date"_L1, Qt::CaseInsensitive ) == 0 )
482 {
484 throw QgsNotSupportedException( QObject::tr( "The Arcade Date function cannot be automatically converted to a QGIS expression." ) );
485 }
486 else if ( functionName.compare( "length"_L1, Qt::CaseInsensitive ) == 0 )
487 {
489 throw QgsNotSupportedException( QObject::tr( "The Arcade Length function cannot be automatically converted to a QGIS expression." ) );
490 }
491 else if ( functionName.compare( "domainname"_L1, Qt::CaseInsensitive ) == 0 )
492 {
493 if ( convertedArguments.size() == 2 && convertedArguments.at( 0 ) == "@feature"_L1 )
494 {
495 if ( convertedArgumentsAreLiteral[1] )
496 {
497 if ( convertedArguments[1].size() < 2 )
498 {
500 throw QgsNotSupportedException( QObject::tr( "A domainname function with invalid name argument was encountered while converting to a QGIS expression." ) );
501 }
502 mCurrentPart = u"represent_value(%1)"_s.arg( QgsExpression::quotedColumnRef( convertedArguments[1].mid( 1, convertedArguments[1].length() - 2 ) ) );
503 mCurrentPartIsLiteral = false;
504 mCurrentPartIsSimple = true;
505 return;
506 }
507 }
508
510 throw QgsNotSupportedException( QObject::tr( "Complex forms of the Arcade DomainName function cannot be automatically converted to QGIS expressions." ) );
511 }
512 else if ( functionName.compare( "when"_L1, Qt::CaseInsensitive ) == 0 )
513 {
514 if ( convertedArguments.size() < 2 )
515 {
517 throw QgsNotSupportedException( QObject::tr( "When function with < 2 arguments encountered while converting to a QGIS expression." ) );
518 }
519 QString res = u"CASE WHEN %1 THEN %2"_s.arg( convertedArguments.value( 0 ), convertedArguments.value( 1 ) );
520 if ( convertedArguments.size() > 2 )
521 {
522 int i = 2;
523 while ( true )
524 {
525 if ( i >= convertedArguments.size() - 1 )
526 break;
527 res += u" WHEN %1 THEN %2"_s.arg( convertedArguments.value( i ), convertedArguments.value( i + 1 ) );
528 i += 2;
529 }
530 }
531
532 if ( convertedArguments.size() % 2 == 1 )
533 {
534 res += u" ELSE %1"_s.arg( convertedArguments.value( convertedArguments.size() - 1 ) );
535 }
536
537 res += " END"_L1;
538
539 mCurrentPart = res;
540 mCurrentPartIsLiteral = false;
541 mCurrentPartIsSimple = false;
542 return;
543 }
544 else if ( functionName.compare( "decode"_L1, Qt::CaseInsensitive ) == 0 )
545 {
546 if ( convertedArguments.size() < 2 )
547 {
549 throw QgsNotSupportedException( QObject::tr( "A decode function with < 2 arguments was encountered while converting to a QGIS expression." ) );
550 }
551 QStringList mapGetParts;
552 int i = 1;
553 while ( true )
554 {
555 if ( i >= convertedArguments.size() - 1 )
556 break;
557
558 // condition
559 mapGetParts.append( convertedArguments.at( i ) );
560 // value
561 mapGetParts.append( convertedArguments.at( i + 1 ) );
562 i += 2;
563 }
564
565 const QString mapGetPart = u"map_get(map(%1), %2)"_s.arg( mapGetParts.join( ", " ), convertedArguments.at( 0 ) );
566
567 QString res = mapGetPart;
568 if ( convertedArguments.size() % 2 == 0 )
569 {
570 res = u"coalesce(%1, %2)"_s.arg( mapGetPart, convertedArguments.at( convertedArguments.size() - 1 ) );
571 }
572 mCurrentPart = res;
573 mCurrentPartIsLiteral = false;
574 mCurrentPartIsSimple = false;
575 return;
576 }
577 else if ( functionName.compare( "isempty"_L1, Qt::CaseInsensitive ) == 0 )
578 {
579 if ( convertedArguments.size() == 1 )
580 {
581 if ( convertedArgumentsAreSimple[0] )
582 {
583 mCurrentPart = u"%1 IS NULL"_s.arg( convertedArguments[0] );
584 }
585 else
586 {
587 mCurrentPart = u"(%1) IS NULL"_s.arg( convertedArguments[0] );
588 }
589 mCurrentPartIsLiteral = false;
590 mCurrentPartIsSimple = false;
591 return;
592 }
593
595 throw QgsNotSupportedException( QObject::tr( "This form of the Arcade IsEmpty function cannot be automatically converted to QGIS expressions." ) );
596 }
597 else if ( functionName.compare( "mid"_L1, Qt::CaseInsensitive ) == 0 )
598 {
599 if ( convertedArguments.empty() )
600 {
602 throw QgsNotSupportedException( QObject::tr( "A mid function with no arguments was encountered while converting to a QGIS expression." ) );
603 }
604 QString start = convertedArguments.value( 1 );
605 bool intOk = false;
606 int intStart = start.toInt( &intOk );
607 if ( convertedArgumentsAreLiteral.value( 1 ) && intOk )
608 {
609 start = QString::number( intStart + 1 );
610 }
611 else
612 {
613 start = u"%1 + 1"_s.arg( start );
614 }
615
616 if ( convertedArguments.size() == 2 )
617 {
618 mCurrentPart = u"substr(%1, %2)"_s.arg( convertedArguments.value( 0 ), start );
619 }
620 else if ( convertedArguments.size() == 3 )
621 {
622 mCurrentPart = u"substr(%1, %2, %3)"_s.arg( convertedArguments.value( 0 ), start, convertedArguments.value( 2 ) );
623 }
624 else
625 {
627 throw QgsNotSupportedException( QObject::tr( "This form of the 'mid' function cannot be converted to a QGIS expression." ) );
628 }
629 mCurrentPartIsLiteral = false;
630 mCurrentPartIsSimple = true;
631 return;
632 }
633 else if ( functionName.compare( "number"_L1, Qt::CaseInsensitive ) == 0 )
634 {
635 if ( convertedArguments.size() == 1 )
636 {
637 mCurrentPart = u"to_real(%1)"_s.arg( convertedArguments.at( 0 ) );
638 mCurrentPartIsLiteral = false;
639 mCurrentPartIsSimple = true;
640 return;
641 }
642 else
643 {
645 throw QgsNotSupportedException( QObject::tr( "This form of the 'number' function cannot be converted to a QGIS expression." ) );
646 }
647 }
648 else if ( functionName.compare( "text"_L1, Qt::CaseInsensitive ) == 0 )
649 {
650 if ( convertedArguments.size() == 1 )
651 {
652 mCurrentPart = u"to_string(%1)"_s.arg( convertedArguments.at( 0 ) );
653 mCurrentPartIsLiteral = false;
654 mCurrentPartIsSimple = true;
655 return;
656 }
657 else
658 {
660 throw QgsNotSupportedException( QObject::tr( "This form of the 'text' function cannot be converted to a QGIS expression." ) );
661 }
662 }
663 else if ( functionName.compare( "split"_L1, Qt::CaseInsensitive ) == 0 )
664 {
665 if ( convertedArguments.size() == 2 )
666 {
667 mCurrentPart = u"string_to_array(%1, %2)"_s.arg( convertedArguments.at( 0 ), convertedArguments.at( 1 ) );
668 mCurrentPartIsLiteral = false;
669 mCurrentPartIsSimple = true;
670 return;
671 }
672 else
673 {
675 throw QgsNotSupportedException( QObject::tr( "This form of the 'split' function cannot be converted to a QGIS expression." ) );
676 }
677 }
678 else if ( functionName.compare( "concatenate"_L1, Qt::CaseInsensitive ) == 0 )
679 {
680 if ( convertedArguments.size() == 1 )
681 {
682 mCurrentPart = u"array_to_string(%1)"_s.arg( convertedArguments.at( 0 ) );
683 mCurrentPartIsLiteral = false;
684 mCurrentPartIsSimple = true;
685 return;
686 }
687 else if ( convertedArguments.size() == 2 )
688 {
689 mCurrentPart = u"array_to_string(%1, %2)"_s.arg( convertedArguments.at( 0 ), convertedArguments.at( 1 ) );
690 mCurrentPartIsLiteral = false;
691 mCurrentPartIsSimple = true;
692 return;
693 }
694 else
695 {
697 throw QgsNotSupportedException( QObject::tr( "This form of the 'concatenate' function cannot be converted to a QGIS expression." ) );
698 }
699 }
700 else if ( functionName.compare( "find"_L1, Qt::CaseInsensitive ) == 0 )
701 {
702 if ( convertedArguments.size() == 2 )
703 {
704 mCurrentPart = u"strpos(%1, %2)"_s.arg( convertedArguments.at( 0 ), convertedArguments.at( 1 ) );
705 mCurrentPartIsLiteral = false;
706 mCurrentPartIsSimple = true;
707 return;
708 }
709 else if ( convertedArguments.size() == 3 )
710 {
711 bool arg2IsInt = false;
712 const int arg2Int = convertedArguments.at( 2 ).toInt( &arg2IsInt );
713 if ( arg2IsInt && arg2Int == 0 )
714 {
715 mCurrentPart = u"strpos(%1, %2)"_s.arg( convertedArguments.at( 0 ), convertedArguments.at( 1 ) );
716 mCurrentPartIsSimple = true;
717 }
718 else
719 {
720 mCurrentPart = u"strpos(substr(%1, %2), %3) + %4"_s.arg( convertedArguments.at( 0 ), convertedArguments.at( 2 ), convertedArguments.at( 1 ), convertedArguments.at( 2 ) );
721 mCurrentPartIsSimple = false;
722 }
723
724 mCurrentPartIsLiteral = false;
725 return;
726 }
727 else
728 {
730 throw QgsNotSupportedException( QObject::tr( "This form of the 'find' function cannot be converted to a QGIS expression." ) );
731 }
732 }
733 else if ( functionName.compare( "replace"_L1, Qt::CaseInsensitive ) == 0 )
734 {
735 if ( convertedArguments.size() == 3 )
736 {
737 mCurrentPart = u"replace(%1, %2, %3)"_s.arg( convertedArguments.at( 0 ), convertedArguments.at( 1 ), convertedArguments.at( 2 ) );
738 mCurrentPartIsLiteral = false;
739 mCurrentPartIsSimple = true;
740 return;
741 }
742 else
743 {
745 throw QgsNotSupportedException( QObject::tr( "This form of the 'replace' function cannot be converted to a QGIS expression." ) );
746 }
747 }
748
749 // these functions are a direct match between Esri and QGIS behavior
750 static const QMap< QString, QString > sARCADE_FUNCTIONS {
751 { "upper", "upper" },
752 { "lower", "lower" },
753 { "proper", "title" },
754 { "defaultvalue", "coalesce" },
755 { "round", "round" },
756 { "iif", "if" },
757 { "count", "length" },
758 { "right", "right" },
759 { "left", "left" },
760 { "trim", "trim" },
761 { "includes", "array_contains" },
762 { "max", "max" },
763 { "min", "min" },
764 { "abs", "abs" },
765 { "acos", "acos" },
766 { "asin", "asin" },
767 { "atan", "atan" },
768 { "atan2", "atan2" },
769 { "cos", "cos" },
770 { "exp", "exp" },
771 { "random", "randf" },
772 { "sin", "sin" },
773 { "sqrt", "sqrt" },
774 { "tan", "tan" },
775 };
776
777 auto it = sARCADE_FUNCTIONS.constFind( functionName );
778 if ( it != sARCADE_FUNCTIONS.constEnd() )
779 {
780 if ( !convertedArguments.empty() )
781 {
782 mCurrentPart = u"%1(%2)"_s.arg( it.value(), convertedArguments.join( ", " ) );
783 mCurrentPartIsLiteral = false;
784 }
785 else
786 {
787 mCurrentPart = u"%1()"_s.arg( it.value() );
788 mCurrentPartIsLiteral = false;
789 }
790 mCurrentPartIsSimple = true;
791 }
792 else
793 {
795 throw QgsNotSupportedException( QObject::tr( "The %1 function cannot be converted to a QGIS expression." ).arg( functionName ) );
796 }
797 }
798
799 void visit( const QgsEsriArcadeIndexExpression &node ) override
800 {
801 QgsEsriArcadeExpression *container = node.containerExpression();
802 if ( !container )
803 {
805 throw QgsNotSupportedException( QObject::tr( "An index operator with no container was encountered while converting to a QGIS expression." ) );
806 }
807 visit( *container );
808 const QString containerPart = mCurrentPart;
809
810 QgsEsriArcadeExpression *index = node.indexExpression();
811 if ( !index )
812 {
814 throw QgsNotSupportedException( QObject::tr( "An index operator with no index was encountered while converting to a QGIS expression." ) );
815 }
816 visit( *index );
817
818 if ( containerPart.compare( "@feature"_L1, Qt::CaseInsensitive ) == 0 )
819 {
820 if ( mCurrentPartIsLiteral )
821 {
822 static const QMap< QString, QString > sSPECIAL_FEATURE_ATTRIBUTES {
823 { "shape.stlength()", "length(@geometry)" },
824 { "shape.starea()", "area(@geometry)" },
825 };
826
827 const QString candidate = mCurrentPart.size() > 2 ? mCurrentPart.toLower().mid( 1, mCurrentPart.length() - 2 ) : QString();
828 auto it = sSPECIAL_FEATURE_ATTRIBUTES.constFind( candidate );
829 if ( it != sSPECIAL_FEATURE_ATTRIBUTES.constEnd() )
830 {
831 mCurrentPart = it.value();
832 }
833 else
834 {
835 if ( mCurrentPart.startsWith( "'" ) && mCurrentPart.size() > 2 )
836 {
837 mCurrentPart = mCurrentPart.mid( 1, mCurrentPart.length() - 2 );
838 }
839 mCurrentPart = QgsExpression::quotedColumnRef( mCurrentPart );
840 }
841
842 mCurrentPartIsLiteral = false;
843 mCurrentPartIsSimple = true;
844 return;
845 }
846 else
847 {
849 throw QgsNotSupportedException( QObject::tr( "This script cannot be converted to a QGIS expression." ) );
850 }
851 }
852 else if ( mCurrentPartIsLiteral )
853 {
854 bool ok = false;
855 const int intValue = mCurrentPart.toInt( &ok );
856 if ( ok )
857 {
858 mCurrentPart = u"%1[%2]"_s.arg( mCurrentPart ).arg( intValue );
859 mCurrentPartIsLiteral = false;
860 mCurrentPartIsSimple = true;
861 return;
862 }
863 else
864 {
866 throw QgsNotSupportedException( QObject::tr( "This script cannot be converted to a QGIS expression." ) );
867 }
868 }
869 else
870 {
872 throw QgsNotSupportedException( QObject::tr( "This script cannot be converted to a QGIS expression." ) );
873 }
874 }
875
876 void visit( const QgsEsriArcadeArrayLiteralExpression &node ) override
877 {
878 QStringList parts;
879 for ( QgsEsriArcadeExpression *element : node.elements() )
880 {
881 if ( !element )
882 {
884 throw QgsNotSupportedException( QObject::tr( "An array literal with a missing element was encountered while converting to a QGIS expression." ) );
885 }
886 visit( *element );
887 parts.append( mCurrentPart );
888 }
889
890 mCurrentPart = u"array(%1)"_s.arg( parts.join( ", "_L1 ) );
891 mCurrentPartIsLiteral = false;
892 mCurrentPartIsSimple = true;
893 }
894
895 void visit( const QgsEsriArcadeDictionaryLiteralExpression & ) override
896 {
898 throw QgsNotSupportedException( QObject::tr( "Converting dictionary literals is supported" ) );
899 }
900
901 void visit( const QgsEsriArcadeIfStatement &node ) override
902 {
903 QgsEsriArcadeExpression *conditionExpression = node.condition();
904 if ( !conditionExpression )
905 {
907 throw QgsNotSupportedException( QObject::tr( "An if statement with no condition was encountered while converting to a QGIS expression." ) );
908 }
909 visit( *conditionExpression );
910 const QString condition = mCurrentPart;
911
912 QgsEsriArcadeBlockStatement *trueBlock = node.trueBlock();
913 if ( !trueBlock )
914 {
916 throw QgsNotSupportedException( QObject::tr( "An if statement with no true block was encountered while converting to a QGIS expression." ) );
917 }
918 visit( *trueBlock );
919 const QString whenTrue = mCurrentPart;
920
921 QString whenFalse;
922 if ( QgsEsriArcadeBlockStatement *falseBlock = node.falseBlock() )
923 {
924 visit( *falseBlock );
925 whenFalse = mCurrentPart;
926 mCurrentPart = u"if(%1, %2, %3)"_s.arg( condition, whenTrue, whenFalse );
927 }
928 else
929 {
930 mCurrentPart = u"if(%1, %2, %3)"_s.arg( condition, whenTrue, NEXT_BLOCK_MARKER );
931 }
932
933 mCurrentPartIsLiteral = false;
934 mCurrentPartIsSimple = true;
935 }
936
937 void visit( const QgsEsriArcadeForStatement & ) override
938 {
940 throw QgsNotSupportedException( QObject::tr( "Scripts containing for loops cannot be converted to QGIS expressions." ) );
941 }
942
943 void visit( const QgsEsriArcadeForInStatement & ) override
944 {
946 throw QgsNotSupportedException( QObject::tr( "Scripts containing for loops cannot be converted to QGIS expressions." ) );
947 }
948
949 void visit( const QgsEsriArcadeWhileStatement & ) override
950 {
952 throw QgsNotSupportedException( QObject::tr( "Scripts containing while loops cannot be converted to QGIS expressions." ) );
953 }
954
955 void visit( const QgsEsriArcadeBreakStatement & ) override
956 {
958 throw QgsNotSupportedException( QObject::tr( "Scripts containing break statements cannot be converted to QGIS expressions." ) );
959 }
960
961 void visit( const QgsEsriArcadeContinueStatement & ) override
962 {
964 throw QgsNotSupportedException( QObject::tr( "Scripts containing continue statements cannot be converted to QGIS expressions." ) );
965 }
966};
967
968const QString ArcadeToExpressionVisitor::NEXT_BLOCK_MARKER = u"!!!!!NEXT_BLOCK!!!!!"_s;
969
971
972
973//
974// QgsEsriArcadeConverter
975//
976
978 : mScript( script )
979{
980 QgsEsriArcadeScript parsedScript = QgsEsriArcadeScript( script );
981 if ( parsedScript.hasParserError() )
982 {
983 mErrorString = QObject::tr( "The Arcade script could not be parsed." );
984 mResult = Result::ParserError;
985 return;
986 }
987
988 ArcadeToExpressionVisitor visitor;
989 try
990 {
991 parsedScript.acceptVisitor( visitor );
992 mExpression = visitor.mCurrentPart;
993 }
994 catch ( QgsNotSupportedException &ex )
995 {
996 mErrorString = ex.what();
997 mResult = visitor.mResult;
998 }
999}
QList< QgsEsriArcadeExpression * > elements() const
Returns the list of element expressions.
BinaryOperator binaryOperator() const
Returns the binary operator.
@ BitwiseSignedRightShift
Bitwise right shift, eg "7 >> 14".
@ BitwiseUnsignedRightShift
Bitwise unsigned right shift, eg "7 >>> 14".
QgsEsriArcadeExpression * rightOperand() const
Returns the right-hand operand expression.
QgsEsriArcadeExpression * leftOperand() const
Returns the left-hand operand expression.
QString text() const
Returns text representation of the operator.
QList< QgsEsriArcadeStatement * > statements() const
Returns the list of statements contained within this block.
Result
Results of converting the Arcade script.
@ InvalidScriptMultiStatements
Scripts containing multiple statements cannot be converted.
@ Success
Script was successfully converted.
@ ParserError
Script could not be parsed.
@ UnknownError
An unknown error occurred while converting the script.
@ InvalidScriptIncompatible
Script is incompatible with conversion to a QGIS expression.
QgsEsriArcadeConverter(const QString &script)
Constructor for QgsEsriArcadeConverter for converting the specified script.
QgsEsriArcadeExpression * expression() const
Returns the underlying expression.
QList< QgsEsriArcadeExpression * > arguments() const
Returns the list of argument expressions.
QString functionName() const
Returns the function name.
QgsEsriArcadeExpression * condition() const
Returns the condition expression.
QgsEsriArcadeBlockStatement * falseBlock() const
Returns the statement block executed when the condition evaluates to false, or nullptr if none.
QgsEsriArcadeBlockStatement * trueBlock() const
Returns the statement block executed when the condition evaluates to true.
QgsEsriArcadeExpression * indexExpression() const
Returns the index expression.
QgsEsriArcadeExpression * containerExpression() const
Returns the container expression being indexed.
QVariant value() const
Returns the literal value.
QgsEsriArcadeExpression * containerExpression() const
Returns the container expression being accessed.
QString memberName() const
Returns the member attribute name.
QString name() const
Returns the profile variable name, including the leading $ symbol.
Visitor implementation that recursively traverses all child nodes in an Esri Arcade script.
void visit(const QgsEsriArcadeBlockStatement &node) override
Visit a QgsEsriArcadeBlockStatement node.
QgsEsriArcadeExpression * expression() const
Returns the expression returned by the statement, or nullptr if void return.
Parses and encapsulates an entire Esri Arcade script.
void acceptVisitor(QgsEsriArcadeVisitor &visitor) const
Accepts a visitor to traverse the script abstract syntax tree.
bool hasParserError() const
Returns true if a parsing error occurred.
QgsEsriArcadeExpression * operand() const
Returns the operand expression.
QString text() const
Returns text representation of the operator.
UnaryOperator unaryOperator() const
Returns the unary operator.
QString name() const
Returns the variable identifier name.
QString what() const
static QString quotedString(QString text)
Returns a quoted version of a string (in single quotes).
static QString quotedColumnRef(QString name)
Returns a quoted column reference (in double quotes).
Custom exception class which is raised when an operation is not supported.
static bool isNull(const QVariant &variant, bool silenceNullWarnings=false)
Returns true if the specified variant should be considered a NULL value.