|
|
|
|
@@ -14,13 +14,9 @@
|
|
|
|
|
*********************/
|
|
|
|
|
#define MY_CLASS &lv_chart_class
|
|
|
|
|
|
|
|
|
|
#define LV_CHART_YMIN_DEF 0
|
|
|
|
|
#define LV_CHART_YMAX_DEF 100
|
|
|
|
|
#define LV_CHART_HDIV_DEF 3
|
|
|
|
|
#define LV_CHART_VDIV_DEF 5
|
|
|
|
|
#define LV_CHART_POINT_CNT_DEF 10
|
|
|
|
|
#define LV_CHART_LABEL_ITERATOR_FORWARD 1
|
|
|
|
|
#define LV_CHART_LABEL_ITERATOR_REVERSE 0
|
|
|
|
|
|
|
|
|
|
/**********************
|
|
|
|
|
* TYPEDEFS
|
|
|
|
|
@@ -36,10 +32,12 @@ static void lv_chart_event(const lv_obj_class_t * class_p, lv_event_t * e);
|
|
|
|
|
static void draw_div_lines(lv_obj_t * obj , const lv_area_t * mask);
|
|
|
|
|
static void draw_series_line(lv_obj_t * obj, const lv_area_t * clip_area);
|
|
|
|
|
static void draw_series_bar(lv_obj_t * obj, const lv_area_t * clip_area);
|
|
|
|
|
static void draw_series_scatter(lv_obj_t * obj, const lv_area_t * clip_area);
|
|
|
|
|
static void draw_cursors(lv_obj_t * obj, const lv_area_t * clip_area);
|
|
|
|
|
static void draw_axes(lv_obj_t * obj, const lv_area_t * mask);
|
|
|
|
|
static uint32_t get_index_from_x(lv_obj_t * obj, lv_coord_t x);
|
|
|
|
|
static void invalidate_point(lv_obj_t * obj, uint16_t i);
|
|
|
|
|
static void new_points_alloc(lv_obj_t * obj, lv_chart_series_t * ser, uint32_t cnt, lv_coord_t ** a);
|
|
|
|
|
|
|
|
|
|
/**********************
|
|
|
|
|
* STATIC VARIABLES
|
|
|
|
|
@@ -77,6 +75,23 @@ void lv_chart_set_type(lv_obj_t * obj, lv_chart_type_t type)
|
|
|
|
|
lv_chart_t * chart = (lv_chart_t *)obj;
|
|
|
|
|
if(chart->type == type) return;
|
|
|
|
|
|
|
|
|
|
if(chart->type == LV_CHART_TYPE_SCATTER) {
|
|
|
|
|
lv_chart_series_t * ser;
|
|
|
|
|
_LV_LL_READ_BACK(&chart->series_ll, ser) {
|
|
|
|
|
lv_mem_free(ser->x_points);
|
|
|
|
|
ser->x_points = NULL;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(type == LV_CHART_TYPE_SCATTER) {
|
|
|
|
|
lv_chart_series_t * ser;
|
|
|
|
|
_LV_LL_READ_BACK(&chart->series_ll, ser) {
|
|
|
|
|
ser->x_points = lv_mem_alloc(sizeof(lv_point_t) * chart->point_cnt);
|
|
|
|
|
LV_ASSERT_MALLOC(ser->x_points);
|
|
|
|
|
if(ser->x_points == NULL) return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
chart->type = type;
|
|
|
|
|
|
|
|
|
|
lv_chart_refresh(obj);
|
|
|
|
|
@@ -90,51 +105,12 @@ void lv_chart_set_point_count(lv_obj_t * obj, uint16_t cnt)
|
|
|
|
|
if(chart->point_cnt == cnt) return;
|
|
|
|
|
|
|
|
|
|
lv_chart_series_t * ser;
|
|
|
|
|
uint16_t point_cnt_old = chart->point_cnt;
|
|
|
|
|
uint16_t i;
|
|
|
|
|
lv_coord_t def = LV_CHART_POINT_NONE;
|
|
|
|
|
|
|
|
|
|
if(cnt < 1) cnt = 1;
|
|
|
|
|
|
|
|
|
|
_LV_LL_READ_BACK(&chart->series_ll, ser) {
|
|
|
|
|
if(!ser->ext_buf_assigned) {
|
|
|
|
|
if(ser->last_point != 0) {
|
|
|
|
|
lv_coord_t * new_points = lv_mem_alloc(sizeof(lv_coord_t) * cnt);
|
|
|
|
|
LV_ASSERT_MALLOC(new_points);
|
|
|
|
|
if(new_points == NULL) return;
|
|
|
|
|
|
|
|
|
|
if(cnt >= point_cnt_old) {
|
|
|
|
|
for(i = 0; i < point_cnt_old; i++) {
|
|
|
|
|
new_points[i] =
|
|
|
|
|
ser->points[(i + ser->last_point) % point_cnt_old]; /*Copy old contents to new array*/
|
|
|
|
|
}
|
|
|
|
|
for(i = point_cnt_old; i < cnt; i++) {
|
|
|
|
|
new_points[i] = def; /*Fill up the rest with default value*/
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
for(i = 0; i < cnt; i++) {
|
|
|
|
|
new_points[i] =
|
|
|
|
|
ser->points[(i + ser->last_point) % point_cnt_old]; /*Copy old contents to new array*/
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*Switch over pointer from old to new*/
|
|
|
|
|
lv_mem_free(ser->points);
|
|
|
|
|
ser->points = new_points;
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
ser->points = lv_mem_realloc(ser->points, sizeof(lv_coord_t) * cnt);
|
|
|
|
|
LV_ASSERT_MALLOC(ser->points);
|
|
|
|
|
if(ser->points == NULL) return;
|
|
|
|
|
/*Initialize the new points*/
|
|
|
|
|
if(cnt > point_cnt_old) {
|
|
|
|
|
for(i = point_cnt_old - 1; i < cnt; i++) {
|
|
|
|
|
ser->points[i] = def;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if(!ser->x_ext_buf_assigned) new_points_alloc(obj, ser, cnt, &ser->x_points);
|
|
|
|
|
if(!ser->y_ext_buf_assigned) new_points_alloc(obj, ser, cnt, &ser->y_points);
|
|
|
|
|
ser->last_point = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -147,14 +123,31 @@ void lv_chart_set_range(lv_obj_t * obj, lv_chart_axis_t axis, lv_coord_t min, lv
|
|
|
|
|
{
|
|
|
|
|
LV_ASSERT_OBJ(obj, MY_CLASS);
|
|
|
|
|
|
|
|
|
|
if(axis != LV_CHART_AXIS_PRIMARY_Y && axis != LV_CHART_AXIS_SECONDARY_Y) {
|
|
|
|
|
max = max == min ? max + 1 : max;
|
|
|
|
|
|
|
|
|
|
lv_chart_t * chart = (lv_chart_t *)obj;
|
|
|
|
|
switch(axis) {
|
|
|
|
|
case LV_CHART_AXIS_PRIMARY_Y:
|
|
|
|
|
chart->ymin[0] = min;
|
|
|
|
|
chart->ymax[0] = max;
|
|
|
|
|
break;
|
|
|
|
|
case LV_CHART_AXIS_SECONDARY_Y:
|
|
|
|
|
chart->ymin[1] = min;
|
|
|
|
|
chart->ymax[1] = max;
|
|
|
|
|
break;
|
|
|
|
|
case LV_CHART_AXIS_PRIMARY_X:
|
|
|
|
|
chart->xmin[0] = min;
|
|
|
|
|
chart->xmax[0] = max;
|
|
|
|
|
break;
|
|
|
|
|
case LV_CHART_AXIS_SECONDARY_X:
|
|
|
|
|
chart->xmin[1] = min;
|
|
|
|
|
chart->xmax[1] = max;
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
LV_LOG_WARN("Invalid axis: %d", axis);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
lv_chart_t * chart = (lv_chart_t *)obj;
|
|
|
|
|
chart->ymin[axis] = min;
|
|
|
|
|
chart->ymax[axis] = (max == min ? max + 1 : max);
|
|
|
|
|
lv_chart_refresh(obj);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -304,8 +297,8 @@ void lv_chart_get_point_pos_by_id(lv_obj_t * obj, lv_chart_series_t * ser, uint1
|
|
|
|
|
|
|
|
|
|
lv_coord_t h = (lv_obj_get_content_height(obj) * chart->zoom_y) >> 8;
|
|
|
|
|
|
|
|
|
|
p_out->y = (int32_t)((int32_t)ser->points[id] - chart->ymin[ser->y_axis]) * h;
|
|
|
|
|
p_out->y = p_out->y / (chart->ymax[ser->y_axis] - chart->ymin[ser->y_axis]);
|
|
|
|
|
p_out->y = (int32_t)((int32_t)ser->y_points[id] - chart->ymin[ser->y_axis_sec]) * h;
|
|
|
|
|
p_out->y = p_out->y / (chart->ymax[ser->y_axis_sec] - chart->ymin[ser->y_axis_sec]);
|
|
|
|
|
p_out->y = h - p_out->y;
|
|
|
|
|
p_out->y += lv_obj_get_style_pad_top(obj, LV_PART_MAIN);
|
|
|
|
|
|
|
|
|
|
@@ -328,11 +321,6 @@ lv_chart_series_t * lv_chart_add_series(lv_obj_t * obj, lv_color_t color, lv_cha
|
|
|
|
|
|
|
|
|
|
LV_ASSERT_OBJ(obj, MY_CLASS);
|
|
|
|
|
|
|
|
|
|
if(axis != LV_CHART_AXIS_PRIMARY_Y && axis != LV_CHART_AXIS_SECONDARY_Y) {
|
|
|
|
|
LV_LOG_WARN("Invalid y axis");
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
lv_chart_t * chart = (lv_chart_t *)obj;
|
|
|
|
|
lv_chart_series_t * ser = _lv_ll_ins_head(&chart->series_ll);
|
|
|
|
|
LV_ASSERT_MALLOC(ser);
|
|
|
|
|
@@ -341,21 +329,27 @@ lv_chart_series_t * lv_chart_add_series(lv_obj_t * obj, lv_color_t color, lv_cha
|
|
|
|
|
lv_coord_t def = LV_CHART_POINT_NONE;
|
|
|
|
|
|
|
|
|
|
ser->color = color;
|
|
|
|
|
ser->points = lv_mem_alloc(sizeof(lv_coord_t) * chart->point_cnt);
|
|
|
|
|
LV_ASSERT_MALLOC(ser->points);
|
|
|
|
|
if(ser->points == NULL) {
|
|
|
|
|
ser->y_points = lv_mem_alloc(sizeof(lv_coord_t) * chart->point_cnt);
|
|
|
|
|
LV_ASSERT_MALLOC(ser->y_points);
|
|
|
|
|
|
|
|
|
|
if(chart->type == LV_CHART_TYPE_SCATTER) {
|
|
|
|
|
ser->x_points = lv_mem_alloc(sizeof(lv_coord_t) * chart->point_cnt);
|
|
|
|
|
LV_ASSERT_MALLOC(ser->x_points);
|
|
|
|
|
}
|
|
|
|
|
if(ser->y_points == NULL) {
|
|
|
|
|
_lv_ll_remove(&chart->series_ll, ser);
|
|
|
|
|
lv_mem_free(ser);
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ser->last_point = 0;
|
|
|
|
|
ser->ext_buf_assigned = false;
|
|
|
|
|
ser->y_ext_buf_assigned = false;
|
|
|
|
|
ser->hidden = 0;
|
|
|
|
|
ser->y_axis = axis;
|
|
|
|
|
ser->x_axis_sec = axis & LV_CHART_AXIS_SECONDARY_X ? 1 : 0;
|
|
|
|
|
ser->y_axis_sec = axis & LV_CHART_AXIS_SECONDARY_Y ? 1 : 0;
|
|
|
|
|
|
|
|
|
|
uint16_t i;
|
|
|
|
|
lv_coord_t * p_tmp = ser->points;
|
|
|
|
|
lv_coord_t * p_tmp = ser->y_points;
|
|
|
|
|
for(i = 0; i < chart->point_cnt; i++) {
|
|
|
|
|
*p_tmp = def;
|
|
|
|
|
p_tmp++;
|
|
|
|
|
@@ -370,7 +364,7 @@ void lv_chart_remove_series(lv_obj_t * obj, lv_chart_series_t * series)
|
|
|
|
|
LV_ASSERT_NULL(series);
|
|
|
|
|
|
|
|
|
|
lv_chart_t * chart = (lv_chart_t *)obj;
|
|
|
|
|
if(!series->ext_buf_assigned && series->points) lv_mem_free(series->points);
|
|
|
|
|
if(!series->y_ext_buf_assigned && series->y_points) lv_mem_free(series->y_points);
|
|
|
|
|
|
|
|
|
|
_lv_ll_remove(&chart->series_ll, series);
|
|
|
|
|
lv_mem_free(series);
|
|
|
|
|
@@ -443,7 +437,6 @@ lv_chart_cursor_t * lv_chart_add_cursor(lv_obj_t * obj, lv_color_t color, lv_di
|
|
|
|
|
cursor->color = color;
|
|
|
|
|
cursor->dir = dir;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return cursor;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -512,7 +505,7 @@ void lv_chart_set_all_value(lv_obj_t * obj, lv_chart_series_t * ser, lv_coord_t
|
|
|
|
|
lv_chart_t * chart = (lv_chart_t *)obj;
|
|
|
|
|
uint16_t i;
|
|
|
|
|
for(i = 0; i < chart->point_cnt; i++) {
|
|
|
|
|
ser->points[i] = value;
|
|
|
|
|
ser->y_points[i] = value;
|
|
|
|
|
}
|
|
|
|
|
ser->last_point = 0;
|
|
|
|
|
lv_chart_refresh(obj);
|
|
|
|
|
@@ -525,13 +518,47 @@ void lv_chart_set_next_value(lv_obj_t * obj, lv_chart_series_t * ser, lv_coord_t
|
|
|
|
|
|
|
|
|
|
lv_chart_t * chart = (lv_chart_t *)obj;
|
|
|
|
|
if(chart->update_mode == LV_CHART_UPDATE_MODE_SHIFT) {
|
|
|
|
|
ser->points[ser->last_point] =
|
|
|
|
|
ser->y_points[ser->last_point] =
|
|
|
|
|
value; /*This was the place of the former left most value, after shifting it is the rightmost*/
|
|
|
|
|
ser->last_point = (ser->last_point + 1) % chart->point_cnt;
|
|
|
|
|
lv_chart_refresh(obj);
|
|
|
|
|
}
|
|
|
|
|
else if(chart->update_mode == LV_CHART_UPDATE_MODE_CIRCULAR) {
|
|
|
|
|
ser->points[ser->last_point] = value;
|
|
|
|
|
ser->y_points[ser->last_point] = value;
|
|
|
|
|
|
|
|
|
|
invalidate_point(obj, ser->last_point);
|
|
|
|
|
|
|
|
|
|
uint32_t next_point = (ser->last_point + 1) % chart->point_cnt; /*update the x for next incoming y*/
|
|
|
|
|
ser->last_point = next_point;
|
|
|
|
|
invalidate_point(obj, next_point);
|
|
|
|
|
|
|
|
|
|
lv_obj_invalidate(obj);
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void lv_chart_set_next_value2(lv_obj_t * obj, lv_chart_series_t * ser, lv_coord_t x_value, lv_coord_t y_value)
|
|
|
|
|
{
|
|
|
|
|
LV_ASSERT_OBJ(obj, MY_CLASS);
|
|
|
|
|
LV_ASSERT_NULL(ser);
|
|
|
|
|
|
|
|
|
|
lv_chart_t * chart = (lv_chart_t *)obj;
|
|
|
|
|
|
|
|
|
|
if(chart->type != LV_CHART_TYPE_SCATTER) {
|
|
|
|
|
LV_LOG_WARN("Type must be LV_CHART_TYPE_SCATTER");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(chart->update_mode == LV_CHART_UPDATE_MODE_SHIFT) {
|
|
|
|
|
/*This was the place of the former left most value, after shifting it is the rightmost*/
|
|
|
|
|
ser->x_points[ser->last_point] = x_value;
|
|
|
|
|
ser->y_points[ser->last_point] = y_value;
|
|
|
|
|
ser->last_point = (ser->last_point + 1) % chart->point_cnt;
|
|
|
|
|
lv_chart_refresh(obj);
|
|
|
|
|
}
|
|
|
|
|
else if(chart->update_mode == LV_CHART_UPDATE_MODE_CIRCULAR) {
|
|
|
|
|
ser->x_points[ser->last_point] = x_value;
|
|
|
|
|
ser->y_points[ser->last_point] = y_value;
|
|
|
|
|
|
|
|
|
|
invalidate_point(obj, ser->last_point);
|
|
|
|
|
|
|
|
|
|
@@ -551,26 +578,61 @@ void lv_chart_set_value_by_id(lv_obj_t * obj, lv_chart_series_t * ser, lv_coord_
|
|
|
|
|
lv_chart_t * chart = (lv_chart_t *)obj;
|
|
|
|
|
|
|
|
|
|
if(id >= chart->point_cnt) return;
|
|
|
|
|
ser->points[id] = value;
|
|
|
|
|
ser->y_points[id] = value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void lv_chart_set_ext_array(lv_obj_t * obj, lv_chart_series_t * ser, lv_coord_t array[])
|
|
|
|
|
void lv_chart_set_value_by_id2(lv_obj_t * obj, lv_chart_series_t * ser, uint16_t id, lv_coord_t x_value, lv_coord_t y_value)
|
|
|
|
|
{
|
|
|
|
|
LV_ASSERT_OBJ(obj, MY_CLASS);
|
|
|
|
|
LV_ASSERT_NULL(ser);
|
|
|
|
|
lv_chart_t * chart = (lv_chart_t *)obj;
|
|
|
|
|
|
|
|
|
|
if(chart->type != LV_CHART_TYPE_SCATTER) {
|
|
|
|
|
LV_LOG_WARN("Type must be LV_CHART_TYPE_SCATTER");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(id >= chart->point_cnt) return;
|
|
|
|
|
ser->x_points[id] = x_value;
|
|
|
|
|
ser->y_points[id] = y_value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void lv_chart_set_ext_y_array(lv_obj_t * obj, lv_chart_series_t * ser, lv_coord_t array[])
|
|
|
|
|
{
|
|
|
|
|
LV_ASSERT_OBJ(obj, MY_CLASS);
|
|
|
|
|
LV_ASSERT_NULL(ser);
|
|
|
|
|
|
|
|
|
|
if(!ser->ext_buf_assigned && ser->points) lv_mem_free(ser->points);
|
|
|
|
|
ser->ext_buf_assigned = true;
|
|
|
|
|
ser->points = array;
|
|
|
|
|
if(!ser->y_ext_buf_assigned && ser->y_points) lv_mem_free(ser->y_points);
|
|
|
|
|
ser->y_ext_buf_assigned = true;
|
|
|
|
|
ser->y_points = array;
|
|
|
|
|
lv_obj_invalidate(obj);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
lv_coord_t * lv_chart_get_array(const lv_obj_t * obj, lv_chart_series_t * ser)
|
|
|
|
|
void lv_chart_set_ext_x_array(lv_obj_t * obj, lv_chart_series_t * ser, lv_coord_t array[])
|
|
|
|
|
{
|
|
|
|
|
LV_ASSERT_OBJ(obj, MY_CLASS);
|
|
|
|
|
LV_ASSERT_NULL(ser);
|
|
|
|
|
|
|
|
|
|
if(!ser->x_ext_buf_assigned && ser->x_points) lv_mem_free(ser->x_points);
|
|
|
|
|
ser->x_ext_buf_assigned = true;
|
|
|
|
|
ser->x_points = array;
|
|
|
|
|
lv_obj_invalidate(obj);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
lv_coord_t * lv_chart_get_y_array(const lv_obj_t * obj, lv_chart_series_t * ser)
|
|
|
|
|
{
|
|
|
|
|
LV_UNUSED(obj);
|
|
|
|
|
LV_ASSERT_OBJ(obj, MY_CLASS);
|
|
|
|
|
LV_ASSERT_NULL(ser);
|
|
|
|
|
return ser->points;
|
|
|
|
|
return ser->y_points;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
lv_coord_t * lv_chart_get_x_array(const lv_obj_t * obj, lv_chart_series_t * ser)
|
|
|
|
|
{
|
|
|
|
|
LV_UNUSED(obj);
|
|
|
|
|
LV_ASSERT_OBJ(obj, MY_CLASS);
|
|
|
|
|
LV_ASSERT_NULL(ser);
|
|
|
|
|
return ser->x_points;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
uint32_t lv_chart_get_pressed_point(const lv_obj_t * obj)
|
|
|
|
|
@@ -593,10 +655,14 @@ static void lv_chart_constructor(const lv_obj_class_t * class_p, lv_obj_t * obj)
|
|
|
|
|
_lv_ll_init(&chart->series_ll, sizeof(lv_chart_series_t));
|
|
|
|
|
_lv_ll_init(&chart->cursor_ll, sizeof(lv_chart_cursor_t));
|
|
|
|
|
|
|
|
|
|
chart->ymin[LV_CHART_AXIS_PRIMARY_Y] = LV_CHART_YMIN_DEF;
|
|
|
|
|
chart->ymax[LV_CHART_AXIS_PRIMARY_Y] = LV_CHART_YMAX_DEF;
|
|
|
|
|
chart->ymin[LV_CHART_AXIS_SECONDARY_Y] = LV_CHART_YMIN_DEF;
|
|
|
|
|
chart->ymax[LV_CHART_AXIS_SECONDARY_Y] = LV_CHART_YMAX_DEF;
|
|
|
|
|
chart->ymin[0] = 0;
|
|
|
|
|
chart->xmin[0] = 0;
|
|
|
|
|
chart->ymin[1] = 0;
|
|
|
|
|
chart->xmin[1] = 0;
|
|
|
|
|
chart->ymax[0] = 100;
|
|
|
|
|
chart->xmax[0] = 100;
|
|
|
|
|
chart->ymax[1] = 100;
|
|
|
|
|
chart->xmax[1] = 100;
|
|
|
|
|
|
|
|
|
|
chart->hdiv_cnt = LV_CHART_HDIV_DEF;
|
|
|
|
|
chart->vdiv_cnt = LV_CHART_VDIV_DEF;
|
|
|
|
|
@@ -620,7 +686,7 @@ static void lv_chart_destructor(const lv_obj_class_t * class_p, lv_obj_t * obj)
|
|
|
|
|
while(chart->series_ll.head) {
|
|
|
|
|
ser = _lv_ll_get_head(&chart->series_ll);
|
|
|
|
|
|
|
|
|
|
if(!ser->ext_buf_assigned) lv_mem_free(ser->points);
|
|
|
|
|
if(!ser->y_ext_buf_assigned) lv_mem_free(ser->y_points);
|
|
|
|
|
|
|
|
|
|
_lv_ll_remove(&chart->series_ll, ser);
|
|
|
|
|
lv_mem_free(ser);
|
|
|
|
|
@@ -664,7 +730,7 @@ static void lv_chart_event(const lv_obj_class_t * class_p, lv_event_t * e)
|
|
|
|
|
lv_obj_refresh_self_size(obj);
|
|
|
|
|
} else if(code == LV_EVENT_REFR_EXT_DRAW_SIZE) {
|
|
|
|
|
lv_coord_t * s = lv_event_get_param(e);
|
|
|
|
|
*s = LV_MAX4(*s, chart->tick[LV_CHART_AXIS_X].draw_size,
|
|
|
|
|
*s = LV_MAX4(*s, chart->tick[LV_CHART_AXIS_PRIMARY_X].draw_size,
|
|
|
|
|
chart->tick[LV_CHART_AXIS_PRIMARY_Y].draw_size, chart->tick[LV_CHART_AXIS_SECONDARY_Y].draw_size);
|
|
|
|
|
} else if(code == LV_EVENT_GET_SELF_SIZE) {
|
|
|
|
|
lv_point_t * p = lv_event_get_param(e);
|
|
|
|
|
@@ -675,8 +741,9 @@ static void lv_chart_event(const lv_obj_class_t * class_p, lv_event_t * e)
|
|
|
|
|
draw_div_lines(obj, clip_area);
|
|
|
|
|
draw_axes(obj, clip_area);
|
|
|
|
|
|
|
|
|
|
if(chart->type & LV_CHART_TYPE_LINE) draw_series_line(obj, clip_area);
|
|
|
|
|
else if(chart->type & LV_CHART_TYPE_BAR) draw_series_bar(obj, clip_area);
|
|
|
|
|
if(chart->type == LV_CHART_TYPE_LINE) draw_series_line(obj, clip_area);
|
|
|
|
|
else if(chart->type == LV_CHART_TYPE_BAR) draw_series_bar(obj, clip_area);
|
|
|
|
|
else if(chart->type == LV_CHART_TYPE_SCATTER) draw_series_scatter(obj, clip_area);
|
|
|
|
|
|
|
|
|
|
draw_cursors(obj, clip_area);
|
|
|
|
|
}
|
|
|
|
|
@@ -823,8 +890,8 @@ static void draw_series_line(lv_obj_t * obj, const lv_area_t * clip_area)
|
|
|
|
|
|
|
|
|
|
lv_coord_t p_act = start_point;
|
|
|
|
|
lv_coord_t p_prev = start_point;
|
|
|
|
|
int32_t y_tmp = (int32_t)((int32_t)ser->points[p_prev] - chart->ymin[ser->y_axis]) * h;
|
|
|
|
|
y_tmp = y_tmp / (chart->ymax[ser->y_axis] - chart->ymin[ser->y_axis]);
|
|
|
|
|
int32_t y_tmp = (int32_t)((int32_t)ser->y_points[p_prev] - chart->ymin[ser->y_axis_sec]) * h;
|
|
|
|
|
y_tmp = y_tmp / (chart->ymax[ser->y_axis_sec] - chart->ymin[ser->y_axis_sec]);
|
|
|
|
|
p2.y = h - y_tmp + y_ofs;
|
|
|
|
|
|
|
|
|
|
lv_obj_draw_part_dsc_t dsc;
|
|
|
|
|
@@ -846,8 +913,8 @@ static void draw_series_line(lv_obj_t * obj, const lv_area_t * clip_area)
|
|
|
|
|
|
|
|
|
|
p_act = (start_point + i) % chart->point_cnt;
|
|
|
|
|
|
|
|
|
|
y_tmp = (int32_t)((int32_t)ser->points[p_act] - chart->ymin[ser->y_axis]) * h;
|
|
|
|
|
y_tmp = y_tmp / (chart->ymax[ser->y_axis] - chart->ymin[ser->y_axis]);
|
|
|
|
|
y_tmp = (int32_t)((int32_t)ser->y_points[p_act] - chart->ymin[ser->y_axis_sec]) * h;
|
|
|
|
|
y_tmp = y_tmp / (chart->ymax[ser->y_axis_sec] - chart->ymin[ser->y_axis_sec]);
|
|
|
|
|
p2.y = h - y_tmp + y_ofs;
|
|
|
|
|
|
|
|
|
|
if(p2.x < clip_area->x1 - point_w - 1) {
|
|
|
|
|
@@ -858,7 +925,7 @@ static void draw_series_line(lv_obj_t * obj, const lv_area_t * clip_area)
|
|
|
|
|
/*Don't draw the first point. A second point is also required to draw the line*/
|
|
|
|
|
if(i != 0) {
|
|
|
|
|
if(crowded_mode) {
|
|
|
|
|
if(ser->points[p_prev] != LV_CHART_POINT_NONE && ser->points[p_act] != LV_CHART_POINT_NONE) {
|
|
|
|
|
if(ser->y_points[p_prev] != LV_CHART_POINT_NONE && ser->y_points[p_act] != LV_CHART_POINT_NONE) {
|
|
|
|
|
/*Draw only one vertical line between the min an max y values on the same x value*/
|
|
|
|
|
y_max = LV_MAX(y_max, p2.y);
|
|
|
|
|
y_min = LV_MIN(y_min, p2.y);
|
|
|
|
|
@@ -883,18 +950,18 @@ static void draw_series_line(lv_obj_t * obj, const lv_area_t * clip_area)
|
|
|
|
|
point_area.y2 = p1.y + point_h;
|
|
|
|
|
|
|
|
|
|
dsc.id = i - 1;
|
|
|
|
|
dsc.p1 = ser->points[p_prev] != LV_CHART_POINT_NONE ? &p1 : NULL;
|
|
|
|
|
dsc.p2 = ser->points[p_act] != LV_CHART_POINT_NONE ? &p2 : NULL;
|
|
|
|
|
dsc.p1 = ser->y_points[p_prev] != LV_CHART_POINT_NONE ? &p1 : NULL;
|
|
|
|
|
dsc.p2 = ser->y_points[p_act] != LV_CHART_POINT_NONE ? &p2 : NULL;
|
|
|
|
|
dsc.draw_area = &point_area;
|
|
|
|
|
dsc.value = ser->points[p_prev];
|
|
|
|
|
dsc.value = ser->y_points[p_prev];
|
|
|
|
|
|
|
|
|
|
lv_event_send(obj, LV_EVENT_DRAW_PART_BEGIN, &dsc);
|
|
|
|
|
|
|
|
|
|
if(ser->points[p_prev] != LV_CHART_POINT_NONE && ser->points[p_act] != LV_CHART_POINT_NONE) {
|
|
|
|
|
if(ser->y_points[p_prev] != LV_CHART_POINT_NONE && ser->y_points[p_act] != LV_CHART_POINT_NONE) {
|
|
|
|
|
lv_draw_line(&p1, &p2, &series_mask, &line_dsc_default);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(point_w && point_h && ser->points[p_act] != LV_CHART_POINT_NONE) {
|
|
|
|
|
if(point_w && point_h && ser->y_points[p_act] != LV_CHART_POINT_NONE) {
|
|
|
|
|
lv_draw_rect(&point_area, &series_mask, &point_dsc_default);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -908,7 +975,7 @@ static void draw_series_line(lv_obj_t * obj, const lv_area_t * clip_area)
|
|
|
|
|
/*Draw the last point*/
|
|
|
|
|
if(!crowded_mode && i == chart->point_cnt) {
|
|
|
|
|
|
|
|
|
|
if(ser->points[p_act] != LV_CHART_POINT_NONE) {
|
|
|
|
|
if(ser->y_points[p_act] != LV_CHART_POINT_NONE) {
|
|
|
|
|
lv_area_t point_area;
|
|
|
|
|
point_area.x1 = p2.x - point_w;
|
|
|
|
|
point_area.x2 = p2.x + point_w;
|
|
|
|
|
@@ -919,7 +986,145 @@ static void draw_series_line(lv_obj_t * obj, const lv_area_t * clip_area)
|
|
|
|
|
dsc.p1 = NULL;
|
|
|
|
|
dsc.p2 = NULL;
|
|
|
|
|
dsc.draw_area = &point_area;
|
|
|
|
|
dsc.value = ser->points[p_act];
|
|
|
|
|
dsc.value = ser->y_points[p_act];
|
|
|
|
|
lv_event_send(obj, LV_EVENT_DRAW_PART_BEGIN, &dsc);
|
|
|
|
|
lv_draw_rect(&point_area, &series_mask, &point_dsc_default);
|
|
|
|
|
lv_event_send(obj, LV_EVENT_DRAW_PART_END, &dsc);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void draw_series_scatter(lv_obj_t * obj, const lv_area_t * clip_area)
|
|
|
|
|
{
|
|
|
|
|
|
|
|
|
|
lv_area_t com_area;
|
|
|
|
|
if(_lv_area_intersect(&com_area, &obj->coords, clip_area) == false) return;
|
|
|
|
|
|
|
|
|
|
lv_chart_t * chart = (lv_chart_t *)obj;
|
|
|
|
|
|
|
|
|
|
uint16_t i;
|
|
|
|
|
lv_point_t p1;
|
|
|
|
|
lv_point_t p2;
|
|
|
|
|
lv_coord_t pad_left = lv_obj_get_style_pad_left(obj, LV_PART_MAIN);
|
|
|
|
|
lv_coord_t pad_top = lv_obj_get_style_pad_top(obj, LV_PART_MAIN);
|
|
|
|
|
lv_coord_t w = (lv_obj_get_content_width(obj) * chart->zoom_x) >> 8;
|
|
|
|
|
lv_coord_t h = (lv_obj_get_content_height(obj) * chart->zoom_y) >> 8;
|
|
|
|
|
lv_coord_t x_ofs = obj->coords.x1 + pad_left - lv_obj_get_scroll_left(obj);
|
|
|
|
|
lv_coord_t y_ofs = obj->coords.y1 + pad_top - lv_obj_get_scroll_top(obj);
|
|
|
|
|
lv_chart_series_t * ser;
|
|
|
|
|
|
|
|
|
|
lv_area_t series_mask;
|
|
|
|
|
bool mask_ret = _lv_area_intersect(&series_mask, &obj->coords, clip_area);
|
|
|
|
|
if(mask_ret == false) return;
|
|
|
|
|
|
|
|
|
|
lv_draw_line_dsc_t line_dsc_default;
|
|
|
|
|
lv_draw_line_dsc_init(&line_dsc_default);
|
|
|
|
|
lv_obj_init_draw_line_dsc(obj, LV_PART_ITEMS, &line_dsc_default);
|
|
|
|
|
|
|
|
|
|
lv_draw_rect_dsc_t point_dsc_default;
|
|
|
|
|
lv_draw_rect_dsc_init(&point_dsc_default);
|
|
|
|
|
lv_obj_init_draw_rect_dsc(obj, LV_PART_INDICATOR, &point_dsc_default);
|
|
|
|
|
|
|
|
|
|
lv_coord_t point_w = lv_obj_get_style_width(obj, LV_PART_INDICATOR) / 2;
|
|
|
|
|
lv_coord_t point_h = lv_obj_get_style_height(obj, LV_PART_INDICATOR) / 2;
|
|
|
|
|
|
|
|
|
|
/*Do not bother with line ending is the point will over it*/
|
|
|
|
|
if(LV_MIN(point_w, point_h) > line_dsc_default.width / 2) line_dsc_default.raw_end = 1;
|
|
|
|
|
if(line_dsc_default.width == 1) line_dsc_default.raw_end = 1;
|
|
|
|
|
|
|
|
|
|
/*Go through all data lines*/
|
|
|
|
|
_LV_LL_READ_BACK(&chart->series_ll, ser) {
|
|
|
|
|
if (ser->hidden) continue;
|
|
|
|
|
line_dsc_default.color = ser->color;
|
|
|
|
|
point_dsc_default.bg_color = ser->color;
|
|
|
|
|
|
|
|
|
|
lv_coord_t start_point = chart->update_mode == LV_CHART_UPDATE_MODE_SHIFT ? ser->last_point : 0;
|
|
|
|
|
|
|
|
|
|
p1.x = x_ofs;
|
|
|
|
|
p2.x = x_ofs;
|
|
|
|
|
|
|
|
|
|
lv_coord_t p_act = start_point;
|
|
|
|
|
lv_coord_t p_prev = start_point;
|
|
|
|
|
if(ser->y_points[p_act] != LV_CHART_POINT_CNT_DEF) {
|
|
|
|
|
p2.x = lv_map(ser->x_points[p_act], chart->xmin[ser->x_axis_sec], chart->xmax[ser->x_axis_sec], 0, w);
|
|
|
|
|
p2.x += x_ofs;
|
|
|
|
|
|
|
|
|
|
p2.y = lv_map(ser->y_points[p_act], chart->ymin[ser->y_axis_sec], chart->ymax[ser->y_axis_sec], 0, h);
|
|
|
|
|
p2.y = h - p2.y;
|
|
|
|
|
p2.y += y_ofs;
|
|
|
|
|
} else {
|
|
|
|
|
p2.x = LV_COORD_MIN;
|
|
|
|
|
p2.y = LV_COORD_MIN;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
lv_obj_draw_part_dsc_t dsc;
|
|
|
|
|
lv_obj_draw_dsc_init(&dsc, clip_area);
|
|
|
|
|
dsc.part = LV_PART_ITEMS;
|
|
|
|
|
dsc.line_dsc = &line_dsc_default;
|
|
|
|
|
dsc.rect_dsc = &point_dsc_default;
|
|
|
|
|
dsc.sub_part_ptr = ser;
|
|
|
|
|
|
|
|
|
|
for(i = 0; i < chart->point_cnt; i++) {
|
|
|
|
|
p1.x = p2.x;
|
|
|
|
|
p1.y = p2.y;
|
|
|
|
|
|
|
|
|
|
p_act = (start_point + i) % chart->point_cnt;
|
|
|
|
|
if(ser->y_points[p_act] != LV_CHART_POINT_NONE) {
|
|
|
|
|
p2.y = lv_map(ser->y_points[p_act], chart->ymin[ser->y_axis_sec], chart->ymax[ser->y_axis_sec], 0, h);
|
|
|
|
|
p2.y = h - p2.y;
|
|
|
|
|
p2.y += y_ofs;
|
|
|
|
|
|
|
|
|
|
p2.x = lv_map(ser->x_points[p_act], chart->xmin[ser->x_axis_sec], chart->xmax[ser->x_axis_sec], 0, w);
|
|
|
|
|
p2.x += x_ofs;
|
|
|
|
|
} else {
|
|
|
|
|
p_prev = p_act;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*Don't draw the first point. A second point is also required to draw the line*/
|
|
|
|
|
if(i != 0) {
|
|
|
|
|
lv_area_t point_area;
|
|
|
|
|
point_area.x1 = p1.x - point_w;
|
|
|
|
|
point_area.x2 = p1.x + point_w;
|
|
|
|
|
point_area.y1 = p1.y - point_h;
|
|
|
|
|
point_area.y2 = p1.y + point_h;
|
|
|
|
|
|
|
|
|
|
dsc.id = i - 1;
|
|
|
|
|
dsc.p1 = ser->y_points[p_prev] != LV_CHART_POINT_NONE ? &p1 : NULL;
|
|
|
|
|
dsc.p2 = ser->y_points[p_act] != LV_CHART_POINT_NONE ? &p2 : NULL;
|
|
|
|
|
dsc.draw_area = &point_area;
|
|
|
|
|
dsc.value = ser->y_points[p_prev];
|
|
|
|
|
|
|
|
|
|
lv_event_send(obj, LV_EVENT_DRAW_PART_BEGIN, &dsc);
|
|
|
|
|
|
|
|
|
|
if(ser->y_points[p_prev] != LV_CHART_POINT_NONE && ser->y_points[p_act] != LV_CHART_POINT_NONE) {
|
|
|
|
|
lv_draw_line(&p1, &p2, &series_mask, &line_dsc_default);
|
|
|
|
|
if(point_w && point_h) {
|
|
|
|
|
lv_draw_rect(&point_area, &series_mask, &point_dsc_default);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
lv_event_send(obj, LV_EVENT_DRAW_PART_END, &dsc);
|
|
|
|
|
}
|
|
|
|
|
p_prev = p_act;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*Draw the last point*/
|
|
|
|
|
if(i == chart->point_cnt) {
|
|
|
|
|
|
|
|
|
|
if(ser->y_points[p_act] != LV_CHART_POINT_NONE) {
|
|
|
|
|
lv_area_t point_area;
|
|
|
|
|
point_area.x1 = p2.x - point_w;
|
|
|
|
|
point_area.x2 = p2.x + point_w;
|
|
|
|
|
point_area.y1 = p2.y - point_h;
|
|
|
|
|
point_area.y2 = p2.y + point_h;
|
|
|
|
|
|
|
|
|
|
dsc.id = i - 1;
|
|
|
|
|
dsc.p1 = NULL;
|
|
|
|
|
dsc.p2 = NULL;
|
|
|
|
|
dsc.draw_area = &point_area;
|
|
|
|
|
dsc.value = ser->y_points[p_act];
|
|
|
|
|
lv_event_send(obj, LV_EVENT_DRAW_PART_BEGIN, &dsc);
|
|
|
|
|
lv_draw_rect(&point_area, &series_mask, &point_dsc_default);
|
|
|
|
|
lv_event_send(obj, LV_EVENT_DRAW_PART_END, &dsc);
|
|
|
|
|
@@ -989,15 +1194,15 @@ static void draw_series_bar(lv_obj_t * obj, const lv_area_t * clip_area)
|
|
|
|
|
col_dsc.bg_color = ser->color;
|
|
|
|
|
|
|
|
|
|
lv_coord_t p_act = (start_point + i) % chart->point_cnt;
|
|
|
|
|
y_tmp = (int32_t)((int32_t)ser->points[p_act] - chart->ymin[ser->y_axis]) * h;
|
|
|
|
|
y_tmp = y_tmp / (chart->ymax[ser->y_axis] - chart->ymin[ser->y_axis]);
|
|
|
|
|
y_tmp = (int32_t)((int32_t)ser->y_points[p_act] - chart->ymin[ser->y_axis_sec]) * h;
|
|
|
|
|
y_tmp = y_tmp / (chart->ymax[ser->y_axis_sec] - chart->ymin[ser->y_axis_sec]);
|
|
|
|
|
col_a.y1 = h - y_tmp + obj->coords.y1 + y_ofs;
|
|
|
|
|
|
|
|
|
|
if(ser->points[p_act] != LV_CHART_POINT_NONE) {
|
|
|
|
|
if(ser->y_points[p_act] != LV_CHART_POINT_NONE) {
|
|
|
|
|
dsc.draw_area = &col_a;
|
|
|
|
|
dsc.rect_dsc = &col_dsc;
|
|
|
|
|
dsc.sub_part_ptr = ser;
|
|
|
|
|
dsc.value = ser->points[p_act];
|
|
|
|
|
dsc.value = ser->y_points[p_act];
|
|
|
|
|
lv_event_send(obj, LV_EVENT_DRAW_PART_BEGIN, &dsc);
|
|
|
|
|
lv_draw_rect(&col_a, &series_mask, &col_dsc);
|
|
|
|
|
lv_event_send(obj, LV_EVENT_DRAW_PART_END, &dsc);
|
|
|
|
|
@@ -1105,7 +1310,6 @@ static void draw_cursors(lv_obj_t * obj, const lv_area_t * clip_area)
|
|
|
|
|
lv_draw_rect(&point_area, &clip_area2, &point_dsc_tmp);
|
|
|
|
|
lv_event_send(obj, LV_EVENT_DRAW_PART_END, &dsc);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -1225,7 +1429,7 @@ static void draw_x_ticks(lv_obj_t * obj, const lv_area_t * clip_area)
|
|
|
|
|
lv_chart_t * chart = (lv_chart_t *)obj;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lv_chart_tick_dsc_t * t = &chart->tick[LV_CHART_AXIS_X];
|
|
|
|
|
lv_chart_tick_dsc_t * t = &chart->tick[LV_CHART_AXIS_PRIMARY_X];
|
|
|
|
|
if(t->major_cnt <= 1) return;
|
|
|
|
|
if(!t->label_en && !t->major_len && !t->minor_len) return;
|
|
|
|
|
|
|
|
|
|
@@ -1256,7 +1460,7 @@ static void draw_x_ticks(lv_obj_t * obj, const lv_area_t * clip_area)
|
|
|
|
|
|
|
|
|
|
lv_obj_draw_part_dsc_t dsc;
|
|
|
|
|
lv_obj_draw_dsc_init(&dsc, clip_area);
|
|
|
|
|
dsc.id = LV_CHART_AXIS_X;
|
|
|
|
|
dsc.id = LV_CHART_AXIS_PRIMARY_X;
|
|
|
|
|
dsc.part = LV_PART_TICKS;
|
|
|
|
|
dsc.label_dsc = &label_dsc;
|
|
|
|
|
dsc.line_dsc = &line_dsc;
|
|
|
|
|
@@ -1391,4 +1595,50 @@ static void invalidate_point(lv_obj_t * obj, uint16_t i)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void new_points_alloc(lv_obj_t * obj, lv_chart_series_t * ser, uint32_t cnt, lv_coord_t ** a)
|
|
|
|
|
{
|
|
|
|
|
if((*a) == NULL) return;
|
|
|
|
|
|
|
|
|
|
lv_chart_t * chart = (lv_chart_t*) obj;
|
|
|
|
|
uint32_t point_cnt_old = chart->point_cnt;
|
|
|
|
|
uint32_t i;
|
|
|
|
|
|
|
|
|
|
if(ser->last_point != 0) {
|
|
|
|
|
lv_coord_t * new_points = lv_mem_alloc(sizeof(lv_coord_t) * cnt);
|
|
|
|
|
LV_ASSERT_MALLOC(new_points);
|
|
|
|
|
if(new_points == NULL) return;
|
|
|
|
|
|
|
|
|
|
if(cnt >= point_cnt_old) {
|
|
|
|
|
for(i = 0; i < point_cnt_old; i++) {
|
|
|
|
|
new_points[i] =
|
|
|
|
|
(*a)[(i + ser->last_point) % point_cnt_old]; /*Copy old contents to new array*/
|
|
|
|
|
}
|
|
|
|
|
for(i = point_cnt_old; i < cnt; i++) {
|
|
|
|
|
new_points[i] = LV_CHART_POINT_NONE; /*Fill up the rest with default value*/
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
for(i = 0; i < cnt; i++) {
|
|
|
|
|
new_points[i] =
|
|
|
|
|
(*a)[(i + ser->last_point) % point_cnt_old]; /*Copy old contents to new array*/
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*Switch over pointer from old to new*/
|
|
|
|
|
lv_mem_free((*a));
|
|
|
|
|
(*a) = new_points;
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
(*a) = lv_mem_realloc((*a), sizeof(lv_coord_t) * cnt);
|
|
|
|
|
LV_ASSERT_MALLOC((*a));
|
|
|
|
|
if((*a) == NULL) return;
|
|
|
|
|
/*Initialize the new points*/
|
|
|
|
|
if(cnt > point_cnt_old) {
|
|
|
|
|
for(i = point_cnt_old - 1; i < cnt; i++) {
|
|
|
|
|
(*a)[i] = LV_CHART_POINT_NONE;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#endif
|
|
|
|
|
|