-
Notifications
You must be signed in to change notification settings - Fork 0
/
GraphView.m
473 lines (337 loc) · 17 KB
/
GraphView.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
//
// GraphView.m
// Metasome
//
// Created by Omar Metwally on 8/21/13.
// Copyright (c) 2013 Logisome. All rights reserved.
//
#import "GraphView.h"
#import "MetasomeDataPointStore.h"
#import "MetasomeDataPoint.h"
#import "MetasomeParameter.h"
#import "MetasomeEvent.h"
#import "MetasomeEventStore.h"
#import "Legend.h"
#import "GraphViewController.h"
#import "HoveringLabel.h"
#import "LineView.h"
#import <QuartzCore/QuartzCore.h>
@implementation GraphView
float const POINT_WIDTH = 10;
float const POINT_HEIGHT = 10;
float const HORIZONTAL_NUMBER_OF_INTERVALS = 20.0;
float const AXIS_LABEL_FONTSIZE = 12.0;
//float const EVENT_BAR_WIDTH = 30.0;
float const EVENT_FONT_SIZE = 15.0;
float const ORIGIN_HORIZONTAL_OFFSET = 50.0;
float const ORIGIN_VERTICAL_OFFSET = 50.0;
float const VERTICAL_AXIS_LINE_WIDTH = 3.0;
float const TOP_BUFFER = 50;
float const RIGHT_BUFFER = 180;
float const VERTICAL_NUMBER_OF_INTERVALS = 10.0;
float const MINIMUM_TOUCH_DISTANCE = 20.0;
float const HORIZONTAL_AXIS_LABEL_WIDTH = 30.0;
float const HORIZONTAL_AXIS_LABEL_HEIGHT = 30.0;
@synthesize name, scrollViewWidth, scrollViewHeight;
@synthesize horizontalScaleFactor, verticalScaleFactor;
@synthesize minValueOnHorizontalAxis, maxValueOnHorizontalAxis, minValueOnVerticalAxis, maxValueOnVerticalAxis;
@synthesize originHorizontalOffset, originVerticalOffset, topBuffer, bottomBuffer, rightBuffer, leftBuffer;
@synthesize horizontalAxisLength, verticalAxisLength, graphTitle;
@synthesize ctx, transform;
@synthesize clearSubviewBlock, parentContext;
@synthesize pointsToGraph;
@synthesize parentGraphViewController;
@synthesize legend, drawEventsFlag, dataPointCoordinates;
@synthesize selectedPointIndex;
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self setGraphTitle:[[[MetasomeDataPointStore sharedStore] toGraph] stringByAppendingString:@" vs time"]];
[self setOriginHorizontalOffset:ORIGIN_HORIZONTAL_OFFSET];
[self setOriginVerticalOffset:ORIGIN_VERTICAL_OFFSET];
[self setTopBuffer:TOP_BUFFER];
[self setRightBuffer:RIGHT_BUFFER];
inputType = [[[MetasomeDataPointStore sharedStore] parameterToGraph] inputType];
[self initializeGraphView];
UILongPressGestureRecognizer *longPressRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(longPress:)];
[longPressRecognizer setMinimumPressDuration:0.1];
[self addGestureRecognizer:longPressRecognizer];
}
return self;
}
-(void)longPress:(UIGestureRecognizer *)gr
{
if ( [gr state] == UIGestureRecognizerStateBegan ) {
CGPoint touchedPoint = [gr locationInView:self];
[self performSelectorInBackground:@selector(showMenuAtPoint:) withObject:[NSValue valueWithCGPoint:touchedPoint]];
}
}
-(void)showMenuAtPoint:(NSValue *)point
{
CGPoint touched = [point CGPointValue];
CGPoint currentPoint;
// iterate through dataPointCoordinates until a nearby
// coordinate is found
float smallestX, smallestY;
float smallestDistance = 1000.0;
int pointIndex = 0;
int pointIndexCounter = 0;
for (NSValue *val in dataPointCoordinates) {
currentPoint = [val CGPointValue];
//NSLog(@"x, y = %f, %f", currentPoint.x, currentPoint.y);
if ( [self distanceBetweenPoint:currentPoint andPoint:touched] < smallestDistance) {
smallestDistance = [self distanceBetweenPoint:currentPoint andPoint:touched];
smallestX = currentPoint.x;
smallestY = currentPoint.y;
pointIndex = pointIndexCounter;
[self setSelectedPointIndex:pointIndex];
}
pointIndexCounter += 1;
}
// return pointIndex to last index position
pointIndexCounter -= 1;
// check for minimum distance before showing menu
if (smallestDistance > MINIMUM_TOUCH_DISTANCE)
return;
// convert screen coordinates of closest point to
// a value/date pair
float yValue = (smallestY + [self originVerticalOffset] - [self scrollViewHeight])/(-1*[self verticalScaleFactor]) + [self minValueOnVerticalAxis];
float xValue = (smallestX - [self originHorizontalOffset])/[self horizontalScaleFactor] + [self minValueOnHorizontalAxis];
NSDate *date = [[NSDate alloc] initWithTimeIntervalSince1970:xValue];
NSString *dateString, *valueString, *displayString;
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:@"dd MMM"];
NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init];
[numberFormatter setMaximumFractionDigits:0];
[numberFormatter setRoundingMode:NSNumberFormatterRoundUp];
//valueString = [numberFormatter stringFromNumber:[NSNumber numberWithFloat:yValue]];
valueString = [[NSNumber numberWithFloat:[[pointsToGraph objectAtIndex:pointIndex] parameterValue]] stringValue];
dateString = [dateFormatter stringFromDate:date];
displayString = [[[[[[MetasomeDataPointStore sharedStore] toGraph] stringByAppendingString:@": "] stringByAppendingString:valueString] stringByAppendingString:@" on "] stringByAppendingString:dateString];
// Must set view to first responder in order to use UIMenu
[self becomeFirstResponder];
UIMenuController *menu = [UIMenuController sharedMenuController];
UIMenuItem *infoItem = [[UIMenuItem alloc] initWithTitle:displayString action:@selector(hideMenu)];
UIMenuItem *deleteItem = [[UIMenuItem alloc] initWithTitle:@"delete" action:@selector(deletePoint)];
[menu setMenuItems:[NSArray arrayWithObjects:infoItem, deleteItem, nil]];
[menu setTargetRect:CGRectMake(smallestX, smallestY, 2, 2) inView:self];
[menu setMenuVisible:YES animated:YES];
[self setNeedsDisplay];
}
-(void)deletePoint
{
MetasomeDataPoint *pointToDelete = [[self pointsToGraph] objectAtIndex:selectedPointIndex];
[[self pointsToGraph] removeObjectAtIndex:selectedPointIndex];
[[MetasomeDataPointStore sharedStore] removePoint:pointToDelete];
[[MetasomeDataPointStore sharedStore] saveChanges];
[self initializeGraphView];
[self setNeedsDisplay];
}
-(float)distanceBetweenPoint:(CGPoint)pointOne andPoint:(CGPoint)pointTwo
{
float xDist = (pointOne.x - pointTwo.x)*(pointOne.x - pointTwo.x);
float yDist = (pointOne.y - pointTwo.y)*(pointOne.y - pointTwo.y);
//float distance = sqrt( pow(abs(pointOne.x - pointTwo.x), 2) + pow(abs(pointOne.y - pointTwo.y), 2) );
//float distance =
float distance = sqrtf(xDist + yDist);
return distance;
}
-(void)initializeGraphView
{
NSString *n = [[[MetasomeDataPointStore sharedStore] parameterToGraph] parameterName];
NSDate *from = [[MetasomeDataPointStore sharedStore] since];
NSDate *to = [NSDate date];
pointsToGraph = [[NSMutableArray alloc] initWithArray:[[MetasomeDataPointStore sharedStore] pointsWithParameterName:n fromDate:from toDate:to]];
[self setScale:pointsToGraph];
[self convertDataPointsToCoordinates:pointsToGraph];
CGPoint here = CGPointMake([self originHorizontalOffset] + [self horizontalAxisLength] + 40, [self topBuffer]);
}
/* convertDataPointsToCoordinates takes a mutable array of
MetasomeDataPoints as an argument, iteratively converts the
points into an array of CGPoints, and returns this array.
-Note: Call this method only after calling initializeGraphView
*/
-(void)convertDataPointsToCoordinates:(NSMutableArray *)dataPoints
{
dataPointCoordinates = [[NSMutableArray alloc] init];
float horizontalPos, verticalPos;
for (MetasomeDataPoint *p in dataPoints) {
horizontalPos = ( [p pDate] - [self minValueOnHorizontalAxis]) * [self horizontalScaleFactor] + [self originHorizontalOffset];
verticalPos = ([p parameterValue] - [self minValueOnVerticalAxis]) * [self verticalScaleFactor] ;
verticalPos = [self scrollViewHeight] - verticalPos - [self originVerticalOffset];
// Adjust points' location based on size
verticalPos -= POINT_HEIGHT/2;
horizontalPos -= POINT_WIDTH/2;
CGPoint point = CGPointMake(horizontalPos, verticalPos);
[dataPointCoordinates addObject:[NSValue valueWithCGPoint:point]];
}
/* to retrieve the value of the point:
CGPoint b = [(NSValue *)[array objectAtIndex:0] CGPointValue];
*/
}
-(void)drawRect:(CGRect)rect
{
[self setCtx: UIGraphicsGetCurrentContext()];
[self setTransform:CGAffineTransformConcat(CGContextGetTextMatrix([self ctx]), CGAffineTransformMake(1.0, 0.0, 0.0, -1.0, 0.0, 0.0))];
[self drawAxes];
[self graphPoints:[self pointsToGraph]];
CGPoint here = CGPointMake([self originHorizontalOffset] + [self horizontalAxisLength] + 40, [self topBuffer]);
legend = [[Legend alloc] initWithContext:[self ctx] withTransformation:[self transform] atPoint:here];
[legend drawBackground];
[legend drawLabels];
[legend setMinValueOnHorizontalAxis:[self minValueOnHorizontalAxis]];
[legend setMinValueOnVerticalAxis:[self minValueOnVerticalAxis]];
[legend setHorizontalScaleFactor:[self horizontalScaleFactor]];
[legend setVerticalScaleFactor:[self verticalScaleFactor]];
[legend setOriginHorizontalOffset:[self originHorizontalOffset]];
[legend setOriginVerticalOffset:[self originVerticalOffset]];
[legend setScrollViewHeight:[self scrollViewHeight]];
[legend setSince:[[[MetasomeDataPointStore sharedStore] since] timeIntervalSince1970]];
[legend setTopBuffer:topBuffer];
[legend setVerticalAxisLength:verticalAxisLength];
if ( [[[[MetasomeDataPointStore sharedStore] parameterToGraph] parameterName] isEqualToString:@"Blood pressure"] )
[legend connectTheBloodPressureDots:[self pointsToGraph]];
else
[legend connectTheDots:[self pointsToGraph]];
parentGraphViewController = nil;
}
-(void)graphPoints:(NSArray *)array
{
[self setScale:pointsToGraph];
CGRect drawInMe;
float radius = 1.0;
float red, green, blue;
CGContextSetLineWidth([self ctx], radius);
CGContextSetRGBStrokeColor([self ctx], 1.0, 0.0, 0.0, 0.5);
//CGContextSetRGBFillColor([self ctx], 1.0, 0.0, 0.0, 0.50);
float horizontalPos, verticalPos;
for (MetasomeDataPoint *p in array) {
red = [p red];
green = [p green];
blue = [p blue];
CGContextSetRGBFillColor([self ctx], red, green, blue, 0.7);
horizontalPos = ( [p pDate] - [self minValueOnHorizontalAxis]) * [self horizontalScaleFactor] + [self originHorizontalOffset];
verticalPos = ([p parameterValue] - [self minValueOnVerticalAxis]) * [self verticalScaleFactor] ;
verticalPos = [self scrollViewHeight] - verticalPos - [self originVerticalOffset];
// Adjust points' location based on size
verticalPos -= POINT_HEIGHT/2;
horizontalPos -= POINT_WIDTH/2;
drawInMe = CGRectMake(horizontalPos, verticalPos, POINT_WIDTH, POINT_HEIGHT);
CGContextAddEllipseInRect([self ctx], drawInMe);
CGContextFillPath([self ctx]);
}
}
-(void)generateAxisLabels
{
}
-(void)drawAxes
{
// Draw horizontal axis line
CGPoint begin = CGPointMake([self originHorizontalOffset], [self scrollViewHeight] - [self originVerticalOffset]);
CGPoint end = CGPointMake([self scrollViewWidth] - [self rightBuffer] , begin.y);
// Draw horizontal axis line
CGContextSetLineWidth([self ctx], 3.0);
CGContextSetLineCap([self ctx], kCGLineCapRound);
CGContextSetRGBStrokeColor([self ctx], 0, 0, 0, 1);
CGContextMoveToPoint([self ctx], begin.x, begin.y);
CGContextAddLineToPoint([self ctx], end.x, end.y);
begin = CGPointMake([self originHorizontalOffset], [self scrollViewHeight] - [self originVerticalOffset]);
end = CGPointMake([self originHorizontalOffset], [self topBuffer]);
//CGPoints used for drawing grid lines
CGPoint beginGrid, endGrid;
//Step size while drawing axes
float horizontalStep = [self horizontalAxisLength] / HORIZONTAL_NUMBER_OF_INTERVALS;
float verticalStep = [self verticalAxisLength] / VERTICAL_NUMBER_OF_INTERVALS;
float valueStep = ([self maxValueOnVerticalAxis] - [self minValueOnVerticalAxis]) / VERTICAL_NUMBER_OF_INTERVALS;
float dateStep = ([self maxValueOnHorizontalAxis] - [self minValueOnHorizontalAxis]) / HORIZONTAL_NUMBER_OF_INTERVALS;
//Draw axes and change line width before drawing grid lines
CGContextStrokePath([self ctx]);
CGContextSetLineWidth([self ctx], 0.5);
CGContextSetRGBStrokeColor([self ctx], 0, 0, 0, 0.5);
CGContextSetRGBFillColor([self ctx], 1.0, 0, 0, 0.2);
float dateCounter = 1;
float horizontalValue = [self minValueOnHorizontalAxis];
float x;
// Draw vertical grid lines
for (
x = [self originHorizontalOffset] + horizontalStep; x < ( [self scrollViewWidth] - [self rightBuffer] ); x += horizontalStep )
{
beginGrid = CGPointMake(x, [self scrollViewHeight] - [self originVerticalOffset]);
endGrid = CGPointMake(x, [self topBuffer]);
CGContextMoveToPoint([self ctx], beginGrid.x, beginGrid.y);
CGContextAddLineToPoint([self ctx], endGrid.x, endGrid.y);
dateCounter +=1;
}
// Draw horizontal grid lines
float valueCounter = 1;
float verticalValue = [self minValueOnVerticalAxis];
for (float y = [self scrollViewHeight] - [self originVerticalOffset] - verticalStep; y > [self topBuffer]; y -= verticalStep)
{
beginGrid = CGPointMake( [self originHorizontalOffset], y );
endGrid = CGPointMake( [self scrollViewWidth] - [self rightBuffer], y);
CGContextMoveToPoint([self ctx], beginGrid.x, beginGrid.y);
CGContextAddLineToPoint([self ctx], endGrid.x, endGrid.y);
verticalValue = [self minValueOnVerticalAxis] + (valueCounter * valueStep);
valueCounter += 1;
}
CGContextStrokePath([self ctx]);
}
-(void)willRemoveSubview:(UIView *)subview
{
[super removeFromSuperview];
}
-(BOOL)canBecomeFirstResponder
{
return YES;
}
-(void)hideMenu
{
//UIAlertView *av = [UIAlertView alloc] initWithTitle:@"Warning" message:@" delegate:<#(id)#> cancelButtonTitle:<#(NSString *)#> otherButtonTitles:<#(NSString *), ...#>, nil
}
-(void)setScale:(NSArray *)array
{
[self setMaxValueOnHorizontalAxis:[[NSDate date] timeIntervalSince1970]];
//minValueOnHorizontalAxis set via NSDate *since, which is set in GraphViewController
[self setMinValueOnHorizontalAxis:[[[MetasomeDataPointStore sharedStore] since] timeIntervalSince1970]];
[self setMinValueOnVerticalAxis:1000.0];
[self setMaxValueOnVerticalAxis:0];
for (MetasomeDataPoint *p in array) {
//Looking only for points of interest
if ( [ [p parameterName] isEqualToString:[[MetasomeDataPointStore sharedStore] toGraph] ] ) {
if ([p parameterValue] > [self maxValueOnVerticalAxis]) {
[self setMaxValueOnVerticalAxis:[p parameterValue]];
}
if ([p parameterValue] < [self minValueOnVerticalAxis]) {
[self setMinValueOnVerticalAxis:[p parameterValue]];
}
}
}
// In case of only one data point...
if ( [self minValueOnVerticalAxis] == [self maxValueOnVerticalAxis])
[self setMinValueOnVerticalAxis:0];
// Add slack to the vertical and horizontal axes -- only if the input is not from a slider
if (inputType != ParameterInputSlider) {
maxValueOnVerticalAxis += 0.2*(maxValueOnVerticalAxis - minValueOnVerticalAxis);
minValueOnVerticalAxis -= 0.3*(maxValueOnVerticalAxis - minValueOnVerticalAxis);
}
//if it is slider input, set minimum value to zero
else {
minValueOnVerticalAxis = 0.0;
maxValueOnVerticalAxis = 100.0;
}
// for rescue inhaler puffs, set maximum to 10
if ([[[[MetasomeDataPointStore sharedStore] parameterToGraph] parameterName] isEqualToString:@"Rescue inhaler puffs"]) {
maxValueOnVerticalAxis = 10;
}
if (minValueOnVerticalAxis < 0)
minValueOnVerticalAxis = 0;
CGPoint begin = CGPointMake([self originHorizontalOffset], [self scrollViewHeight] - [self originVerticalOffset]);
CGPoint end = CGPointMake([self scrollViewWidth] - [self rightBuffer] , begin.y);
[self setHorizontalAxisLength:abs(end.x - begin.x)];
end = CGPointMake([self originHorizontalOffset], [self topBuffer]);
[self setVerticalAxisLength:(begin.y - end.y)];
[self setHorizontalScaleFactor:([self horizontalAxisLength] / ([self maxValueOnHorizontalAxis] - [self minValueOnHorizontalAxis])) ];
[self setVerticalScaleFactor:([self verticalAxisLength] / ([self maxValueOnVerticalAxis] - [self minValueOnVerticalAxis])) ];
}
@end