/* * Copyright 2020 Vincent Sanders * * This file is part of NetSurf. * * NetSurf is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * NetSurf is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ /** * \file * content generator for the about scheme chart page * * A chart consists of the figure area in which a chart a title and a * key are placed. * * */ #include #include #include #include #include #include #include "netsurf/inttypes.h" #include "utils/utils.h" #include "utils/errors.h" #include "utils/nsurl.h" #include "private.h" #include "chart.h" /** minimum figure dimension */ #define FIGURE_MIN_WIDTH 150 #define FIGURE_MIN_HEIGHT 100 enum chart_type { CHART_TYPE_UNKNOWN, CHART_TYPE_PIE, }; /* type of chart key */ enum key_type { CHART_KEY_UNSET, CHART_KEY_NONE, CHART_KEY_LEFT, CHART_KEY_RIGHT, CHART_KEY_TOP, CHART_KEY_BOT, CHART_KEY_END }; struct chart_label { char *title; /* label title */ unsigned int colour; /* colour */ }; struct chart_series { unsigned int len; /* number of values in the series */ float *value; /* array of values */ }; #define MAX_SERIES 4 struct chart_data { unsigned int series_len; struct chart_series series[MAX_SERIES]; unsigned int label_len; /* number of labels */ struct chart_label *label; }; /** * parameters for a chart figure */ struct chart_param { enum chart_type type; enum key_type key; /* what type of key to use */ unsigned int width; /* width of figure */ unsigned int height; /* height of figure */ char *title; /* title */ struct { unsigned int x; unsigned int y; unsigned int width; unsigned int height; } area; /* chart area within figure */ struct chart_data data; }; #define DEF_COLOUR_NUM 8 /** default colour series */ static unsigned int colour_series[DEF_COLOUR_NUM] = { 0x00ff00, /* green */ 0x0000ff, /* blue */ 0xff0000, /* red */ 0xffff00, /* yellow */ 0x00ffff, /* cyan */ 0xff00ff, /* pink */ 0x777777, /* grey */ 0x000000, /* black */ }; /* ensures there are labels present for every value */ static nserror ensure_label_count(struct chart_param *chart, unsigned int count) { unsigned int lidx; int deltac; struct chart_label *nlabels; deltac = count - chart->data.label_len; if (deltac <= 0) { /* there are enough labels */ return NSERROR_OK; } nlabels = realloc(chart->data.label, count * sizeof(struct chart_label)); if (nlabels == NULL) { return NSERROR_NOMEM; } chart->data.label = nlabels; for (lidx = chart->data.label_len; lidx < count; lidx++) { chart->data.label[lidx].title = calloc(1, 20); snprintf(chart->data.label[lidx].title, 19, "item %d", lidx + 1); chart->data.label[lidx].colour = colour_series[lidx % DEF_COLOUR_NUM]; } chart->data.label_len = count; return NSERROR_OK; } /** * extract values for a series */ static nserror extract_series_values(struct chart_param *chart, unsigned int series_num, const char *valstr, size_t valstrlen) { nserror res; unsigned int valcur; size_t valstart;/* value start in valstr */ size_t vallen; /* value end in valstr */ struct chart_series *series; series = chart->data.series + series_num; /* ensure we do not leak any data in this series */ if (series->value != NULL) { free(series->value); } /* count how many values present */ for (series->len = 1, valstart=0; valstart < valstrlen; valstart++) { if (valstr[valstart] == ',') { series->len++; } } /* allocate storage for values */ series->value = calloc(series->len, sizeof(float)); if (series->value == NULL) { return NSERROR_NOMEM; } /* extract values from query string */ for (valcur = 0, vallen = 0, valstart = 0; (valstart < valstrlen) && (valcur < series->len); valstart += vallen, valcur++) { /* get query section length */ vallen = 0; while (((valstart + vallen) < valstrlen) && (valstr[valstart + vallen] != ',')) { vallen++; } series->value[valcur] = strtof(valstr + valstart, NULL); vallen++; /* account for , separator */ } res = ensure_label_count(chart, series->len); return res; } /** * extract values for next series */ static nserror extract_next_series_values(struct chart_param *chart, const char *valstr, size_t valstrlen) { nserror res; if (chart->data.series_len >= MAX_SERIES) { return NSERROR_NOSPACE; } res = extract_series_values(chart, chart->data.series_len, valstr, valstrlen); if (res == NSERROR_OK) { chart->data.series_len++; } return res; } /** * extract label title */ static nserror extract_series_labels(struct chart_param *chart, const char *valstr, size_t valstrlen) { nserror res; unsigned int valcount; /* count of values in valstr */ unsigned int valcur; size_t valstart;/* value start in valstr */ size_t vallen; /* value end in valstr */ for (valcount = 1, valstart=0; valstart < valstrlen; valstart++) { if (valstr[valstart] == ',') { valcount++; } } res = ensure_label_count(chart, valcount); if (res != NSERROR_OK) { return res; } for (valcur = 0, vallen = 0, valstart = 0; (valstart < valstrlen) && (valcur < chart->data.label_len); valstart += vallen, valcur++) { /* get query section length */ vallen = 0; while (((valstart + vallen) < valstrlen) && (valstr[valstart + vallen] != ',')) { vallen++; } chart->data.label[valcur].title = strndup(valstr + valstart, vallen); vallen++; /* account for , separator */ } return NSERROR_OK; } /** * extract labels colour */ static nserror extract_series_colours(struct chart_param *chart, const char *valstr, size_t valstrlen) { return NSERROR_OK; } /** * process a part of a query */ static nserror process_query_section(const char *str, size_t len, struct chart_param *chart) { nserror res = NSERROR_OK; if ((len > 6) && (strncmp(str, "width=", 6) == 0)) { /* figure width */ chart->width = strtoul(str + 6, NULL, 10); } else if ((len > 7) && (strncmp(str, "height=", 7) == 0)) { /* figure height */ chart->height = strtoul(str + 7, NULL, 10); } else if ((len > 8) && (strncmp(str, "cawidth=", 8) == 0)) { /* chart area width */ chart->area.width = strtoul(str + 8, NULL, 10); } else if ((len > 9) && (strncmp(str, "caheight=", 9) == 0)) { /* chart area height */ chart->area.height = strtoul(str + 9, NULL, 10); } else if ((len > 4) && (strncmp(str, "key=", 4) == 0)) { /* figure has key */ chart->key = strtoul(str + 4, NULL, 10); } else if ((len > 6) && (strncmp(str, "title=", 6) == 0)) { chart->title = strndup(str + 6, len - 6); } else if ((len > 5) && (strncmp(str, "type=", 5) == 0)) { if (strncmp(str + 5, "pie", len - 5) == 0) { chart->type = CHART_TYPE_PIE; } else { chart->type = CHART_TYPE_UNKNOWN; } } else if ((len > 7) && (strncmp(str, "values=", 7) == 0)) { res = extract_next_series_values(chart, str + 7, len - 7); } else if ((len > 7) && (strncmp(str, "labels=", 7) == 0)) { res = extract_series_labels(chart, str + 7, len - 7); } else if ((len > 8) && (strncmp(str, "colours=", 8) == 0)) { res = extract_series_colours(chart, str + 8, len - 8); } return res; } static nserror chart_from_query(struct nsurl *url, struct chart_param *chart) { nserror res; char *querystr; size_t querylen; size_t kvstart;/* key value start */ size_t kvlen; /* key value end */ res = nsurl_get(url, NSURL_QUERY, &querystr, &querylen); if (res != NSERROR_OK) { return res; } for (kvlen = 0, kvstart = 0; kvstart < querylen; kvstart += kvlen) { /* get query section length */ kvlen = 0; while (((kvstart + kvlen) < querylen) && (querystr[kvstart + kvlen] != '&')) { kvlen++; } res = process_query_section(querystr + kvstart, kvlen, chart); if (res != NSERROR_OK) { break; } kvlen++; /* account for & separator */ } free(querystr); /* sanity check dimensions */ if (chart->width < FIGURE_MIN_WIDTH) { /* bad width - check height */ if (chart->height < FIGURE_MIN_HEIGHT) { /* both bad set to defaults */ chart->width = FIGURE_MIN_WIDTH; chart->height = FIGURE_MIN_HEIGHT; } else { /* base width on valid height */ chart->width = (chart->height * 3) / 2; } } else { /* good width check height */ if (chart->height < FIGURE_MIN_HEIGHT) { /* base height on valid width */ chart->height = (chart->width * 2) / 3; } } /* ensure legend type correct */ if ((chart->key == CHART_KEY_UNSET) || (chart->key >= CHART_KEY_END )) { /* default to putting key on right */ chart->key = CHART_KEY_RIGHT; } return NSERROR_OK; } static nserror output_pie_legend(struct fetch_about_context *ctx, struct chart_param *chart) { nserror res; unsigned int lblidx; unsigned int legend_width; unsigned int legend_height; unsigned int vertical_spacing; switch (chart->key) { case CHART_KEY_NONE: break; case CHART_KEY_RIGHT: legend_width = chart->width - chart->area.width - chart->area.x; legend_width -= 10; /* margin */ legend_height = chart->height; vertical_spacing = legend_height / (chart->data.label_len + 1); for(lblidx = 0; lblidx < chart->data.label_len ; lblidx++) { res = fetch_about_ssenddataf(ctx, "", chart->width - legend_width, (vertical_spacing * lblidx) + (vertical_spacing/2), vertical_spacing * 2 / 3, vertical_spacing * 2 / 3, chart->data.label[lblidx].colour); if (res != NSERROR_OK) { return res; } res = fetch_about_ssenddataf(ctx, "%s", chart->width - legend_width + vertical_spacing, vertical_spacing * (lblidx+1), chart->data.label[lblidx].colour, chart->data.label[lblidx].title); if (res != NSERROR_OK) { return res; } } break; default: break; } return NSERROR_OK; } static float compute_series_total(struct chart_param *chart, unsigned int series) { float total; unsigned int curdata; for (total = 0, curdata = 0; curdata < chart->data.series[series].len; curdata++) { total += chart->data.series[series].value[curdata]; } return total; } /** * render the data as a pie chart svg */ static bool pie_chart(struct fetch_about_context *ctx, struct chart_param *chart) { nserror res; float ra; /* pie a radius */ float rb; /* pie b radius */ float series_total; unsigned int curdata; /* current data point index */ float last_x, last_y; float end_x, end_y; float start; float extent; bool large; float circle_centre_x, circle_centre_y; /* ensure there is data to render */ if ((chart->data.series_len < 1) || (chart->data.series[0].len < 2)) { return NSERROR_BAD_PARAMETER; } /* get the first series total value */ series_total = compute_series_total(chart, 0); if (series_total == 0) { /* dividing by zero is embarasing */ return NSERROR_BAD_PARAMETER; } /* * need to ensure the chart area is setup correctly * * this is left to each chart type as different charts * have differnt requirements */ if ((chart->area.width == 0) || (chart->area.height == 0)) { /* * pie chart defaults to square of smaller of figure * width and height */ if (chart->width > chart->height) { chart->area.width = chart->area.height = (chart->height - chart->area.x); } else { chart->area.width = chart->area.height = (chart->width - chart->area.y); } } /* content is going to return ok */ fetch_about_set_http_code(ctx, 200); /* content type */ if (fetch_about_send_header(ctx, "Content-Type: image/svg; charset=utf-8")) { goto aborted; } /* get the pie charts elipse radii */ ra = chart->area.width / 2; rb = chart->area.height / 2; /* get the offset to the circle centre */ circle_centre_x = chart->area.x + ra; circle_centre_y = chart->area.y + rb; /* svg header */ res = fetch_about_ssenddataf(ctx, "\n", chart->width, chart->height); if (res != NSERROR_OK) { goto aborted; } /* generate the legend */ res = output_pie_legend(ctx, chart); if (res != NSERROR_OK) { goto aborted; } /* plot the arcs */ start = -M_PI_2; last_x = (ra * cos(start)); last_y = (rb * sin(start)); /* iterate over each data point creating a slice o pie */ for (curdata=0; curdata < chart->data.series[0].len; curdata++) { extent = ((chart->data.series[0].value[curdata] / series_total) * 2 * M_PI); end_x = (ra * cos(start + extent)); end_y = (rb * sin(start + extent)); if (extent > M_PI) { large = true; } else { large = false; } res = fetch_about_ssenddataf( ctx, "\n", circle_centre_x + last_x, circle_centre_y + last_y, ra, rb, large?1:0, circle_centre_x + end_x, circle_centre_y + end_y, circle_centre_x, circle_centre_y, chart->data.label[curdata].colour); if (res != NSERROR_OK) { goto aborted; } last_x = end_x; last_y = end_y; start +=extent; } res = fetch_about_ssenddataf(ctx, "\n"); if (res != NSERROR_OK) { goto aborted; } fetch_about_send_finished(ctx); return true; aborted: return false; } /** * Handler to generate about scheme chart page. * * generates an svg chart * * \param ctx The fetcher context. * \return true if handled false if aborted. */ bool fetch_about_chart_handler(struct fetch_about_context *ctx) { nserror res; struct chart_param chart; memset(&chart, 0, sizeof(struct chart_param)); res = chart_from_query(fetch_about_get_url(ctx), &chart); if (res != NSERROR_OK) { goto aborted; } switch (chart.type) { case CHART_TYPE_PIE: return pie_chart(ctx, &chart); default: break; } aborted: return false; }