| 714 |
iacus |
1 |
/*
|
|
|
2 |
* R.app : a Cocoa front end to: "R A Computer Language for Statistical Data Analysis"
|
|
|
3 |
*
|
|
|
4 |
* R.app Copyright notes:
|
| 1254 |
iacus |
5 |
* Copyright (C) 2004-5 The R Foundation
|
| 714 |
iacus |
6 |
* written by Stefano M. Iacus and Simon Urbanek
|
|
|
7 |
*
|
|
|
8 |
*
|
|
|
9 |
* R Copyright notes:
|
|
|
10 |
* Copyright (C) 1995-1996 Robert Gentleman and Ross Ihaka
|
| 6408 |
ripley |
11 |
* Copyright (C) 1998-2012 The R Development Core Team
|
| 714 |
iacus |
12 |
* Copyright (C) 2002-2004 The R Foundation
|
|
|
13 |
*
|
|
|
14 |
* This program is free software; you can redistribute it and/or modify
|
|
|
15 |
* it under the terms of the GNU General Public License as published by
|
|
|
16 |
* the Free Software Foundation; either version 2 of the License, or
|
|
|
17 |
* (at your option) any later version.
|
|
|
18 |
*
|
|
|
19 |
* This program is distributed in the hope that it will be useful,
|
|
|
20 |
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
21 |
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
22 |
* GNU General Public License for more details.
|
|
|
23 |
*
|
|
|
24 |
* A copy of the GNU General Public License is available via WWW at
|
|
|
25 |
* http://www.gnu.org/copyleft/gpl.html. You can also obtain it by
|
|
|
26 |
* writing to the Free Software Foundation, Inc., 59 Temple Place,
|
|
|
27 |
* Suite 330, Boston, MA 02111-1307 USA.
|
|
|
28 |
*/
|
|
|
29 |
|
| 568 |
iacus |
30 |
#import "DataManager.h"
|
|
|
31 |
#import "RController.h"
|
| 816 |
urbaneks |
32 |
#import "REngine/REngine.h"
|
| 5737 |
urbaneks |
33 |
#import "RegexKitLite.h"
|
| 568 |
iacus |
34 |
|
|
|
35 |
#import <WebKit/WebKit.h>
|
|
|
36 |
#import <WebKit/WebFrame.h>
|
|
|
37 |
|
| 5729 |
urbaneks |
38 |
#define kDataManagerData @"data"
|
|
|
39 |
#define kDataManagerPackage @"package"
|
|
|
40 |
#define kDataManagerDescription @"description"
|
|
|
41 |
#define kDataManagerURL @"URL"
|
| 568 |
iacus |
42 |
|
| 5729 |
urbaneks |
43 |
#define kSortModeNone 0
|
|
|
44 |
#define kSortModeAsc 1
|
|
|
45 |
#define kSortModeDesc 2
|
|
|
46 |
|
|
|
47 |
static DataManager* sharedController;
|
|
|
48 |
|
| 568 |
iacus |
49 |
@implementation DataManager
|
|
|
50 |
|
| 5729 |
urbaneks |
51 |
+ (DataManager*) sharedController{
|
|
|
52 |
return sharedController;
|
|
|
53 |
}
|
|
|
54 |
|
| 568 |
iacus |
55 |
- (void)awakeFromNib
|
|
|
56 |
{
|
| 688 |
urbaneks |
57 |
[RDataSource setDoubleAction:@selector(loadRData:)];
|
| 5705 |
urbaneks |
58 |
[dataInfoView setFrameLoadDelegate:self];
|
| 5729 |
urbaneks |
59 |
[self enableGUIActions:NO];
|
| 568 |
iacus |
60 |
}
|
|
|
61 |
|
|
|
62 |
- (id)init
|
|
|
63 |
{
|
| 5729 |
urbaneks |
64 |
self = [super init];
|
|
|
65 |
if (self) {
|
| 688 |
urbaneks |
66 |
sharedController = self;
|
| 5729 |
urbaneks |
67 |
datasets = [[NSMutableArray alloc] initWithCapacity:500];
|
|
|
68 |
filteredDatasets = [[NSMutableArray alloc] initWithCapacity:500];
|
|
|
69 |
sortMode = kSortModeNone;
|
|
|
70 |
sortedColumn = nil;
|
| 568 |
iacus |
71 |
}
|
| 5729 |
urbaneks |
72 |
|
|
|
73 |
return self;
|
| 568 |
iacus |
74 |
}
|
|
|
75 |
|
|
|
76 |
- (void)dealloc {
|
| 5729 |
urbaneks |
77 |
if(datasets) [datasets release], datasets = nil;
|
|
|
78 |
if(filteredDatasets) [filteredDatasets release], filteredDatasets = nil;
|
| 568 |
iacus |
79 |
[super dealloc];
|
|
|
80 |
}
|
|
|
81 |
|
| 5729 |
urbaneks |
82 |
#pragma mark -
|
|
|
83 |
|
|
|
84 |
- (NSWindow*)window
|
| 688 |
urbaneks |
85 |
{
|
| 5729 |
urbaneks |
86 |
return DataManagerWindow;
|
| 688 |
urbaneks |
87 |
}
|
|
|
88 |
|
| 5729 |
urbaneks |
89 |
- (void)show
|
| 568 |
iacus |
90 |
{
|
| 5729 |
urbaneks |
91 |
[RDataSource reloadData];
|
|
|
92 |
[DataManagerWindow makeKeyAndOrderFront:self];
|
|
|
93 |
}
|
|
|
94 |
|
|
|
95 |
#pragma mark -
|
|
|
96 |
|
|
|
97 |
- (void)updateDatasets:(int)count withNames:(char**)name descriptions:(char**)desc packages:(char**)pkg URLs:(char**)url
|
|
|
98 |
{
|
|
|
99 |
|
|
|
100 |
[self enableGUIActions:NO];
|
|
|
101 |
[[self window] display];
|
| 688 |
urbaneks |
102 |
[self show];
|
| 5729 |
urbaneks |
103 |
[self resetDatasets];
|
|
|
104 |
|
|
|
105 |
NSInteger i = 0;
|
|
|
106 |
|
|
|
107 |
for(i = 0; i < count; i++) {
|
|
|
108 |
NSDictionary *entry = [NSDictionary dictionaryWithObjectsAndKeys:
|
|
|
109 |
[NSString stringWithUTF8String: name[i]], kDataManagerData,
|
|
|
110 |
[NSString stringWithUTF8String: desc[i]], kDataManagerDescription,
|
|
|
111 |
[NSString stringWithUTF8String: pkg[i]], kDataManagerPackage,
|
|
|
112 |
[NSString stringWithUTF8String: url[i]], kDataManagerURL,
|
|
|
113 |
nil
|
|
|
114 |
];
|
|
|
115 |
[datasets addObject:entry];
|
|
|
116 |
[filteredDatasets addObject:entry];
|
|
|
117 |
}
|
|
|
118 |
|
|
|
119 |
[self filterTable:nil];
|
|
|
120 |
[RDataSource reloadData];
|
|
|
121 |
[self enableGUIActions:YES];
|
|
|
122 |
|
| 568 |
iacus |
123 |
}
|
|
|
124 |
|
| 5729 |
urbaneks |
125 |
- (void)resetDatasets
|
| 568 |
iacus |
126 |
{
|
| 5729 |
urbaneks |
127 |
[datasets removeAllObjects];
|
|
|
128 |
[filteredDatasets removeAllObjects];
|
| 568 |
iacus |
129 |
}
|
|
|
130 |
|
| 5729 |
urbaneks |
131 |
- (NSInteger)count
|
|
|
132 |
{
|
|
|
133 |
return [datasets count];
|
| 568 |
iacus |
134 |
}
|
|
|
135 |
|
| 5729 |
urbaneks |
136 |
- (void)enableGUIActions:(BOOL)enabled
|
| 568 |
iacus |
137 |
{
|
| 5729 |
urbaneks |
138 |
[loadButton setEnabled:enabled];
|
|
|
139 |
[refreshButton setEnabled:enabled];
|
|
|
140 |
[searchField setEnabled:enabled];
|
|
|
141 |
[RDataSource setEnabled:enabled];
|
| 568 |
iacus |
142 |
}
|
|
|
143 |
|
| 5729 |
urbaneks |
144 |
#pragma mark -
|
|
|
145 |
|
| 568 |
iacus |
146 |
- (IBAction)loadRData:(id)sender
|
|
|
147 |
{
|
| 5729 |
urbaneks |
148 |
|
|
|
149 |
NSIndexSet *selectedRows = [RDataSource selectedRowIndexes];
|
|
|
150 |
|
|
|
151 |
if(![selectedRows count]) return;
|
|
|
152 |
|
|
|
153 |
NSInteger anIndex = [selectedRows firstIndex];
|
|
|
154 |
NSMutableArray *data = [[NSMutableArray alloc] initWithCapacity:[selectedRows count]];
|
|
|
155 |
NSMutableArray *pkgs = [[NSMutableArray alloc] initWithCapacity:[selectedRows count]];
|
|
|
156 |
while(anIndex != NSNotFound) {
|
|
|
157 |
[data addObject:[[filteredDatasets objectAtIndex:anIndex] objectForKey:kDataManagerData]];
|
|
|
158 |
[pkgs addObject:[[filteredDatasets objectAtIndex:anIndex] objectForKey:kDataManagerPackage]];
|
|
|
159 |
anIndex = [selectedRows indexGreaterThanIndex:anIndex];
|
|
|
160 |
}
|
|
|
161 |
[[REngine mainEngine] evaluateString:[NSString stringWithFormat:@"data(%@,package=c(\"%@\"))",
|
|
|
162 |
[data componentsJoinedByString:@","],
|
|
|
163 |
[pkgs componentsJoinedByString:@"\",\""]]];
|
| 568 |
iacus |
164 |
}
|
|
|
165 |
|
| 5729 |
urbaneks |
166 |
- (IBAction)filterTable:(id)sender
|
|
|
167 |
{
|
|
|
168 |
|
|
|
169 |
NSString *searchPattern = [searchField stringValue];
|
|
|
170 |
|
|
|
171 |
if(![searchPattern length]) {
|
|
|
172 |
[filteredDatasets setArray:datasets];
|
|
|
173 |
[RDataSource deselectAll:nil];
|
|
|
174 |
} else {
|
|
|
175 |
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"data CONTAINS[cd] %@ OR package CONTAINS[cd] %@ OR description CONTAINS[cd] %@",
|
|
|
176 |
searchPattern, searchPattern, searchPattern];
|
|
|
177 |
[filteredDatasets setArray:[datasets filteredArrayUsingPredicate:predicate]];
|
|
|
178 |
}
|
|
|
179 |
|
|
|
180 |
if(sortedColumn) {
|
|
|
181 |
[RDataSource setIndicatorImage:(sortMode == kSortModeAsc) ? [NSImage imageNamed:@"NSAscendingSortIndicator"] : [NSImage imageNamed:@"NSDescendingSortIndicator"] inTableColumn:[RDataSource tableColumnWithIdentifier:sortedColumn]];
|
|
|
182 |
[RDataSource setHighlightedTableColumn:[RDataSource tableColumnWithIdentifier:sortedColumn]];
|
|
|
183 |
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:sortedColumn ascending:(sortMode == kSortModeAsc)
|
|
|
184 |
selector:@selector(localizedCaseInsensitiveCompare:)];
|
|
|
185 |
[filteredDatasets sortUsingDescriptors:
|
|
|
186 |
[NSArray arrayWithObjects:sortDescriptor, nil]];
|
|
|
187 |
[sortDescriptor release];
|
|
|
188 |
}
|
|
|
189 |
|
|
|
190 |
|
|
|
191 |
[RDataSource reloadData];
|
|
|
192 |
}
|
|
|
193 |
|
|
|
194 |
- (IBAction)reloadDatasets:(id)sender
|
|
|
195 |
{
|
|
|
196 |
[self enableGUIActions:NO];
|
|
|
197 |
[[self window] display];
|
|
|
198 |
[[REngine mainEngine] executeString:@"data.manager()"];
|
|
|
199 |
}
|
|
|
200 |
|
| 568 |
iacus |
201 |
- (IBAction)showHelp:(id)sender
|
|
|
202 |
{
|
| 5729 |
urbaneks |
203 |
|
|
|
204 |
if([RDataSource numberOfSelectedRows] != 1) return;
|
|
|
205 |
|
|
|
206 |
NSInteger row = [RDataSource selectedRow];
|
|
|
207 |
|
|
|
208 |
NSDictionary *selectedItem = [filteredDatasets objectAtIndex:row];
|
|
|
209 |
|
|
|
210 |
SLog(@"DataManager showHelp: %@", selectedItem);
|
|
|
211 |
|
| 5535 |
urbaneks |
212 |
NSString *urlText = nil;
|
|
|
213 |
int port = [[RController sharedController] helpServerPort];
|
|
|
214 |
if (port == 0) {
|
|
|
215 |
NSRunInformationalAlertPanel(NLS(@"Cannot start HTML help server."), NLS(@"Help"), NLS(@"Ok"), nil, nil);
|
|
|
216 |
return;
|
|
|
217 |
}
|
| 5729 |
urbaneks |
218 |
|
|
|
219 |
NSString *topic = [selectedItem objectForKey:kDataManagerData];
|
| 5535 |
urbaneks |
220 |
NSRange r = [topic rangeOfString:@" ("];
|
|
|
221 |
if (r.length > 0 && [topic length] - r.length > 3) // some datasets have the topic in parents
|
|
|
222 |
topic = [topic substringWithRange: NSMakeRange(r.location + 2, [topic length] - r.location - 3)];
|
| 5729 |
urbaneks |
223 |
|
| 7337 |
urbaneks |
224 |
urlText = [NSString stringWithFormat:@"http://localhost:%d/library/%@/html/%@.html", port, [selectedItem objectForKey:kDataManagerPackage], topic];
|
| 5729 |
urbaneks |
225 |
|
| 568 |
iacus |
226 |
[[dataInfoView mainFrame] loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:urlText]]];
|
| 5729 |
urbaneks |
227 |
|
| 568 |
iacus |
228 |
}
|
|
|
229 |
|
| 5729 |
urbaneks |
230 |
- (IBAction)printDocument:(id)sender
|
| 568 |
iacus |
231 |
{
|
| 5729 |
urbaneks |
232 |
|
|
|
233 |
[NSObject cancelPreviousPerformRequestsWithTarget:self
|
|
|
234 |
selector:@selector(showHelp:)
|
|
|
235 |
object:RDataSource];
|
|
|
236 |
|
|
|
237 |
NSPrintInfo *printInfo;
|
|
|
238 |
NSPrintOperation *printOp;
|
|
|
239 |
|
|
|
240 |
printInfo = [NSPrintInfo sharedPrintInfo];
|
|
|
241 |
[printInfo setHorizontalPagination: NSFitPagination];
|
|
|
242 |
[printInfo setVerticalPagination: NSAutoPagination];
|
|
|
243 |
[printInfo setVerticallyCentered:NO];
|
|
|
244 |
|
|
|
245 |
printOp = [NSPrintOperation printOperationWithView:[[[dataInfoView mainFrame] frameView] documentView]
|
|
|
246 |
printInfo:printInfo];
|
|
|
247 |
[printOp setShowPanels:YES];
|
|
|
248 |
[printOp runOperationModalForWindow:[self window]
|
|
|
249 |
delegate:self
|
|
|
250 |
didRunSelector:@selector(sheetDidEnd:returnCode:contextInfo:)
|
|
|
251 |
contextInfo:@""];
|
| 568 |
iacus |
252 |
}
|
|
|
253 |
|
| 5737 |
urbaneks |
254 |
- (IBAction)executeSelection:(id)sender
|
|
|
255 |
{
|
|
|
256 |
DOMRange *dr = [dataInfoView selectedDOMRange];
|
|
|
257 |
if (dr) { /* we don't do line-exec since we don't get the text outside the selection */
|
|
|
258 |
NSString *stx = [dr markupString];
|
|
|
259 |
// Ok, some simple processing here - it may not work in all cases
|
|
|
260 |
stx = [stx stringByReplacingOccurrencesOfRegex:@"(?i)<br[^>]*?>" withString:@"\n"];
|
|
|
261 |
stx = [stx stringByReplacingOccurrencesOfRegex:@"<[^>]*?>" withString:@""];
|
|
|
262 |
stx = [stx stringByReplacingOccurrencesOfString:@"<" withString:@"<"];
|
|
|
263 |
stx = [stx stringByReplacingOccurrencesOfString:@">" withString:@">"];
|
|
|
264 |
stx = [stx stringByReplacingOccurrencesOfString:@"&" withString:@"&"];
|
|
|
265 |
[[RController sharedController] sendInput:stx];
|
|
|
266 |
}
|
|
|
267 |
}
|
|
|
268 |
|
| 5729 |
urbaneks |
269 |
#pragma mark -
|
|
|
270 |
#pragma mark tableView delegates
|
|
|
271 |
|
|
|
272 |
- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView
|
| 688 |
urbaneks |
273 |
{
|
| 5729 |
urbaneks |
274 |
return [filteredDatasets count];
|
| 688 |
urbaneks |
275 |
}
|
|
|
276 |
|
| 5729 |
urbaneks |
277 |
- (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)rowIndex
|
|
|
278 |
{
|
|
|
279 |
if([[tableColumn identifier] isEqualToString:kDataManagerPackage])
|
|
|
280 |
return [[filteredDatasets objectAtIndex:rowIndex] objectForKey:kDataManagerPackage];
|
|
|
281 |
else if([[tableColumn identifier] isEqualToString:kDataManagerData])
|
|
|
282 |
return [[filteredDatasets objectAtIndex:rowIndex] objectForKey:kDataManagerData];
|
|
|
283 |
else if([[tableColumn identifier] isEqualToString:kDataManagerDescription])
|
|
|
284 |
return [[filteredDatasets objectAtIndex:rowIndex] objectForKey:kDataManagerDescription];
|
|
|
285 |
return @"...";
|
|
|
286 |
}
|
|
|
287 |
|
|
|
288 |
- (BOOL)tableView:(NSTableView *)aTableView shouldEditTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndex
|
|
|
289 |
{
|
|
|
290 |
return NO;
|
|
|
291 |
}
|
|
|
292 |
|
| 5705 |
urbaneks |
293 |
- (void)tableViewSelectionDidChange:(NSNotification *)aNotification
|
|
|
294 |
{
|
|
|
295 |
// Check our notification object is our table
|
|
|
296 |
if ([aNotification object] != RDataSource) return;
|
|
|
297 |
|
|
|
298 |
// Update Info delayed
|
|
|
299 |
[NSObject cancelPreviousPerformRequestsWithTarget:self
|
|
|
300 |
selector:@selector(showHelp:)
|
|
|
301 |
object:RDataSource];
|
|
|
302 |
|
|
|
303 |
[self performSelector:@selector(showHelp:) withObject:RDataSource afterDelay:0.5];
|
|
|
304 |
|
|
|
305 |
}
|
|
|
306 |
|
| 5729 |
urbaneks |
307 |
- (void)tableView:(NSTableView*)tableView didClickTableColumn:(NSTableColumn *)tableColumn
|
| 5705 |
urbaneks |
308 |
{
|
| 5729 |
urbaneks |
309 |
|
|
|
310 |
[tableView deselectAll:nil];
|
|
|
311 |
|
|
|
312 |
// Tri-state sorting none -> asc -> desc -> none -> ...
|
|
|
313 |
if(!sortedColumn || (sortedColumn && [sortedColumn isEqualToString:[tableColumn identifier]])) {
|
|
|
314 |
sortMode++;
|
|
|
315 |
if (sortMode > kSortModeDesc) sortMode = kSortModeNone;
|
|
|
316 |
} else {
|
|
|
317 |
sortMode = kSortModeAsc;
|
|
|
318 |
}
|
|
|
319 |
|
|
|
320 |
// remove sort indicator of old column
|
|
|
321 |
if(sortedColumn) {
|
|
|
322 |
[self filterTable:nil];
|
|
|
323 |
[tableView setIndicatorImage:nil inTableColumn:[tableView tableColumnWithIdentifier:sortedColumn]];
|
|
|
324 |
}
|
|
|
325 |
|
|
|
326 |
// remember last to be sorted column
|
|
|
327 |
sortedColumn = [tableColumn identifier];
|
|
|
328 |
|
|
|
329 |
NSSortDescriptor* sortDescriptor;
|
|
|
330 |
|
|
|
331 |
switch(sortMode) {
|
|
|
332 |
case kSortModeNone:
|
|
|
333 |
[tableView setIndicatorImage:nil inTableColumn:tableColumn];
|
|
|
334 |
[tableView setHighlightedTableColumn:nil];
|
|
|
335 |
[filteredDatasets removeAllObjects];
|
|
|
336 |
[filteredDatasets setArray:datasets];
|
|
|
337 |
sortedColumn = nil;
|
|
|
338 |
break;
|
|
|
339 |
case kSortModeAsc:
|
|
|
340 |
[tableView setIndicatorImage:[NSImage imageNamed:@"NSAscendingSortIndicator"] inTableColumn:tableColumn];
|
|
|
341 |
[tableView setHighlightedTableColumn:tableColumn];
|
|
|
342 |
sortDescriptor = [[NSSortDescriptor alloc] initWithKey:sortedColumn ascending:YES
|
|
|
343 |
selector:@selector(localizedCaseInsensitiveCompare:)];
|
|
|
344 |
[filteredDatasets sortUsingDescriptors:
|
|
|
345 |
[NSArray arrayWithObjects:sortDescriptor, nil]];
|
|
|
346 |
[sortDescriptor release];
|
|
|
347 |
break;
|
|
|
348 |
case kSortModeDesc:
|
|
|
349 |
[tableView setIndicatorImage:[NSImage imageNamed:@"NSDescendingSortIndicator"] inTableColumn:tableColumn];
|
|
|
350 |
[tableView setHighlightedTableColumn:tableColumn];
|
|
|
351 |
sortDescriptor = [[NSSortDescriptor alloc] initWithKey:sortedColumn ascending:NO
|
|
|
352 |
selector:@selector(localizedCaseInsensitiveCompare:)];
|
|
|
353 |
[filteredDatasets sortUsingDescriptors:
|
|
|
354 |
[NSArray arrayWithObjects:sortDescriptor, nil]];
|
|
|
355 |
[sortDescriptor release];
|
|
|
356 |
break;
|
|
|
357 |
}
|
|
|
358 |
|
|
|
359 |
[RDataSource reloadData];
|
|
|
360 |
|
| 5705 |
urbaneks |
361 |
}
|
|
|
362 |
|
| 5729 |
urbaneks |
363 |
#pragma mark -
|
|
|
364 |
|
|
|
365 |
- (void)sheetDidEnd:(id)sheet returnCode:(int)returnCode contextInfo:(NSString *)contextInfo
|
| 5705 |
urbaneks |
366 |
{
|
|
|
367 |
|
| 5729 |
urbaneks |
368 |
SLog(@"DataManager: sheetDidEnd: returnCode: %d contextInfo: %@", returnCode, contextInfo);
|
| 5705 |
urbaneks |
369 |
|
| 5729 |
urbaneks |
370 |
// Order out the sheet - could be a NSPanel or NSWindow
|
|
|
371 |
if ([sheet respondsToSelector:@selector(orderOut:)]) {
|
|
|
372 |
[sheet orderOut:nil];
|
|
|
373 |
}
|
|
|
374 |
else if ([sheet respondsToSelector:@selector(window)]) {
|
|
|
375 |
[[sheet window] orderOut:nil];
|
|
|
376 |
}
|
|
|
377 |
|
|
|
378 |
[DataManagerWindow makeKeyAndOrderFront:nil];
|
|
|
379 |
|
| 5705 |
urbaneks |
380 |
}
|
|
|
381 |
|
| 5737 |
urbaneks |
382 |
- (BOOL)validateMenuItem:(NSMenuItem *)menuItem
|
|
|
383 |
{
|
|
|
384 |
if ([menuItem action] == @selector(executeSelection:)) {
|
|
|
385 |
return ([dataInfoView selectedDOMRange] == nil) ? NO : YES;
|
|
|
386 |
}
|
|
|
387 |
|
|
|
388 |
return YES;
|
|
|
389 |
}
|
|
|
390 |
|
| 568 |
iacus |
391 |
@end
|