Commit d2e2f9f3f9dd94e27588dce087cf94c4bac70a71
1 parent
cdf09f30
updated command-line arguments
Showing
1 changed file
with
547 additions
and
0 deletions
Show diff stats
1 | +#include "rts/tools/arguments.h" | |
2 | + | |
3 | +#include "rts/optics/material.h" | |
4 | + | |
5 | +#include "nearfield.h" | |
6 | +#include "microscope.h" | |
7 | +#include "rts/visualization/colormap.h" | |
8 | +#include "fileout.h" | |
9 | +extern microscopeStruct* SCOPE; | |
10 | +extern fileoutStruct gFileOut; | |
11 | + | |
12 | +//default values | |
13 | +#include "defaults.h" | |
14 | + | |
15 | +#include <string> | |
16 | +#include <sstream> | |
17 | +#include <fstream> | |
18 | +#include <limits> | |
19 | + | |
20 | +extern bool verbose; | |
21 | +extern bool gui; | |
22 | + | |
23 | +#ifdef _WIN32 | |
24 | + extern bool ansi; | |
25 | +#endif | |
26 | + | |
27 | +void SetArguments(rts::arglist &args) | |
28 | +{ | |
29 | + args.section("Interface Flags"); | |
30 | + args.add("help", "prints this help"); | |
31 | + args.add("gui", "run using the Qt GUI"); | |
32 | + args.add("verbose", "verbose output"); | |
33 | + | |
34 | +#ifdef _WIN32 | |
35 | + args.add("ansi", "activates ANSI in Windows"); | |
36 | +#endif | |
37 | + | |
38 | + args.section("Output Parameters"); | |
39 | + args.add("vector", "run a vector field simulation"); | |
40 | + args.add("intensity", "output measured intensity (filename)", DEFAULT_INTENSITY_FILE); | |
41 | + args.add("absorbance", "output measured absorbance (filename)", DEFAULT_ABSORBANCE_FILE); | |
42 | + args.add("transmittance", "output measured transmittance (filename)", DEFAULT_TRANSMITTANCE_FILE); | |
43 | + args.add("far-field", "output far-field at detector (filename)", DEFAULT_FAR_FILE); | |
44 | + args.add("near-field", "output field at focal plane (filename)", DEFAULT_NEAR_FILE); | |
45 | + args.add("extended-source", "image of source at focus (filename)", DEFAULT_EXTENDED_SOURCE); | |
46 | + args.add("output-type", "output field value", DEFAULT_FIELD_TYPE, "magnitude, polarization, real, imaginary"); | |
47 | + args.add("colormap", "colormap", DEFAULT_COLORMAP, "gray, brewer"); | |
48 | + args.add("append", "append result to an existing (binary) file"); | |
49 | + | |
50 | + args.section("Sphere Parameters"); | |
51 | + args.add("spheres", "sphere position", "", "--spheres x y z a m"); | |
52 | + args.add("sphere-file", "sphere file:", "", "[x y z radius material]"); | |
53 | + args.add("materials", "refractive indices as n, k pairs", DEFAULT_MATERIAL, "--materials n0 k0 n1 k1 n2 k2"); | |
54 | + args.add("material-file", "material file", "", "[lambda n k]"); | |
55 | + | |
56 | + args.section("Optics"); | |
57 | + args.add("lambda", "incident wavelength (micrometers)", DEFAULT_LAMBDA); | |
58 | + args.add("nu", "incident frequency (in cm^-1)\n(if specified, lambda is ignored)"); | |
59 | + args.add("k", "k-vector direction in spherical coordinates", "", "--k theta phi; theta = [0 2*pi], phi = [0 pi]"); | |
60 | + args.add("amplitude", "incident field amplitude", DEFAULT_AMPLITUDE); | |
61 | + args.add("condenser", "condenser numerical aperature\nA pair of values specify an inner obscuration", DEFAULT_CONDENSER); | |
62 | + args.add("objective", "objective numerical aperature\nA pair of values specify an inner obscuration", DEFAULT_OBJECTIVE); | |
63 | + args.add("focus", "focal position for the incident point source", DEFAULT_FOCUS); | |
64 | + args.add("plane-wave", "simulates an incident plane wave"); | |
65 | + | |
66 | + args.section("Imaging Parameters"); | |
67 | + args.add("resolution", "resolution of the detector", DEFAULT_SLICE_RES); | |
68 | + args.add("plane-lower-left", "lower-left position of the image plane", DEFAULT_PLANE_MIN); | |
69 | + args.add("plane-upper-right", "upper-right position of the image plane", DEFAULT_PLANE_MAX); | |
70 | + args.add("plane-normal", "normal for the image plane", DEFAULT_PLANE_NORM); | |
71 | + args.add("xy", "specify an x-y axis-aligned image (standard microscope)"); | |
72 | + args.add("xz", "specify a x-z axis-aligned image"); | |
73 | + args.add("yz", "specify a y-z axis-aligned image"); | |
74 | + | |
75 | + args.section("Sampling Parameters"); | |
76 | + args.add("samples", "Monte-Carlo samples used to compute Us", DEFAULT_SAMPLES); | |
77 | + args.add("padding", "FFT padding for the objective bandpass", DEFAULT_PADDING); | |
78 | + args.add("supersample", "super-sampling rate for the detector field", DEFAULT_SUPERSAMPLE); | |
79 | + args.add("field-order", "order of the incident field", DEFAULT_FIELD_ORDER); | |
80 | + args.add("seed", "seed for the Monte-Carlo random number generator"); | |
81 | + args.add("recursive", "evaluate all Bessel functions recursively"); | |
82 | + args.add("recursive-us", "evaluate scattered-field Bessel functions recursively"); | |
83 | + args.add("lut-uf", "evaluate the focused-field using a look-up table"); | |
84 | + | |
85 | +} | |
86 | + | |
87 | +void lFlags(rts::arglist args) | |
88 | +{ | |
89 | + | |
90 | + //flag for verbose output | |
91 | + if(args("verbose")) | |
92 | + verbose = true; | |
93 | + | |
94 | + if(args("recursive")) | |
95 | + { | |
96 | + SCOPE->nf.lut_us = false; | |
97 | + SCOPE->nf.lut_uf = false; | |
98 | + } | |
99 | + else if(args("recursive-us")) | |
100 | + { | |
101 | + SCOPE->nf.lut_us = false; | |
102 | + } | |
103 | + else if(args("lut-uf")) | |
104 | + { | |
105 | + SCOPE->nf.lut_uf = true; | |
106 | + } | |
107 | + | |
108 | + //gui | |
109 | + if(args("gui")) | |
110 | + gui = true; | |
111 | + | |
112 | + //ANSI output for color text | |
113 | +#ifdef _WIN32 | |
114 | + if(args("ansi")) | |
115 | + ansi = true; | |
116 | +#endif | |
117 | + | |
118 | +} | |
119 | + | |
120 | +void lWavelength(rts::arglist args) | |
121 | +{ | |
122 | + //load the wavelength | |
123 | + if(args("nu")) | |
124 | + { | |
125 | + //wavelength is given in wavenumber - transform and flag | |
126 | + SCOPE->nf.lambda = 10000/args["nu"].as_float(); | |
127 | + gFileOut.wavenumber = true; | |
128 | + } | |
129 | + //otherwise we are using lambda = wavelength | |
130 | + else | |
131 | + { | |
132 | + SCOPE->nf.lambda = args["lambda"].as_float(); | |
133 | + gFileOut.wavenumber = false; | |
134 | + } | |
135 | +} | |
136 | + | |
137 | +static void lMaterials(rts::arglist args) | |
138 | +{ | |
139 | + //if materials are specified at the command line | |
140 | + if(args("materials")) | |
141 | + { | |
142 | + rts::argument mats = args["materials"]; | |
143 | + int nMats = mats.nargs(); | |
144 | + | |
145 | + if(nMats == 1) | |
146 | + { | |
147 | + rts::material<ptype> newM(SCOPE->nf.lambda, mats.as_float(), 0); | |
148 | + SCOPE->nf.mVector.push_back(newM); | |
149 | + } | |
150 | + else if(nMats %2 != 0) | |
151 | + { | |
152 | + cout<<"BIMSim Error: materials must be specified in n, k pairs"<<endl; | |
153 | + exit(1); | |
154 | + } | |
155 | + else | |
156 | + { | |
157 | + for(unsigned int i=0; i<nMats; i+=2) | |
158 | + { | |
159 | + rts::material<ptype> newM(SCOPE->nf.lambda, mats.as_float(i), mats.as_float(i+1)); | |
160 | + SCOPE->nf.mVector.push_back(newM); | |
161 | + } | |
162 | + } | |
163 | + } | |
164 | + | |
165 | + //if file names are specified, load the materials | |
166 | + if(args("material-file")) | |
167 | + { | |
168 | + rts::argument matfiles = args["material-file"]; | |
169 | + int nMats = matfiles.nargs(); | |
170 | + | |
171 | + for(unsigned int i=0; i<nMats; i++) | |
172 | + { | |
173 | + //load the file into a string | |
174 | + //std::ifstream ifs(filenames[i].c_str()); | |
175 | + | |
176 | + //std::string instr((std::istreambuf_iterator<char>(ifs)), std::istreambuf_iterator<char>()); | |
177 | + | |
178 | + //load the list of spheres from a string | |
179 | + rts::material<ptype> newM(matfiles.as_text(i)); | |
180 | + //newM.fromStr(instr, ""); | |
181 | + SCOPE->nf.mVector.push_back(newM); | |
182 | + } | |
183 | + } | |
184 | + | |
185 | +} | |
186 | + | |
187 | +static void lSpheres(string sphereList) | |
188 | +{ | |
189 | + /*This function loads a list of spheres given in the string sphereList | |
190 | + The format is: | |
191 | + x y z a m | |
192 | + where | |
193 | + x, y, z = sphere position (required) | |
194 | + a = sphere radius (required) | |
195 | + m = material ID (optional) */ | |
196 | + | |
197 | + std::stringstream ss(sphereList); | |
198 | + | |
199 | + while(!ss.eof()) | |
200 | + { | |
201 | + //create a new sphere | |
202 | + sphere newS; | |
203 | + | |
204 | + //get the sphere data | |
205 | + ss>>newS.p[0]; | |
206 | + ss>>newS.p[1]; | |
207 | + ss>>newS.p[2]; | |
208 | + ss>>newS.a; | |
209 | + | |
210 | + if(ss.peek() != '\n') | |
211 | + ss>>newS.iMaterial; | |
212 | + | |
213 | + //add the new sphere to the sphere vector | |
214 | + SCOPE->nf.sVector.push_back(newS); | |
215 | + | |
216 | + //ignore the rest of the line | |
217 | + ss.ignore(1000, '\n'); | |
218 | + | |
219 | + //check out the next element (this should set the EOF error flag) | |
220 | + ss.peek(); | |
221 | + } | |
222 | +} | |
223 | + | |
224 | +void lSpheres(rts::arglist args) | |
225 | +{ | |
226 | + //if a sphere is specified at the command line | |
227 | + if(args("spheres")) | |
228 | + { | |
229 | + rts::argument sphere_arg = args["spheres"]; | |
230 | + int nArgs = sphere_arg.nargs(); | |
231 | + | |
232 | + //compute the number of spheres specified | |
233 | + unsigned int nS; | |
234 | + if(nArgs <= 5) | |
235 | + nS = 1; | |
236 | + else | |
237 | + { | |
238 | + //if the number of parameters is divisible by 4, compute the number of spheres | |
239 | + if(nArgs % 5 == 0) | |
240 | + nS = nArgs / 5; | |
241 | + else | |
242 | + { | |
243 | + cout<<"BIMSIM Error: Invalid number of sphere parameters."<<endl; | |
244 | + exit(1); | |
245 | + } | |
246 | + } | |
247 | + | |
248 | + stringstream ss; | |
249 | + | |
250 | + //for each sphere | |
251 | + for(unsigned int s=0; s<nS; s++) | |
252 | + { | |
253 | + //compute the number of sphere parameters | |
254 | + unsigned int nP; | |
255 | + if(nS == 1) nP = nArgs; | |
256 | + else nP = 5; | |
257 | + | |
258 | + //store each parameter as a string | |
259 | + for(unsigned int i=0; i<nP; i++) | |
260 | + { | |
261 | + ss<<sphere_arg.as_float(s*5 + i)<<" "; | |
262 | + } | |
263 | + ss<<endl; | |
264 | + } | |
265 | + | |
266 | + | |
267 | + | |
268 | + //convert the string to a sphere list | |
269 | + lSpheres(ss.str()); | |
270 | + } | |
271 | + | |
272 | + //if a files are specified | |
273 | + if(args("sphere-file")) | |
274 | + { | |
275 | + rts::argument sfiles = args["sphere-file"]; | |
276 | + int nFiles = sfiles.nargs(); | |
277 | + | |
278 | + //load each file | |
279 | + for(unsigned int iS=0; iS<nFiles; iS++) | |
280 | + { | |
281 | + //load the file into a string | |
282 | + std::ifstream ifs(sfiles.as_text(iS).c_str()); | |
283 | + | |
284 | + if(!ifs) | |
285 | + { | |
286 | + cout<<"Error loading sphere file."<<endl; | |
287 | + exit(1); | |
288 | + } | |
289 | + | |
290 | + std::string instr((std::istreambuf_iterator<char>(ifs)), std::istreambuf_iterator<char>()); | |
291 | + | |
292 | + //load the list of spheres from a string | |
293 | + lSpheres(instr); | |
294 | + } | |
295 | + } | |
296 | + | |
297 | + //make sure the appropriate materials are loaded | |
298 | + unsigned int nS = SCOPE->nf.sVector.size(); | |
299 | + | |
300 | + //for each sphere | |
301 | + for(unsigned int s = 0; s<nS; s++) | |
302 | + { | |
303 | + //make sure the corresponding material exists | |
304 | + if(SCOPE->nf.sVector[s].iMaterial + 1 > SCOPE->nf.mVector.size()) | |
305 | + { | |
306 | + //otherwise output an error | |
307 | + cout<<"BIMSIM Error - A material is not loaded for sphere "<<s+1<<"."<<endl; | |
308 | + cout<<"Material requested: "<<SCOPE->nf.sVector[s].iMaterial + 1<<endl; | |
309 | + cout<<"Number of materials: "<<SCOPE->nf.mVector.size()<<endl; | |
310 | + exit(1); | |
311 | + } | |
312 | + } | |
313 | +} | |
314 | + | |
315 | +static void lOptics(rts::arglist &args) | |
316 | +{ | |
317 | + if(args("objective")) | |
318 | + { | |
319 | + | |
320 | + if(args["objective"].nargs() == 1) | |
321 | + { | |
322 | + SCOPE->objective[0] = 0.0; | |
323 | + SCOPE->objective[1] = args["objective"].as_float(); | |
324 | + } | |
325 | + else | |
326 | + { | |
327 | + SCOPE->objective[0] = args["objective"].as_float(0); | |
328 | + SCOPE->objective[1] = args["objective"].as_float(1); | |
329 | + } | |
330 | + } | |
331 | +} | |
332 | + | |
333 | +static void lImagePlane(rts::arglist args) | |
334 | +{ | |
335 | + bsPoint pMin(DEFAULT_PLANE_MIN_X, DEFAULT_PLANE_MIN_Y, DEFAULT_PLANE_MIN_Z); | |
336 | + bsPoint pMax(DEFAULT_PLANE_MAX_X, DEFAULT_PLANE_MAX_Y, DEFAULT_PLANE_MAX_Z); | |
337 | + bsVector normal(DEFAULT_PLANE_NORM_X, DEFAULT_PLANE_NORM_Y, DEFAULT_PLANE_NORM_Z); | |
338 | + | |
339 | + //set the default values for the slice position and orientation | |
340 | + if(args("plane-lower-left") && args("plane-upper-right") && args("plane-normal")) | |
341 | + { | |
342 | + | |
343 | + | |
344 | + pMin = bsPoint(args["plane-lower-left"].as_float(0), args["plane-lower-left"].as_float(1), args["plane-lower-left"].as_float(2)); | |
345 | + pMax = bsPoint(args["plane-upper-right"].as_float(0), args["plane-upper-right"].as_float(1), args["plane-upper-right"].as_float(2)); | |
346 | + normal = bsVector(args["plane-normal"].as_float(0), args["plane-normal"].as_float(1), args["plane-normal"].as_float(2)); | |
347 | + } | |
348 | + else if(args("xy")) | |
349 | + { | |
350 | + //default plane size in microns | |
351 | + ptype s = DEFAULT_PLANE_SIZE; | |
352 | + ptype pos = DEFAULT_PLANE_POSITION; | |
353 | + | |
354 | + | |
355 | + if(args["xy"].nargs() >= 1) | |
356 | + s = args["xy"].as_float(0); | |
357 | + if(args["xy"].nargs() == 2) | |
358 | + pos = args["xy"].as_float(1); | |
359 | + | |
360 | + //calculate the plane corners and normal based on the size and position | |
361 | + pMin = bsPoint(-s/2, -s/2, pos); | |
362 | + pMax = bsPoint(s/2, s/2, pos); | |
363 | + normal = bsVector(0, 0, 1); | |
364 | + } | |
365 | + else if(args("xz")) | |
366 | + { | |
367 | + //default plane size in microns | |
368 | + ptype size = DEFAULT_PLANE_SIZE; | |
369 | + ptype pos = DEFAULT_PLANE_POSITION; | |
370 | + | |
371 | + if(args["xz"].nargs() >= 1) | |
372 | + size = args["xz"].as_float(0); | |
373 | + if(args["xz"].nargs() >= 2) | |
374 | + pos = args["xz"].as_float(1); | |
375 | + | |
376 | + //calculate the plane corners and normal based on the size and position | |
377 | + pMin = bsPoint(-size/2, pos, -size/2); | |
378 | + pMax = bsPoint(size/2, pos, size/2); | |
379 | + normal = bsVector(0, -1, 0); | |
380 | + } | |
381 | + else if(args("yz")) | |
382 | + { | |
383 | + //default plane size in microns | |
384 | + ptype size = DEFAULT_PLANE_SIZE; | |
385 | + ptype pos = DEFAULT_PLANE_POSITION; | |
386 | + | |
387 | + if(args["yz"].nargs() >= 1) | |
388 | + size = args["yz"].as_float(0); | |
389 | + if(args["yz"].nargs() >= 2) | |
390 | + pos = args["yz"].as_float(1); | |
391 | + | |
392 | + //calculate the plane corners and normal based on the size and position | |
393 | + pMin = bsPoint(pos, -size/2, -size/2); | |
394 | + pMax = bsPoint(pos, size/2, size/2); | |
395 | + normal = bsVector(1, 0, 0); | |
396 | + } | |
397 | + SCOPE->setPos(pMin, pMax, normal); | |
398 | + | |
399 | + //resolution | |
400 | + SCOPE->setRes(args["resolution"].as_float(), | |
401 | + args["resolution"].as_float(), | |
402 | + args["padding"].as_float(), | |
403 | + args["supersample"].as_float()); | |
404 | + | |
405 | + | |
406 | + | |
407 | + | |
408 | + | |
409 | + SCOPE->setNearfield(); | |
410 | +} | |
411 | + | |
412 | +static void lNearfield(rts::arglist args) | |
413 | +{ | |
414 | + //test to see if we are running a vector field simulation | |
415 | + bool vectorField = false; | |
416 | + if(args("vector")) | |
417 | + vectorField = true; | |
418 | + SCOPE->scalarSim = !vectorField; | |
419 | + | |
420 | + //test to see if we are simulating a plane wave | |
421 | + bool planeWave = DEFAULT_PLANEWAVE; | |
422 | + if(args("plane-wave")) | |
423 | + planeWave = !planeWave; | |
424 | + SCOPE->nf.planeWave = planeWave; | |
425 | + | |
426 | + //get the incident field amplitude | |
427 | + SCOPE->nf.A = args["amplitude"].as_float(); | |
428 | + | |
429 | + //get the condenser parameters | |
430 | + | |
431 | + if(args["condenser"].nargs() == 1) | |
432 | + { | |
433 | + SCOPE->nf.condenser[0] = 0; | |
434 | + SCOPE->nf.condenser[1] = args["condenser"].as_float(0); | |
435 | + } | |
436 | + else | |
437 | + { | |
438 | + SCOPE->nf.condenser[0] = args["condenser"].as_float(0); | |
439 | + SCOPE->nf.condenser[1] = args["condenser"].as_float(1); | |
440 | + } | |
441 | + | |
442 | + | |
443 | + | |
444 | + //get the focal rtsPoint position | |
445 | + SCOPE->nf.focus[0] = args["focus"].as_float(0); | |
446 | + SCOPE->nf.focus[1] = args["focus"].as_float(1); | |
447 | + SCOPE->nf.focus[2] = args["focus"].as_float(2); | |
448 | + | |
449 | + //get the incident light direction (k-vector) | |
450 | + bsVector spherical(1, 0, 0); | |
451 | + | |
452 | + //if a k-vector is specified | |
453 | + if(args("k")) | |
454 | + { | |
455 | + | |
456 | + spherical[1] = args["k"].as_float(0); | |
457 | + spherical[2] = args["k"].as_float(1); | |
458 | + } | |
459 | + SCOPE->nf.k = spherical.sph2cart(); | |
460 | + | |
461 | + | |
462 | + //incident field order | |
463 | + SCOPE->nf.m = args["field-order"].as_int(); | |
464 | + | |
465 | + //number of Monte-Carlo samples | |
466 | + SCOPE->nf.nWaves = args["samples"].as_int(); | |
467 | + | |
468 | + //random number seed for Monte-Carlo samples | |
469 | + if(args("seed")) | |
470 | + srand(args["seed"].as_int()); | |
471 | +} | |
472 | + | |
473 | +static void lOutputParams(rts::arglist args) | |
474 | +{ | |
475 | + //append simulation results to previous binary files | |
476 | + gFileOut.append = DEFAULT_APPEND; | |
477 | + if(args("append")) | |
478 | + gFileOut.append = true; | |
479 | + | |
480 | + //image parameters | |
481 | + //component of the field to be saved | |
482 | + std::string fieldStr; | |
483 | + fieldStr = args["output-type"].as_text(); | |
484 | + | |
485 | + if(fieldStr == "magnitude") | |
486 | + gFileOut.field = fileoutStruct::fieldMag; | |
487 | + else if(fieldStr == "intensity") | |
488 | + gFileOut.field = fileoutStruct::fieldIntensity; | |
489 | + else if(fieldStr == "polarization") | |
490 | + gFileOut.field = fileoutStruct::fieldPolar; | |
491 | + else if(fieldStr == "imaginary") | |
492 | + gFileOut.field = fileoutStruct::fieldImag; | |
493 | + else if(fieldStr == "real") | |
494 | + gFileOut.field = fileoutStruct::fieldReal; | |
495 | + else if(fieldStr == "angular-spectrum") | |
496 | + gFileOut.field = fileoutStruct::fieldAngularSpectrum; | |
497 | + | |
498 | + | |
499 | + //image file names | |
500 | + gFileOut.intFile = args["intensity"].as_text(); | |
501 | + gFileOut.absFile = args["absorbance"].as_text(); | |
502 | + | |
503 | + if(args("transmittance")) | |
504 | + gFileOut.transFile = args["transmittance"].as_text(); | |
505 | + gFileOut.nearFile = args["near-field"].as_text(); | |
506 | + gFileOut.farFile = args["far-field"].as_text(); | |
507 | + | |
508 | + //colormap | |
509 | + std::string cmapStr; | |
510 | + cmapStr = args["colormap"].as_text(); | |
511 | + if(cmapStr == "brewer") | |
512 | + gFileOut.colormap = rts::cmBrewer; | |
513 | + else if(cmapStr == "gray") | |
514 | + gFileOut.colormap = rts::cmGrayscale; | |
515 | + else | |
516 | + cout<<"color-map value not recognized (using default): "<<cmapStr<<endl; | |
517 | +} | |
518 | + | |
519 | +void LoadParameters(rts::arglist &args) | |
520 | +{ | |
521 | + lFlags(args); | |
522 | + lWavelength(args); | |
523 | + lMaterials(args); | |
524 | + lSpheres(args); | |
525 | + lOptics(args); | |
526 | + lImagePlane(args); | |
527 | + lNearfield(args); | |
528 | + lOutputParams(args); | |
529 | + | |
530 | + //if an extended source will be used | |
531 | + if(args("extended-source")) | |
532 | + { | |
533 | + //load the point sources | |
534 | + string filename = args["extended-source"].as_text(); | |
535 | + SCOPE->LoadExtendedSource(filename); | |
536 | + | |
537 | + } | |
538 | + | |
539 | +} | |
540 | + | |
541 | +static void OutputOptions() | |
542 | +{ | |
543 | + cout<<SCOPE->toStr(); | |
544 | + | |
545 | + cout<<"# of source points: "<<SCOPE->focalPoints.size()<<endl; | |
546 | + | |
547 | +} | ... | ... |