annotate rdf/generator/vamp-rdf-template-generator.cpp @ 365:feac60b5d82b

Add dc:title for plugin library
author Chris Cannam
date Thu, 05 Jun 2014 15:40:04 +0100
parents 34754c776530
children aede6e90a6b8
rev   line source
cannam@138 1 /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */
cannam@138 2
cannam@231 3 #include <vamp-hostsdk/PluginHostAdapter.h>
cannam@233 4 #include <vamp-hostsdk/PluginChannelAdapter.h>
cannam@233 5 #include <vamp-hostsdk/PluginInputDomainAdapter.h>
cannam@233 6 #include <vamp-hostsdk/PluginLoader.h>
cannam@231 7 #include <vamp/vamp.h>
cannam@138 8
cannam@138 9 #include <iostream>
cannam@138 10 #include <fstream>
cannam@138 11 #include <sstream>
cannam@138 12
cannam@138 13 #include <cmath>
cannam@232 14 #include <cstdlib>
cannam@232 15 #include <cstring>
cannam@138 16
cannam@243 17 #include <cstdlib>
cannam@243 18 #include <cstring>
cannam@243 19
cannam@138 20 using std::cout;
cannam@138 21 using std::cin;
cannam@138 22 using std::cerr;
cannam@138 23 using std::getline;
cannam@138 24 using std::endl;
cannam@138 25 using std::string;
cannam@138 26 using std::vector;
cannam@138 27 using std::ofstream;
cannam@138 28 using std::ios;
cannam@138 29
cannam@138 30 using Vamp::HostExt::PluginLoader;
cannam@138 31 using Vamp::Plugin;
cannam@138 32
cannam@217 33 //???
cannam@138 34 string programURI = "http://www.vamp-plugins.org/doap.rdf#template-generator";
cannam@138 35
cannam@138 36 void usage()
cannam@138 37 {
cannam@217 38 cerr << endl;
cannam@251 39 cerr << "vamp-rdf-template-generator: Create a skeleton RDF description file describing" << endl;
cannam@251 40 cerr << "a Vamp plugin library using the Vamp ontology." << endl;
cannam@217 41 cerr << endl;
cannam@217 42 cerr << "Usage:" << endl;
cannam@251 43 cerr << " vamp-rdf-template-generator -i vamp:soname[:plugin] [vamp:soname[:plugin] ...]" << endl;
cannam@251 44 cerr << " vamp-rdf-template-generator PLUGIN_BASE_URI [ -m YOUR_URI ] [vamp:]soname[:plugin] [[vamp:]soname[:plugin] ...]" << endl;
cannam@217 45 cerr << endl;
cannam@217 46 cerr << "Example:" << endl;
cannam@251 47 cerr << " vamp-rdf-template-generator http://vamp-plugins.org/rdf/plugins/ vamp-example-plugins" << endl;
cannam@217 48 cerr << endl;
cannam@138 49 exit(2);
cannam@138 50 }
cannam@138 51
cannam@138 52 template <class T>
cannam@138 53 inline string to_string (const T& t)
cannam@138 54 {
cannam@138 55 std::stringstream ss;
cannam@138 56 ss << t;
cannam@138 57 return ss.str();
cannam@138 58 }
cannam@138 59
cannam@144 60 string describe_namespaces(string pluginBundleBaseURI, string libname)
cannam@138 61 {
cannam@142 62 string res=\
cannam@142 63 "@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .\n\
cannam@138 64 @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .\n\
cannam@147 65 @prefix vamp: <http://purl.org/ontology/vamp/> .\n\
cannam@144 66 @prefix plugbase: <"+pluginBundleBaseURI+libname+"#> .\n\
cannam@138 67 @prefix owl: <http://www.w3.org/2002/07/owl#> .\n\
cannam@138 68 @prefix dc: <http://purl.org/dc/elements/1.1/> .\n\
cannam@138 69 @prefix af: <http://purl.org/ontology/af/> .\n\
cannam@138 70 @prefix foaf: <http://xmlns.com/foaf/0.1/> .\n\
cannam@138 71 @prefix cc: <http://web.resource.org/cc/> .\n\
cannam@267 72 @prefix : <#> .\n\n";
cannam@138 73
cannam@142 74 return res;
cannam@138 75 }
cannam@138 76
cannam@144 77 string describe_doc(string describerURI, string pluginBundleBaseURI,
cannam@144 78 string libname)
cannam@138 79 {
cannam@142 80 string res=\
cannam@217 81 "<> a vamp:PluginDescription ;\n";
cannam@217 82 if (describerURI != "") {
cannam@217 83 res += " foaf:maker <"+describerURI+"> ;\n";
cannam@217 84 }
cannam@217 85 res += "\
cannam@144 86 foaf:maker <"+programURI+"> ;\n\
cannam@144 87 foaf:primaryTopic <"+pluginBundleBaseURI+libname+"> .\n\n";
cannam@142 88 return res;
cannam@138 89 }
cannam@138 90
cannam@138 91
cannam@144 92 string describe_library(string libname, vector<Plugin *> plugins)
cannam@144 93 {
cannam@144 94 string res=\
cannam@144 95 ":"+libname+" a vamp:PluginLibrary ;\n\
Chris@365 96 vamp:identifier \""+libname+"\"";
cannam@144 97
cannam@144 98 for (size_t i = 0; i < plugins.size(); ++i) {
cannam@218 99 res += " ; \n\
cannam@145 100 vamp:available_plugin plugbase:"+plugins[i]->getIdentifier();
cannam@144 101 }
cannam@144 102
cannam@218 103 res += " ; \n\
Chris@365 104 # dc:title \"\" ; # Place a descriptive name for the plugin library here and uncomment\n\
cannam@218 105 # foaf:page <Place more-information HTML page URL here and uncomment> ;\n\
cannam@218 106 .\n\n";
cannam@144 107 return res;
cannam@144 108 }
cannam@144 109
cannam@138 110 string describe_plugin(Plugin* plugin)
cannam@138 111 {
cannam@142 112 string res=\
cannam@142 113 "plugbase:"+plugin->getIdentifier()+" a vamp:Plugin ;\n\
cannam@138 114 dc:title \""+plugin->getName()+"\" ;\n\
cannam@139 115 vamp:name \""+plugin->getName()+"\" ;\n\
cannam@221 116 dc:description \"\"\""+plugin->getDescription()+"\"\"\" ;\n\
cannam@154 117 foaf:maker [ foaf:name \""+plugin->getMaker()+"\" ] ; # FIXME could give plugin author's URI here\n\
cannam@224 118 dc:rights \"\"\""+plugin->getCopyright()+"\"\"\" ;\n\
cannam@154 119 # cc:license <Place plugin license URI here and uncomment> ; \n\
cannam@138 120 vamp:identifier \""+plugin->getIdentifier()+"\" ;\n\
cannam@138 121 vamp:vamp_API_version vamp:api_version_"+to_string(plugin->getVampApiVersion())+" ;\n\
cannam@138 122 owl:versionInfo \""+to_string(plugin->getPluginVersion())+"\" ;\n";
cannam@142 123 if (plugin->getInputDomain() == Vamp::Plugin::FrequencyDomain)
cannam@142 124 res+=" vamp:input_domain vamp:FrequencyDomain ;\n\n";
cannam@142 125 else
cannam@194 126 res+=" vamp:input_domain vamp:TimeDomain ;\n";
cannam@138 127
cannam@138 128
cannam@142 129 Plugin::ParameterList params = plugin->getParameterDescriptors();
cannam@194 130 if (!params.empty()) res+="\n";
cannam@142 131 for (Plugin::ParameterList::const_iterator i = params.begin(); i != params.end(); i++)
dpastor@156 132 res+=" vamp:parameter plugbase:"+plugin->getIdentifier()+"_param_"+(*i).identifier+" ;\n";
cannam@194 133 if (!params.empty()) res+="\n";
cannam@138 134
cannam@142 135 Plugin::OutputList outputs = plugin->getOutputDescriptors();
cannam@142 136 for (Plugin::OutputList::const_iterator i = outputs.begin(); i!= outputs.end(); i++)
cannam@158 137 res+=" vamp:output plugbase:"+plugin->getIdentifier()+"_output_"+(*i).identifier+" ;\n";
cannam@142 138 res+=" .\n";
cannam@138 139
cannam@142 140 return res;
cannam@138 141 }
cannam@138 142
cannam@144 143 string describe_param(Plugin *plugin, Plugin::ParameterDescriptor p)
cannam@138 144 {
cannam@148 145
cannam@148 146 //FIXME: dc:format and vamp:unit are the same???
dpastor@157 147 //Should be a QUantizedParameter also a Parameter??
cannam@148 148 if(p.isQuantized){
cannam@148 149 string res=\
dpastor@156 150 "plugbase:"+plugin->getIdentifier()+"_param_"+p.identifier+" a vamp:QuantizedParameter ;\n\
cannam@148 151 vamp:identifier \""+p.identifier+"\" ;\n\
cannam@148 152 dc:title \""+p.name+"\" ;\n\
cannam@148 153 dc:format \""+p.unit+"\" ;\n\
cannam@148 154 vamp:min_value "+to_string(p.minValue)+" ;\n\
cannam@148 155 vamp:max_value "+to_string(p.maxValue)+" ;\n\
cannam@148 156 vamp:unit \""+p.unit+"\" ;\n\
cannam@152 157 vamp:quantize_step "+to_string(p.quantizeStep)+" ;\n\
cannam@148 158 vamp:default_value "+to_string(p.defaultValue)+" ;\n\
cannam@148 159 vamp:value_names (";
cannam@148 160
cannam@148 161 unsigned int i;
cannam@148 162 for (i=0; i+1 < p.valueNames.size(); i++)
cannam@148 163 res+=" \""+p.valueNames[i]+"\"";
cannam@148 164 if (i < p.valueNames.size())
cannam@148 165 res+=" \""+p.valueNames[i]+"\"";
cannam@148 166 res+=");\n";
cannam@148 167
cannam@148 168 res+=" .\n";
cannam@148 169
cannam@148 170 return res;
cannam@148 171
cannam@148 172 }else{
cannam@142 173 string res=\
dpastor@156 174 "plugbase:"+plugin->getIdentifier()+"_param_"+p.identifier+" a vamp:Parameter ;\n\
cannam@138 175 vamp:identifier \""+p.identifier+"\" ;\n\
cannam@138 176 dc:title \""+p.name+"\" ;\n\
cannam@138 177 dc:format \""+p.unit+"\" ;\n\
cannam@139 178 vamp:min_value "+to_string(p.minValue)+" ;\n\
cannam@139 179 vamp:max_value "+to_string(p.maxValue)+" ;\n\
cannam@148 180 vamp:unit \""+p.unit+"\" ;\n\
cannam@148 181 vamp:default_value "+to_string(p.defaultValue)+" ;\n\
cannam@148 182 vamp:value_names (";
cannam@148 183
cannam@148 184 unsigned int i;
cannam@148 185 for (i=0; i+1 < p.valueNames.size(); i++)
cannam@148 186 res+=" \""+p.valueNames[i]+"\"";
cannam@148 187 if (i < p.valueNames.size())
cannam@148 188 res+=" \""+p.valueNames[i]+"\"";
cannam@148 189 res+=");\n";
cannam@148 190
cannam@148 191 res+=" .\n";
cannam@148 192
cannam@142 193 return res;
cannam@148 194
cannam@148 195 }
cannam@138 196 }
cannam@138 197
cannam@144 198 string describe_output(Plugin *plugin, Plugin::OutputDescriptor o)
cannam@138 199 {
cannam@138 200
cannam@142 201 //we need to distinguish here between different output types:
cannam@148 202
cannam@148 203 //Quantize or not
cannam@148 204 //KnownExtents or not
cannam@148 205 //Data output classification:
cannam@142 206 //DenseOutput
cannam@142 207 //SparseOutput
cannam@142 208 //TrackLevelOutput
cannam@139 209
cannam@139 210
cannam@168 211 // SparseOutput: variable sample rate. Events are not evenly
cannam@168 212 // spaced so we need to record the time associated with the event
cannam@168 213 // as it its not ensured that we have an event after the next one
cannam@168 214 // (but there is not time to set the duration, it has to be
cannam@168 215 // calculated as the different between 2 different events). The
cannam@168 216 // timestamp must be read.
cannam@139 217
cannam@142 218 string res;
cannam@139 219
cannam@168 220 if (o.sampleType == Plugin::OutputDescriptor::VariableSampleRate ||
cannam@168 221 !o.hasFixedBinCount)
cannam@142 222 {
cannam@139 223
cannam@142 224 res=\
cannam@144 225 "plugbase:"+plugin->getIdentifier()+"_output_"+o.identifier+" a vamp:SparseOutput ;\n\
cannam@139 226 vamp:identifier \""+o.identifier+"\" ;\n\
cannam@139 227 dc:title \""+o.name+"\" ;\n\
cannam@273 228 dc:description \"\"\""+o.description+"\"\"\" ;\n\
cannam@139 229 vamp:fixed_bin_count \""+(o.hasFixedBinCount == 1 ? "true" : "false")+"\" ;\n\
cannam@139 230 vamp:unit \""+(o.unit)+"\" ;\n";
cannam@139 231
cannam@148 232
cannam@148 233 //another type of output
cannam@148 234 if(o.isQuantized){
cannam@148 235
cannam@148 236 res+=" a vamp:QuantizedOutput ;\n";
cannam@148 237 res+=" vamp:quantize_step "+to_string(o.quantizeStep)+" ;\n";
cannam@148 238 }
cannam@148 239
cannam@148 240 //and yet another type
cannam@148 241 if(o.hasKnownExtents){
cannam@148 242
cannam@148 243 res+=" a vamp:KnownExtentsOutput ;\n";
cannam@148 244 res+=" vamp:min_value "+to_string(o.minValue)+" ;\n";
cannam@148 245 res+=" vamp:max_value "+to_string(o.maxValue)+" ;\n";
cannam@148 246 }
cannam@139 247
cannam@142 248 // FIXME ? Bin names may vary based on plugin setup, so including them here might be misleading...
cannam@142 249 if (o.hasFixedBinCount)
cannam@142 250 {
cannam@144 251 res+=" vamp:bin_count "+to_string(o.binCount)+" ;\n";
cannam@138 252
cannam@264 253 bool haveBinNames = false;
cannam@308 254 for (unsigned int i=0; i < o.binNames.size(); i++) {
cannam@264 255 if (o.binNames[i] != "") {
cannam@264 256 haveBinNames = true;
cannam@264 257 break;
cannam@264 258 }
cannam@264 259 }
cannam@264 260
cannam@264 261 if (haveBinNames) {
cannam@264 262 res+=" vamp:bin_names (";
cannam@264 263
cannam@264 264 unsigned int i;
cannam@264 265 for (i=0; i+1 < o.binNames.size(); i++)
cannam@264 266 res+=" \""+o.binNames[i]+"\"";
cannam@264 267 if (i < o.binNames.size())
cannam@264 268 res+=" \""+o.binNames[i]+"\"";
cannam@264 269 res+=");\n";
cannam@264 270 }
cannam@142 271 }
cannam@148 272
cannam@144 273 res+=" vamp:sample_type vamp:VariableSampleRate ;\n";
cannam@142 274 if (o.sampleRate > 0.0f)
cannam@144 275 res+=" vamp:sample_rate "+to_string(o.sampleRate)+" ;\n";
cannam@142 276
cannam@142 277 }
cannam@139 278
cannam@142 279 //If we do not have SparseOutput, then we have DenseOutput. TrackLevelOutput can not be inferred from the plugin directly without actually
cannam@142 280 //running the plugin.
cannam@142 281 else{
cannam@142 282
cannam@142 283 res=\
cannam@144 284 "plugbase:"+plugin->getIdentifier()+"_output_"+o.identifier+" a vamp:DenseOutput ;\n\
cannam@139 285 vamp:identifier \""+o.identifier+"\" ;\n\
cannam@139 286 dc:title \""+o.name+"\" ;\n\
cannam@273 287 dc:description \"\"\""+o.description+"\"\"\" ;\n\
cannam@139 288 vamp:fixed_bin_count \""+(o.hasFixedBinCount == 1 ? "true" : "false")+"\" ;\n\
cannam@139 289 vamp:unit \""+(o.unit)+"\" ;\n";
cannam@139 290
cannam@139 291
cannam@148 292 //another type of output
cannam@148 293 if(o.isQuantized){
cannam@148 294
cannam@148 295 res+=" a vamp:QuantizedOutput ;\n";
cannam@148 296 res+=" vamp:quantize_step "+to_string(o.quantizeStep)+" ;\n";
cannam@148 297 }
cannam@148 298
cannam@148 299 //and yet another type
cannam@148 300 if(o.hasKnownExtents){
cannam@148 301
cannam@148 302 res+=" a vamp:KnownExtentsOutput ;\n";
cannam@148 303 res+=" vamp:min_value "+to_string(o.minValue)+" ;\n";
cannam@148 304 res+=" vamp:max_value "+to_string(o.maxValue)+" ;\n";
cannam@148 305 }
cannam@139 306
cannam@142 307 // FIXME ? Bin names may vary based on plugin setup, so including them here might be misleading...
cannam@142 308 if (o.hasFixedBinCount)
cannam@142 309 {
cannam@144 310 res+=" vamp:bin_count "+to_string(o.binCount)+" ;\n";
cannam@139 311
cannam@264 312 bool haveBinNames = false;
cannam@308 313 for (unsigned int i=0; i < o.binNames.size(); i++) {
cannam@264 314 if (o.binNames[i] != "") {
cannam@264 315 haveBinNames = true;
cannam@264 316 break;
cannam@264 317 }
cannam@264 318 }
cannam@264 319
cannam@264 320 if (haveBinNames) {
cannam@264 321 res+=" vamp:bin_names (";
cannam@264 322
cannam@264 323 unsigned int i;
cannam@264 324 for (i=0; i+1 < o.binNames.size(); i++)
cannam@264 325 res+=" \""+o.binNames[i]+"\"";
cannam@264 326 if (i < o.binNames.size())
cannam@264 327 res+=" \""+o.binNames[i]+"\"";
cannam@264 328 res+=");\n";
cannam@264 329 }
cannam@139 330 }
cannam@139 331
cannam@142 332 else if (o.sampleType == Plugin::OutputDescriptor::FixedSampleRate)
cannam@142 333 {
cannam@144 334 res+=" vamp:sample_type vamp:FixedSampleRate ;\n";
cannam@144 335 res+=" vamp:sample_rate "+to_string(o.sampleRate)+" ;\n";
cannam@142 336 }
cannam@142 337 else if (o.sampleType == Plugin::OutputDescriptor::OneSamplePerStep)
cannam@144 338 res+=" vamp:sample_type vamp:OneSamplePerStep ;\n";
cannam@142 339 else
cannam@142 340 {
cannam@142 341 cerr<<"Incomprehensible sampleType for output descriptor "+o.identifier<<" !"<<endl;
cannam@142 342 exit(1);
cannam@142 343 }
cannam@142 344 }
cannam@142 345
cannam@142 346 //There is no way to know this in advance, but we can use the km a bit for this.
cannam@159 347 res+="# vamp:computes_event_type <Place event type URI here and uncomment> ;\n";
cannam@159 348 res+="# vamp:computes_feature <Place feature attribute URI here and uncomment> ;\n";
cannam@159 349 res+="# vamp:computes_signal_type <Place signal type URI here and uncomment> ;\n";
cannam@142 350 res+=" .\n";
cannam@142 351
cannam@142 352 return res;
cannam@138 353 }
cannam@139 354
cannam@144 355 string describe(vector<Plugin *> plugins, string pluginBundleBaseURI,
cannam@144 356 string describerURI, string libname)
cannam@138 357 {
cannam@144 358 string res = describe_namespaces(pluginBundleBaseURI, libname);
cannam@138 359
cannam@144 360 res += describe_doc(describerURI, pluginBundleBaseURI, libname);
cannam@138 361
cannam@144 362 res += describe_library(libname, plugins);
cannam@144 363
cannam@144 364 for (size_t i = 0; i < plugins.size(); ++i) {
cannam@144 365
cannam@144 366 Plugin *plugin = plugins[i];
cannam@144 367
cannam@144 368 res += describe_plugin(plugin);
cannam@138 369
cannam@144 370 Plugin::ParameterList params = plugin->getParameterDescriptors();
cannam@144 371 for (Plugin::ParameterList::const_iterator i = params.begin(); i != params.end(); i++)
cannam@144 372 res += describe_param(plugin, *i);
cannam@138 373
cannam@144 374 Plugin::OutputList outputs = plugin->getOutputDescriptors();
cannam@144 375 for (Plugin::OutputList::const_iterator i = outputs.begin(); i!= outputs.end(); i++)
cannam@144 376 res += describe_output(plugin, *i);
cannam@144 377 }
cannam@138 378
cannam@142 379 return res;
cannam@138 380 }
cannam@138 381
cannam@138 382 int main(int argc, char **argv)
cannam@138 383 {
cannam@144 384 if (argc < 3) usage();
cannam@138 385
cannam@144 386 bool interactive = false;
cannam@144 387 if (!strcmp(argv[1], "-i")) interactive = true;
cannam@138 388
cannam@217 389 if (!interactive && argc < 3) usage();
cannam@138 390
cannam@138 391 string pluginBundleBaseURI, describerURI;
cannam@144 392
cannam@144 393 int argidx = 2;
cannam@138 394
cannam@144 395 if (!interactive) {
cannam@138 396 pluginBundleBaseURI = argv[1];
cannam@217 397 if (!strcmp(argv[2], "-m")) {
cannam@217 398 if (argc < 5) usage();
cannam@217 399 describerURI = argv[3];
cannam@217 400 argidx = 4;
cannam@217 401 }
cannam@144 402 } else {
cannam@138 403 cerr << "Please enter the base URI for the plugin bundle : ";
cannam@138 404 getline(cin, pluginBundleBaseURI);
cannam@217 405 cerr << "Please enter your URI (empty to omit) : ";
cannam@138 406 getline(cin, describerURI);
cannam@138 407 }
cannam@144 408
cannam@144 409 vector<Plugin *> plugins;
cannam@144 410 string libname;
cannam@144 411
cannam@144 412 PluginLoader *loader = PluginLoader::getInstance();
cannam@144 413
cannam@144 414 while (argidx < argc) {
cannam@144 415
cannam@144 416 string pluginName = argv[argidx];
cannam@144 417
cannam@144 418 if (pluginName.substr(0, 5) == "vamp:") {
cannam@144 419 pluginName = pluginName.substr(5);
cannam@144 420 }
cannam@144 421
cannam@144 422 string mylibname = pluginName.substr(0, pluginName.find(':'));
cannam@144 423
cannam@144 424 if (libname == "") libname = mylibname;
cannam@144 425 else if (libname != mylibname) {
cannam@144 426 cerr << "ERROR: All plugins specified on command line must originate in the same library" << endl;
cannam@144 427 exit(1);
cannam@144 428 }
cannam@144 429
cannam@144 430 if (mylibname == pluginName) { // pluginName is a library, not a plugin
cannam@144 431
cannam@144 432 PluginLoader::PluginKeyList list = loader->listPlugins();
cannam@144 433 for (size_t i = 0; i < list.size(); ++i) {
cannam@144 434 string thislibname = list[i].substr(0, list[i].find(':'));
cannam@144 435 if (thislibname != mylibname) continue;
cannam@144 436 Plugin *plugin = loader->loadPlugin(list[i], 44100);
cannam@144 437 if (!plugin) {
cannam@144 438 cerr << "ERROR: Plugin \"" << list[i] << "\" could not be loaded" << endl;
cannam@144 439 exit(1);
cannam@144 440 }
cannam@144 441 plugins.push_back(plugin);
cannam@144 442 }
cannam@151 443
cannam@151 444 if (plugins.empty()) {
cannam@151 445 cerr << "ERROR: Plugin library \"" << mylibname << "\" does not exist, could not be opened, or contains no plugins" << endl;
cannam@151 446 exit(1);
cannam@151 447 }
cannam@151 448
cannam@144 449 } else { // pluginName is a plugin
cannam@144 450
cannam@144 451 Plugin *plugin = loader->loadPlugin(pluginName, size_t(44100));
cannam@144 452 if (!plugin) {
cannam@144 453 cerr << "ERROR: Plugin \"" << pluginName << "\" could not be loaded" << endl;
cannam@144 454 exit(1);
cannam@144 455 }
cannam@144 456 plugins.push_back(plugin);
cannam@144 457 }
cannam@144 458
cannam@144 459 ++argidx;
cannam@144 460 }
cannam@138 461
cannam@144 462 cout << describe(plugins, pluginBundleBaseURI, describerURI, libname) << endl;
cannam@138 463
cannam@138 464 return 0;
cannam@138 465 }
cannam@138 466
cannam@138 467