Commit 975ad2a572aee15ad5d823e3197eafb86df1a580

Added SimilarityChain constraint for APG

This constraint depends on the fingerprinting functionality, so it was
moved out of the generator branch to the fingerprint branch when the
branch dependencies were reordered.
  
579579 playlistgenerator/constraints/Matching.cpp
580580 playlistgenerator/constraints/PlaylistLength.cpp
581581 playlistgenerator/constraints/PreventDuplicates.cpp
582 playlistgenerator/constraints/SimilarityChain.cpp
582583 playlistgenerator/constraints/TrackSpreader.cpp
583584)
584585
590590 playlistgenerator/constraints/GlobalMatchEditWidget.ui
591591 playlistgenerator/constraints/PlaylistLengthEditWidget.ui
592592 playlistgenerator/constraints/PreventDuplicatesEditWidget.ui
593 playlistgenerator/constraints/SimilarityChainEditWidget.ui
593594)
594595
595596#####################################################################
  
2424#include "constraints/GlobalMatch.h"
2525#include "constraints/PlaylistLength.h"
2626#include "constraints/PreventDuplicates.h"
27#include "constraints/SimilarityChain.h"
2728#include "Debug.h"
2829
2930#include <QList>
7979
8080 r = ConstraintTypes::PreventDuplicates::registerMe();
8181 m_registryIds[2] = r;
82 m_registryNames[r->m_name] = r;
83
84 r = ConstraintTypes::SimilarityChain::registerMe();
85 m_registryIds[3] = r;
8286 m_registryNames[r->m_name] = r;
8387
8488 /*
  
1/****************************************************************************************
2 * Copyright (c) 2008-2010 Soren Harward <stharward@gmail.com> *
3 * *
4 * This program is free software; you can redistribute it and/or modify it under *
5 * the terms of the GNU General Public License as published by the Free Software *
6 * Foundation; either version 2 of the License, or (at your option) any later *
7 * version. *
8 * *
9 * This program is distributed in the hope that it will be useful, but WITHOUT ANY *
10 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A *
11 * PARTICULAR PURPOSE. See the GNU General Public License for more details. *
12 * *
13 * You should have received a copy of the GNU General Public License along with *
14 * this program. If not, see <http://www.gnu.org/licenses/>. *
15 ****************************************************************************************/
16
17#define DEBUG_PREFIX "Constraint::SimilarityChain"
18
19#include "Debug.h"
20#include "Fingerprint.h"
21#include "QueryMaker.h"
22#include "SimilarityChain.h"
23#include "meta/capabilities/FingerprintCapability.h"
24#include "meta/Meta.h"
25#include "playlistgenerator/Constraint.h"
26#include "playlistgenerator/ConstraintFactory.h"
27
28#include <QtGlobal>
29#include <QtGui>
30#include <math.h>
31#include <stdlib.h>
32
33Constraint*
34ConstraintTypes::SimilarityChain::createFromXml( QDomElement& xmlelem, ConstraintNode* p )
35{
36 if ( p ) {
37 return new
38ConstraintTypes::SimilarityChain( xmlelem, p );
39 } else {
40 return 0;
41 }
42}
43
44Constraint*
45ConstraintTypes::SimilarityChain::createNew( ConstraintNode* p )
46{
47 if ( p ) {
48 return new
49ConstraintTypes::SimilarityChain( p );
50 } else {
51 return 0;
52 }
53}
54
55ConstraintFactoryEntry*
56ConstraintTypes::SimilarityChain::registerMe()
57{
58 return new ConstraintFactoryEntry( "SimilarityChain",
59 "Smoothes the playlist by making each track similar to the ones around it",
60 &ConstraintTypes::SimilarityChain::createFromXml, &SimilarityChain::createNew );
61}
62
63ConstraintTypes::SimilarityChain::SimilarityChain( QDomElement& xmlelem, ConstraintNode* p )
64 : Constraint( xmlelem, p )
65{
66 QDomAttr a;
67
68 a = xmlelem.attributeNode( "similarity" );
69 if ( !a.isNull() )
70 m_similarity = a.value().toDouble();
71
72 a = xmlelem.attributeNode( "comparison" );
73 if ( !a.isNull() ) {
74 QString c = a.value();
75 if ( c == "equals" ) {
76 m_comparison = Constraint::CompareNumEquals;
77 } else if ( c == "greaterthan" ) {
78 m_comparison = Constraint::CompareNumGreaterThan;
79 } else if ( c == "lessthan" ) {
80 m_comparison = Constraint::CompareNumLessThan;
81 }
82 }
83}
84
85ConstraintTypes::SimilarityChain::SimilarityChain( ConstraintNode* p )
86 : Constraint( p )
87 , m_similarity( 0.75 )
88 , m_comparison( Constraint::CompareNumGreaterThan )
89{
90 setName( "new SimilarityChain constraint" );
91}
92
93QWidget*
94ConstraintTypes::SimilarityChain::editWidget() const
95{
96 SimilarityChainEditWidget* e = new SimilarityChainEditWidget( getName(), m_comparison, static_cast<int>( 100*m_similarity ) );
97 connect( e, SIGNAL( nameChanged( const QString& ) ), this, SLOT( setName( const QString& ) ) );
98 connect( e, SIGNAL( comparisonChanged( const int ) ), this, SLOT( setComparison( const int ) ) );
99 connect( e, SIGNAL( similarityChanged( const int ) ), this, SLOT( setSimilarity( const int ) ) );
100 return e;
101}
102
103void
104ConstraintTypes::SimilarityChain::toXml( QDomDocument& doc, QDomElement& elem ) const
105{
106 QDomElement c = doc.createElement( "constraint" );
107 QDomText t = doc.createTextNode( getName() );
108 c.appendChild( t );
109 c.setAttribute( "type", "SimilarityChain" );
110 if ( m_comparison == Constraint::CompareNumEquals ) {
111 c.setAttribute( "comparison", "equals" );
112 } else if ( m_comparison == Constraint::CompareNumGreaterThan ) {
113 c.setAttribute( "comparison", "greaterthan" );
114 } else if ( m_comparison == Constraint::CompareNumLessThan ) {
115 c.setAttribute( "comparison", "lessthan" );
116 }
117 c.setAttribute( "similarity", QString::number( m_similarity ) );
118 elem.appendChild( c );
119}
120
121QueryMaker*
122ConstraintTypes::SimilarityChain::initQueryMaker( QueryMaker* qm ) const
123{
124 return qm;
125}
126
127// TODO: check tracks other than just the one immediately before
128double
129ConstraintTypes::SimilarityChain::satisfaction( const Meta::TrackList& tl )
130{
131 if ( tl.size() <= 1 )
132 return 1.0;
133
134 double sum = 0.0;
135 for ( int i = 1; i < tl.size(); i++ ) {
136 Meta::TrackPtr t = tl.at( i );
137 if ( t->hasCapabilityInterface( Meta::Capability::Fingerprint ) ) {
138 Meta::FingerprintCapability* fpc = t->create<Meta::FingerprintCapability>();
139 sum += simSatisfaction( fpc->calcSimilarityTo( tl.at( i - 1 ) ) );
140 } else {
141 debug() << t->prettyName() << "does not have a fingerprint";
142 }
143 }
144 m_satisfaction = sum / ( double )tl.size();
145 return m_satisfaction;
146}
147
148double
149ConstraintTypes::SimilarityChain::deltaS_insert( const Meta::TrackList& tl, const Meta::TrackPtr t, const int i ) const
150{
151 if ( tl.size() <= 0 )
152 return 0.0;
153
154 double oldSum = 0.0;
155 double newSum = 0.0;
156
157 Meta::TrackPtr newT = t;
158 Meta::FingerprintCapability* newfpc = newT->create<Meta::FingerprintCapability>();
159
160 if ( i >= 1 ) {
161 newSum += simSatisfaction( newfpc->calcSimilarityTo( tl.at( i - 1 ) ) );
162 }
163
164 if ( i < ( tl.size() ) ) {
165 Meta::TrackPtr oldT = tl.at( i );
166 Meta::FingerprintCapability* oldfpc = oldT->create<Meta::FingerprintCapability>();
167
168 newSum += simSatisfaction( newfpc->calcSimilarityTo( oldT ) );
169 if ( i >= 1 ) {
170 oldSum += simSatisfaction( oldfpc->calcSimilarityTo( tl.at( i - 1 ) ) );
171 }
172 }
173
174 double x = m_satisfaction * tl.size();
175 return ( x + newSum - oldSum ) / ( double )( tl.size() + 1 ) - m_satisfaction;
176}
177
178double
179ConstraintTypes::SimilarityChain::deltaS_replace( const Meta::TrackList& tl, const Meta::TrackPtr t, const int i ) const
180{
181 if ( tl.size() <= 1 )
182 return 0.0;
183
184 double oldSum = 0.0;
185 double newSum = 0.0;
186
187 Meta::TrackPtr oldT = tl.at( i );
188 Meta::FingerprintCapability* oldfpc = oldT->create<Meta::FingerprintCapability>();
189
190 Meta::TrackPtr newT = t;
191 Meta::FingerprintCapability* newfpc = newT->create<Meta::FingerprintCapability>();
192
193 if ( i >= 1 ) {
194 oldSum += simSatisfaction( oldfpc->calcSimilarityTo( tl.at( i - 1 ) ) );
195 newSum += simSatisfaction( newfpc->calcSimilarityTo( tl.at( i - 1 ) ) );
196 }
197
198 if ( i < ( tl.size() - 1 ) ) {
199 oldSum += simSatisfaction( oldfpc->calcSimilarityTo( tl.at( i + 1 ) ) );
200 newSum += simSatisfaction( newfpc->calcSimilarityTo( tl.at( i + 1 ) ) );
201 }
202
203 return ( newSum -oldSum ) / ( double )tl.size();
204}
205
206double
207ConstraintTypes::SimilarityChain::deltaS_delete( const Meta::TrackList& tl, const int i ) const
208{
209 if ( tl.size() <= 2 )
210 return 0.0;
211
212 double oldSum = 0.0;
213 double newSum = 0.0;
214
215 Meta::TrackPtr oldT = tl.at( i );
216 Meta::FingerprintCapability* oldfpc = oldT->create<Meta::FingerprintCapability>();
217
218 if ( i >= 1 ) {
219 oldSum += simSatisfaction( oldfpc->calcSimilarityTo( tl.at( i - 1 ) ) );
220 }
221
222 if ( i < ( tl.size() - 1 ) ) {
223 oldSum += simSatisfaction( oldfpc->calcSimilarityTo( tl.at( i + 1 ) ) );
224
225 if ( i >= 1 ) {
226 Meta::TrackPtr newT = tl.at( i + 1 );
227 Meta::FingerprintCapability* newfpc = newT->create<Meta::FingerprintCapability>();
228 newSum += simSatisfaction( newfpc->calcSimilarityTo( tl.at( i - 1 ) ) );
229 }
230 }
231
232 double x = m_satisfaction * ( double )tl.size();
233 return ( x + newSum - oldSum ) / ( double )( tl.size() - 1 ) - m_satisfaction;
234}
235
236double
237ConstraintTypes::SimilarityChain::deltaS_swap( const Meta::TrackList& tl, const int i, const int j ) const
238{
239 if ( tl.size() <= 1 )
240 return 0.0;
241
242 double oldSum = 0.0;
243 double newSum = 0.0;
244
245 Meta::TrackPtr rightT = tl.at( i );
246 Meta::FingerprintCapability* rightfpc = rightT->create<Meta::FingerprintCapability>();
247
248 Meta::TrackPtr leftT = tl.at( j );
249 Meta::FingerprintCapability* leftfpc = leftT->create<Meta::FingerprintCapability>();
250
251 // if we don't make a changed copy, then this algorithm fails if the swap points are adjacent
252 Meta::TrackList newTl( tl );
253 newTl.swap( i, j );
254
255 if ( i >= 1 ) {
256 oldSum += simSatisfaction( rightfpc->calcSimilarityTo( tl.at( i - 1 ) ) );
257 newSum += simSatisfaction( leftfpc->calcSimilarityTo( newTl.at( i - 1 ) ) );
258 }
259
260 if ( i < ( tl.size() - 1 ) ) {
261 oldSum += simSatisfaction( rightfpc->calcSimilarityTo( tl.at( i + 1 ) ) );
262 newSum += simSatisfaction( leftfpc->calcSimilarityTo( newTl.at( i + 1 ) ) );
263 }
264
265 if ( j >= 1 ) {
266 oldSum += simSatisfaction( leftfpc->calcSimilarityTo( tl.at( j - 1 ) ) );
267 newSum += simSatisfaction( rightfpc->calcSimilarityTo( newTl.at( j - 1 ) ) );
268 }
269
270 if ( j < ( tl.size() - 1 ) ) {
271 oldSum += simSatisfaction( leftfpc->calcSimilarityTo( tl.at( j + 1 ) ) );
272 newSum += simSatisfaction( rightfpc->calcSimilarityTo( newTl.at( j + 1 ) ) );
273 }
274
275 return ( newSum -oldSum ) / ( double )tl.size();
276}
277
278void
279ConstraintTypes::SimilarityChain::insertTrack( const Meta::TrackList& tl, const Meta::TrackPtr t, const int i )
280{
281 m_satisfaction += deltaS_insert( tl, t, i );
282}
283
284void
285ConstraintTypes::SimilarityChain::replaceTrack( const Meta::TrackList& tl, const Meta::TrackPtr t, const int i )
286{
287 m_satisfaction += deltaS_replace( tl, t, i );
288}
289
290void
291ConstraintTypes::SimilarityChain::deleteTrack( const Meta::TrackList& tl, const int i )
292{
293 m_satisfaction += deltaS_delete( tl, i );
294}
295
296void
297ConstraintTypes::SimilarityChain::swapTracks( const Meta::TrackList& tl, const int i, const int j )
298{
299 m_satisfaction += deltaS_swap( tl, i, j );
300}
301
302#ifndef KDE_NO_DEBUG_OUTPUT
303void
304ConstraintTypes::SimilarityChain::audit( const Meta::TrackList& tl ) const
305{
306 if ( tl.size() <= 0 )
307 return;
308
309 debug() << tl.at( 0 )->prettyName();
310 for ( int i = 1; i < tl.size(); i++ ) {
311 Meta::TrackPtr t = tl.at( i );
312 Meta::FingerprintCapability* tfpc = t->create<Meta::FingerprintCapability>();
313 debug() << "audit" << t->prettyName() << " " << tfpc->calcSimilarityTo( tl.at( i - 1 ) ) << " " << simSatisfaction( tfpc->calcSimilarityTo( tl.at( i - 1 ) ) );
314 }
315}
316#endif
317
318double
319ConstraintTypes::SimilarityChain::simSatisfaction( const double s ) const
320{
321 //return s;
322 if ( s != Fingerprint::INVALID_SIMILARITY ) {
323 switch ( m_comparison ) {
324 case Constraint::CompareNumEquals:
325 return exp( -4.0 * fabs( m_similarity - s ) );
326 case Constraint::CompareNumGreaterThan:
327 return qMin( 1.0, exp( -4.0 * ( m_similarity - s ) ) );
328 case Constraint::CompareNumLessThan:
329 return qMin( 1.0, exp( -4.0 * ( s - m_similarity ) ) );
330 default:
331 return 0.0;
332 }
333 }
334 return 0.0;
335}
336
337void
338ConstraintTypes::SimilarityChain::setComparison( const int c )
339{
340 m_comparison = c;
341}
342
343void
344ConstraintTypes::SimilarityChain::setSimilarity( const int sv )
345{
346 m_similarity = static_cast<double>( sv ) / 100.0;
347}
348
349/******************************
350 * Edit Widget *
351 ******************************/
352
353ConstraintTypes::SimilarityChainEditWidget::SimilarityChainEditWidget( const QString& name,
354 const int comparison,
355 const int similarity ) : QWidget( 0 )
356{
357 ui.setupUi( this );
358
359 ui.lineEdit_Name->setText( name );
360 ui.comboBox_Comparison->setCurrentIndex( comparison );
361 ui.slider_Similarity->setValue( similarity );
362 ui.label_SimilarityValue->setText( QString( "%1%" ).arg( similarity, 3 ) );
363}
364
365void
366ConstraintTypes::SimilarityChainEditWidget::on_lineEdit_Name_textChanged( const QString& s )
367{
368 emit nameChanged( s );
369 emit updated();
370}
371
372void
373ConstraintTypes::SimilarityChainEditWidget::on_comboBox_Comparison_currentIndexChanged( const int v )
374{
375 emit comparisonChanged( v );
376 emit updated();
377}
378
379void
380ConstraintTypes::SimilarityChainEditWidget::on_slider_Similarity_valueChanged( const int v )
381{
382 ui.label_SimilarityValue->setText( QString( "%1%" ).arg( v, 3 ) );
383 emit similarityChanged( v );
384 emit updated();
385}
  
1/****************************************************************************************
2 * Copyright (c) 2008-2010 Soren Harward <stharward@gmail.com> *
3 * *
4 * This program is free software; you can redistribute it and/or modify it under *
5 * the terms of the GNU General Public License as published by the Free Software *
6 * Foundation; either version 2 of the License, or (at your option) any later *
7 * version. *
8 * *
9 * This program is distributed in the hope that it will be useful, but WITHOUT ANY *
10 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A *
11 * PARTICULAR PURPOSE. See the GNU General Public License for more details. *
12 * *
13 * You should have received a copy of the GNU General Public License along with *
14 * this program. If not, see <http://www.gnu.org/licenses/>. *
15 ****************************************************************************************/
16
17#ifndef APG_SIMILARITYCHAIN_CONSTRAINT
18#define APG_SIMILARITYCHAIN_CONSTRAINT
19
20#include "ui_SimilarityChainEditWidget.h"
21
22#include "playlistgenerator/Constraint.h"
23
24#include <QHash>
25#include <QString>
26
27class ConstraintFactoryEntry;
28class QueryMaker;
29class QWidget;
30
31namespace ConstraintTypes {
32
33 /* This constraint creates a Chain of songs based on their relative Similarity values */
34
35 class SimilarityChain : public Constraint {
36 Q_OBJECT
37
38 public:
39 static Constraint* createFromXml(QDomElement&, ConstraintNode*);
40 static Constraint* createNew(ConstraintNode*);
41 static ConstraintFactoryEntry* registerMe();
42
43 virtual QWidget* editWidget() const;
44 virtual void toXml(QDomDocument&, QDomElement&) const;
45
46 virtual QueryMaker* initQueryMaker(QueryMaker*) const;
47 virtual double satisfaction(const Meta::TrackList&);
48 virtual double deltaS_insert(const Meta::TrackList&, const Meta::TrackPtr, const int) const;
49 virtual double deltaS_replace(const Meta::TrackList&, const Meta::TrackPtr, const int) const;
50 virtual double deltaS_delete(const Meta::TrackList&, const int) const;
51 virtual double deltaS_swap(const Meta::TrackList&, const int, const int) const;
52 virtual void insertTrack(const Meta::TrackList&, const Meta::TrackPtr, const int);
53 virtual void replaceTrack(const Meta::TrackList&, const Meta::TrackPtr, const int);
54 virtual void deleteTrack(const Meta::TrackList&, const int);
55 virtual void swapTracks(const Meta::TrackList&, const int, const int);
56
57#ifndef KDE_NO_DEBUG_OUTPUT
58 virtual void audit(const Meta::TrackList&) const;
59#endif
60
61 private slots:
62 void setComparison( const int );
63 void setSimilarity( const int );
64
65 private:
66 SimilarityChain(QDomElement&, ConstraintNode*);
67 SimilarityChain(ConstraintNode*);
68
69 double simSatisfaction( const double ) const;
70 double m_satisfaction;
71
72 // constraint parameters
73 double m_similarity;
74 int m_comparison;
75 };
76
77 class SimilarityChainEditWidget : public QWidget {
78 Q_OBJECT
79
80 public:
81 SimilarityChainEditWidget( const QString&, const int, const int );
82
83 signals:
84 void updated();
85 void nameChanged( const QString& );
86 void comparisonChanged( const int );
87 void similarityChanged( const int );
88
89 private slots:
90 void on_lineEdit_Name_textChanged( const QString& );
91 void on_comboBox_Comparison_currentIndexChanged( const int );
92 void on_slider_Similarity_valueChanged( const int );
93
94 private:
95 Ui::SimilarityChainEditWidget ui;
96 };
97} // namespace ConstraintTypes
98
99#endif
  
1<ui version="4.0" >
2 <author>Soren Harward &lt;stharward@gmail.com></author>
3 <class>SimilarityChainEditWidget</class>
4 <widget class="QWidget" name="SimilarityChainEditWidget" >
5 <property name="geometry" >
6 <rect>
7 <x>0</x>
8 <y>0</y>
9 <width>269</width>
10 <height>130</height>
11 </rect>
12 </property>
13 <property name="windowTitle" >
14 <string>Form</string>
15 </property>
16 <property name="whatsThis" >
17 <string>An editor for a SimilarityChain constraint. Similarity values are expressed as percentages, and higher percentages correspond to greater similarity. The user can specify a similarity value, and whether the similarity of adjacent tracks in the playlist should be less than, equal to, or greater than this value.</string>
18 </property>
19 <layout class="QVBoxLayout" name="verticalLayout" >
20 <item>
21 <widget class="QGroupBox" name="groupBox" >
22 <property name="title" >
23 <string>SimilarityChain Constraint Settings</string>
24 </property>
25 <layout class="QGridLayout" name="gridLayout_2" >
26 <item row="0" column="0" >
27 <layout class="QGridLayout" name="gridLayout" >
28 <item row="0" column="0" >
29 <widget class="QLabel" name="label_Name" >
30 <property name="text" >
31 <string>Name:</string>
32 </property>
33 <property name="buddy" >
34 <cstring>lineEdit_Name</cstring>
35 </property>
36 </widget>
37 </item>
38 <item row="0" column="1" >
39 <widget class="QLineEdit" name="lineEdit_Name" >
40 <property name="toolTip" >
41 <string/>
42 </property>
43 <property name="whatsThis" >
44 <string>The name of the constraint will be shown in the Constraint Tree Editor above. You can give a constraint any name you want.</string>
45 </property>
46 </widget>
47 </item>
48 <item row="1" column="0" >
49 <widget class="QComboBox" name="comboBox_Comparison" >
50 <property name="toolTip" >
51 <string/>
52 </property>
53 <property name="whatsThis" >
54 <string>Whether the similarity of adjacent tracks in the generated playlist should be less than, equal to, or greater than the specified value. A "less than" setting will create a playlist that jumps around genres. A "greater than" setting will create a playlist that transitions smoothly between genres. The "equal to" setting will create a playlist that falls somewhere in between these behaviors.</string>
55 </property>
56 <item>
57 <property name="text" >
58 <string>less than</string>
59 </property>
60 </item>
61 <item>
62 <property name="text" >
63 <string>equal to</string>
64 </property>
65 </item>
66 <item>
67 <property name="text" >
68 <string>more than</string>
69 </property>
70 </item>
71 </widget>
72 </item>
73 <item row="1" column="1" >
74 <layout class="QHBoxLayout" name="horizontalLayout" >
75 <item>
76 <widget class="QSlider" name="slider_Similarity" >
77 <property name="toolTip" >
78 <string/>
79 </property>
80 <property name="whatsThis" >
81 <string>The threshold similarity value for adjacent tracks, expressed as a percentage. The algorithm that calculates the similarity between two tracks involves quite a bit of voodoo and depends on the contents of your collection, so you'll just have to experiment on your own to figure out how different similarity value settings affect the APG's track selection.</string>
82 </property>
83 <property name="maximum" >
84 <number>100</number>
85 </property>
86 <property name="value" >
87 <number>80</number>
88 </property>
89 <property name="orientation" >
90 <enum>Qt::Horizontal</enum>
91 </property>
92 </widget>
93 </item>
94 <item>
95 <widget class="QLabel" name="label_SimilarityValue" >
96 <property name="toolTip" >
97 <string/>
98 </property>
99 <property name="whatsThis" >
100 <string>The current similarity value.</string>
101 </property>
102 <property name="text" >
103 <string>100%</string>
104 </property>
105 </widget>
106 </item>
107 </layout>
108 </item>
109 <item row="2" column="1" >
110 <spacer name="verticalSpacer" >
111 <property name="orientation" >
112 <enum>Qt::Vertical</enum>
113 </property>
114 <property name="sizeHint" stdset="0" >
115 <size>
116 <width>20</width>
117 <height>40</height>
118 </size>
119 </property>
120 </spacer>
121 </item>
122 </layout>
123 </item>
124 </layout>
125 </widget>
126 </item>
127 </layout>
128 </widget>
129 <tabstops>
130 <tabstop>lineEdit_Name</tabstop>
131 <tabstop>comboBox_Comparison</tabstop>
132 <tabstop>slider_Similarity</tabstop>
133 </tabstops>
134 <resources/>
135 <connections/>
136</ui>