889 lines
31 KiB
PHP
889 lines
31 KiB
PHP
<?php
|
|
define("DUPLICATE_NAME_IN_SAME_GROUP", true);
|
|
|
|
// kiolvassa a választható emberek listáját
|
|
function read_electable_people(string $fname): array
|
|
{
|
|
$s = file_get_contents($fname);
|
|
|
|
// szétszedés csoportnév-csoporttag sorozatra
|
|
$group_name_seq = preg_split("/(\*\*)|(__)/", trim($s), -1, PREG_SPLIT_NO_EMPTY);
|
|
|
|
// csoportok feldolgozása egyenként
|
|
$ep = [];
|
|
for ($i = 0; $i < count($group_name_seq); $i += 2) {
|
|
$group = trim($group_name_seq[$i]); // csoport kiolvasása
|
|
$names = preg_split("/(\n)|(\r\n)/", trim($group_name_seq[$i + 1]), -1, PREG_SPLIT_NO_EMPTY); // nevek a csoportból
|
|
|
|
foreach ($names as $name) {
|
|
if (trim($name) === "") { // üres sorokat átugorja
|
|
continue;
|
|
}
|
|
$new_rec = ["name" => $name, "group" => $group];
|
|
if (DUPLICATE_NAME_IN_SAME_GROUP || !in_array($new_rec, $ep)) {
|
|
$ep[] = $new_rec;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
return $ep;
|
|
}
|
|
|
|
define("MINIMUM_NEEDLE_LENGTH", 3);
|
|
define("HIDE_GROUP_OF_NON_DUPLICATES", true);
|
|
|
|
// keresés a nevek között
|
|
function search_in_names(array $records, string $needle, string $group = "", bool $show_only_exact_match = false,
|
|
bool $hide_groups_of_non_duplicates = HIDE_GROUP_OF_NON_DUPLICATES): array
|
|
{
|
|
// keresés
|
|
$needle = trim($needle);
|
|
|
|
if (strlen($needle) < MINIMUM_NEEDLE_LENGTH) {
|
|
return [];
|
|
}
|
|
|
|
$exact_match = [];
|
|
$hitpos_array = [];
|
|
|
|
$filter = function (array $var) use (&$hitpos_array, &$exact_match, $needle, $group, $show_only_exact_match): bool {
|
|
// ha a csoporton belül keres
|
|
if ($group !== "" && strtolower($var["group"]) !== strtolower($group)) {
|
|
return false;
|
|
}
|
|
|
|
$hitpos = iconv_strpos(strtolower($var["name"]), strtolower($needle));
|
|
|
|
// pontos egyezés vizsgálata (hossz és kezdőindex vizsgálata)
|
|
if ($show_only_exact_match && ($hitpos != 0 || strlen($var["name"]) !== strlen($needle))) {
|
|
$hitpos = false;
|
|
}
|
|
|
|
// ha van egyezés...
|
|
if ($hitpos !== false) {
|
|
$hitpos_array[] = $hitpos;
|
|
|
|
// megvizsgálja, hogy exact egyezés van-e?
|
|
$exact_match[] = $var["name"] === $needle;
|
|
}
|
|
|
|
return $hitpos !== false;
|
|
};
|
|
|
|
$filter_res = array_values(array_filter($records, $filter));
|
|
|
|
if (count($filter_res) > 0) {
|
|
for ($i = 0; $i < count($filter_res); $i++) {
|
|
$filter_res[$i]["hitpos"] = $hitpos_array[$i];
|
|
$filter_res[$i]["exact_match"] = $exact_match[$i];
|
|
$filter_res[$i]["duplicate"] = false;
|
|
}
|
|
}
|
|
|
|
if ($hide_groups_of_non_duplicates) {
|
|
// nevek rendezése hossz szerint (substr gyorsítása n*(n-1)-ről (n-1)+(n-2)+(n-3)... vizsgálatra)
|
|
usort($filter_res, fn($a, $b) => strlen($a["name"]) - strlen($b["name"]));
|
|
|
|
// ha a név nem szerepel több, mint egyszer, akkor a csoportot törli (security reasons)
|
|
for ($i = 0; $i < count($filter_res); $i++) {
|
|
if ($filter_res[$i]["duplicate"]) {
|
|
continue;
|
|
}
|
|
|
|
$name_needle = $filter_res[$i]["name"]; // név, amit keresünk a többiben
|
|
$is_duplicate = false;
|
|
for ($j = $i + 1; $j < count($filter_res); $j++) {
|
|
// ha van találat, akkor kiugrunk a keresésből
|
|
if (stripos($filter_res[$j]["name"], $name_needle) !== false) {
|
|
$filter_res[$j]["duplicate"] = true;
|
|
$is_duplicate = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
$filter_res[$i]["duplicate"] = $is_duplicate;
|
|
}
|
|
|
|
// csoport törlése azokból a nevekből, amikből nem szerepel több
|
|
for ($i = 0; $i < count($filter_res); $i++) {
|
|
if (!$filter_res[$i]["duplicate"]) {
|
|
$filter_res[$i]["group"] = "";
|
|
}
|
|
}
|
|
}
|
|
|
|
// rendezés abc-rendbe
|
|
usort($filter_res, fn($a, $b) => $a["name"] > $b["name"]);
|
|
|
|
return $filter_res;
|
|
}
|
|
|
|
// hozzáillesztés és sorok felcserélése
|
|
function append_and_shuffle(string $filename, string $str)
|
|
{
|
|
$file_contents = file_get_contents($filename, $str); // betöltés
|
|
$file_contents = ($file_contents === false) ? $str : ($file_contents . $str); // új adat hozzáadása
|
|
$lines = explode("\n", $file_contents); // szétszedés soronként
|
|
$lines_trimmed = []; // üres sorok kihagyása
|
|
foreach ($lines as $line) {
|
|
$lt = trim($line);
|
|
if ($lt !== "") {
|
|
$lines_trimmed[] = $lt;
|
|
}
|
|
}
|
|
shuffle($lines_trimmed); // sorok felcserélése
|
|
$file_contents = implode("\n", $lines_trimmed) . "\n"; // összeállítás egy stringbe
|
|
return file_put_contents($filename, $file_contents); // mentés
|
|
}
|
|
|
|
define("DATA_DIR", "data");
|
|
define("LOGIN_ID_FILE_NAME", "electors_masked.md");
|
|
define("VOTE_FILENAME", "votes.dat");
|
|
define("VOTE_COUNT_FILENAME", "vote_count.dat");
|
|
define("USED_ELECTOR_HASH_FILENAME", "electors_submitted_vote.dat");
|
|
define("VOTE_STATE_FILENAME", "vote_state.json");
|
|
define("VOTE_TIMESTAMP_FILENAME", "vote_timestamps.dat");
|
|
|
|
function submit_name(string $name, string $group, string $elector_id)
|
|
{
|
|
// default...
|
|
$ret = [
|
|
"successful" => false,
|
|
"index" => -1
|
|
];
|
|
|
|
// ---------------
|
|
|
|
$used_hash_filename = DATA_DIR . DIRECTORY_SEPARATOR . USED_ELECTOR_HASH_FILENAME;
|
|
$votes_filename = DATA_DIR . DIRECTORY_SEPARATOR . VOTE_FILENAME;
|
|
$vote_count_filename = DATA_DIR . DIRECTORY_SEPARATOR . VOTE_COUNT_FILENAME;
|
|
$timestamp_filename = DATA_DIR . DIRECTORY_SEPARATOR . VOTE_TIMESTAMP_FILENAME;
|
|
|
|
// ---------------
|
|
|
|
// kölcsönös kizárás a számláló fájllal
|
|
$fp = fopen($vote_count_filename, "r+");
|
|
flock($fp, LOCK_EX); // kölcsönös kizárás
|
|
|
|
// megvizsgáljuk, hogy szavazhat-e a felhasználó
|
|
$used_elector_hashes = file_get_contents($used_hash_filename);
|
|
$elector_hash = sha1($elector_id);
|
|
if (strpos($used_elector_hashes, sha1($elector_id)) === false) { // ha szavazhat...
|
|
// személy kikeresése
|
|
$p = read_electable_people(ELECTABLE_PEOPLE_DATABASE);
|
|
$hits = search_in_names($p, $name, $group, true, false);
|
|
|
|
// ha egzakt találat van, azaz a szavazó megfelelő nevet adott meg
|
|
if (count($hits) === 1) {
|
|
// felhasználó azonosítójának beírása a felhasznált hash-ek jegyzékébe
|
|
copy($used_hash_filename, "$used_hash_filename.prev");
|
|
$hash_save_successful = append_and_shuffle($used_hash_filename, "$elector_hash\n") !== false;
|
|
|
|
// szavazat mentése
|
|
$name = $hits[0]["name"];
|
|
$group = $hits[0]["group"];
|
|
$record = "$name//$group\n";
|
|
copy($votes_filename, "$votes_filename.prev");
|
|
$vote_save_successful = append_and_shuffle($votes_filename, $record) !== false;
|
|
|
|
// időbélyeg mentése
|
|
copy($timestamp_filename, "$timestamp_filename.prev");
|
|
$timestamp = date("Y-m-d H:i:s");
|
|
$timestamping_successful = file_put_contents($timestamp_filename, "$timestamp\n", FILE_APPEND) !== false;
|
|
|
|
$success = $hash_save_successful && $vote_save_successful && $timestamping_successful;
|
|
|
|
if ($success) { // ha sikeres minden...
|
|
// szavazatszám növelése
|
|
$cnt = (int)(fread($fp, 255));
|
|
$cnt++;
|
|
$ret["index"] = $cnt;
|
|
fseek($fp, 0);
|
|
fwrite($fp, $cnt);
|
|
} else { // ha valamelyik nem sikerül, akkor visszaállunk a korábbi állapotba
|
|
rename($used_hash_filename, "$used_hash_filename.err_" . time()); // hibás fájl eltárolása
|
|
rename($votes_filename, "$votes_filename.err_" . time());
|
|
rename($timestamp_filename, "$timestamp_filename.err_" . time());
|
|
copy("$used_hash_filename.prev", $used_hash_filename); // régi állapot visszaállítása
|
|
copy("$votes_filename.prev", $votes_filename);
|
|
copy("$timestamp_filename.prev", $timestamp_filename);
|
|
}
|
|
|
|
$ret["successful"] = $success;
|
|
}
|
|
}
|
|
|
|
fclose($fp); // lock elengedése és fájl bezárása
|
|
|
|
return $ret;
|
|
}
|
|
|
|
// név és csoport szétválasztása (NEM UGYANAZ, MINT JS-ben!)
|
|
function fetch_name_and_group(string $compound): array
|
|
{
|
|
$parts = explode("//", $compound);
|
|
return ["name" => $parts[0], "group" => $parts[1]];
|
|
}
|
|
|
|
// állapotfájl előállítása, ha nem létezik
|
|
function load_state()
|
|
{
|
|
$vote_state_fname = DATA_DIR . DIRECTORY_SEPARATOR . VOTE_STATE_FILENAME;
|
|
$state = [];
|
|
if (!file_exists($vote_state_fname)) {
|
|
$state = [
|
|
"state" => "closed",
|
|
"open_for_submit" => false,
|
|
"auto_manage" => false,
|
|
"open_date" => "2022-08-01 08:00:00",
|
|
"close_date" => "2022-08-01 20:00:00",
|
|
"results_public" => false,
|
|
"public_results_plot_filename" => "",
|
|
"public_results_plot_filename_mobile" => ""
|
|
];
|
|
} else {
|
|
$state = json_decode(file_get_contents($vote_state_fname), true);
|
|
}
|
|
|
|
return $state;
|
|
}
|
|
|
|
function save_state($state)
|
|
{
|
|
file_put_contents(DATA_DIR . DIRECTORY_SEPARATOR . VOTE_STATE_FILENAME, json_encode($state, JSON_PRETTY_PRINT));
|
|
}
|
|
|
|
// jelentés generálása
|
|
function generate_report($omit_votes = true)
|
|
{
|
|
$state = load_state();
|
|
$elector_hashes = explode("\n", file_get_contents(DATA_DIR . DIRECTORY_SEPARATOR . LOGIN_ID_FILE_NAME));
|
|
$total_elector_count = 0;
|
|
foreach ($elector_hashes as $elector_hash) {
|
|
if (trim($elector_hash) !== "") {
|
|
$total_elector_count++;
|
|
}
|
|
}
|
|
|
|
$vote_list_str = file_get_contents(DATA_DIR . DIRECTORY_SEPARATOR . VOTE_FILENAME);
|
|
$vote_list = explode("\n", $vote_list_str);
|
|
$votes_grouped = [];
|
|
$total_votes = 0;
|
|
foreach ($vote_list as $vote) {
|
|
// üres sorok kihagyása
|
|
if (strlen(trim($vote)) === 0) {
|
|
continue;
|
|
}
|
|
|
|
$votes_grouped[$vote] = (($votes_grouped[$vote]) ?? 0) + 1;
|
|
$total_votes++;
|
|
}
|
|
|
|
// rendezés szavazatok száma alapján
|
|
uasort($votes_grouped, fn($a, $b) => ($b - $a));
|
|
|
|
// név és csoport szétválasztása
|
|
$votes_structured = [];
|
|
foreach ($votes_grouped as $id_compound => $vote_cnt) {
|
|
$expl_vote = fetch_name_and_group($id_compound);
|
|
$votes_structured[$expl_vote["name"]] = ["name" => $expl_vote["name"], "group" => $expl_vote["group"], "count" => $vote_cnt, "ratio" => ($vote_cnt / $total_votes)];
|
|
}
|
|
|
|
$report = [
|
|
"total_votes" => $total_votes,
|
|
"total_electors" => $total_elector_count,
|
|
"state" => $state
|
|
];
|
|
|
|
if (!$omit_votes) {
|
|
$report["records"] = array_values($votes_structured);
|
|
}
|
|
|
|
return $report;
|
|
}
|
|
|
|
function RGBToHSLV($r, $g, $b, bool $hsv = false)
|
|
{
|
|
$r /= 255;
|
|
$g /= 255;
|
|
$b /= 255;
|
|
|
|
$x_max = max($r, $g, $b);
|
|
$x_min = min($r, $g, $b);
|
|
$c = $x_max - $x_min;
|
|
|
|
$l = ($x_max + $x_min) / 2;
|
|
|
|
$h = 0;
|
|
if ($c === 0) {
|
|
$h = 0;
|
|
} else if ($x_max === $r) {
|
|
$h = 60 * ($g - $b) / $c;
|
|
} else if ($x_max === $g) {
|
|
$h = 60 * (2 + ($b - $r) / $c);
|
|
} else if ($x_max === $b) {
|
|
$h = 60 * (4 + ($r - $g) / $c);
|
|
}
|
|
|
|
if ($hsv) {
|
|
$s = (($x_max === 0)) ? 0 : ((float)$c / $x_max);
|
|
return [$h, $s, $x_max];
|
|
} else {
|
|
$s = (($l === 0) || ($l === 1)) ? 0 : (2 * ($x_max - $l) / (1 - abs(2 * $l - 1)));
|
|
return [$h, $s, $l];
|
|
}
|
|
|
|
}
|
|
|
|
function HSVtoRGB($h, $s, $v)
|
|
{
|
|
$c = $v * $s;
|
|
$h_ = $h / 60;
|
|
$x = $c * (1 - abs(fmod($h_, 2) - 1));
|
|
|
|
$rgb1 = [0, 0, 0];
|
|
if ($h_ >= 0 && $h_ < 1) {
|
|
$rgb1 = [$c, $x, 0];
|
|
} else if ($h_ >= 1 && $h_ < 2) {
|
|
$rgb1 = [$x, $c, 0];
|
|
} else if ($h_ >= 2 && $h_ < 3) {
|
|
$rgb1 = [0, $c, $x];
|
|
} else if ($h_ >= 3 && $h_ < 4) {
|
|
$rgb1 = [0, $x, $c];
|
|
} else if ($h_ >= 4 && $h_ < 5) {
|
|
$rgb1 = [$x, 0, $c];
|
|
} else if ($h_ >= 5 && $h_ < 6) {
|
|
$rgb1 = [$c, 0, $x];
|
|
}
|
|
|
|
$m = $v - $c;
|
|
$rgb = [$rgb1[0] + $m, $rgb1[1] + $m, $rgb1[2] + $m];
|
|
|
|
return $rgb;
|
|
}
|
|
|
|
function RGBtoHex($r, $g, $b)
|
|
{
|
|
$r_str = dechex($r);
|
|
$g_str = dechex($g);
|
|
$b_str = dechex($b);
|
|
|
|
$r_str = (strlen($r_str) === 1) ? "0$r_str" : $r_str;
|
|
$g_str = (strlen($g_str) === 1) ? "0$g_str" : $g_str;
|
|
$b_str = (strlen($b_str) === 1) ? "0$b_str" : $b_str;
|
|
|
|
return "#$r_str$g_str$b_str";
|
|
}
|
|
|
|
//function RGBtoHCL($r, $g, $b)
|
|
//{
|
|
//
|
|
//}
|
|
|
|
define("SVG_REPORT_WIDTH", 700);
|
|
define("SVG_REPORT_HEIGHT", 350);
|
|
define("SVG_REPORT_STROKE_WIDTH", 30);
|
|
define("SVG_REPORT_LEGEND_MARK_SIZE", 20);
|
|
define("SVG_REPORT_HIGHLIGHT_STROKE_WIDTH", 40);
|
|
define("SVG_REPORT_PLOT_THRESHOLD_PERCENT", 5);
|
|
define("SVG_REPORT_LEGEND_FONT_SIZE", 20);
|
|
define("SVG_REPORT_START_COLOR", "003F5C");
|
|
define("SVG_REPORT_END_COLOR", "F89A17");
|
|
define("SVG_REPORT_PIE_RADIUS", 110);
|
|
define("SVG_REPORT_ANIMATION_DUR", 2);
|
|
|
|
// SVG-kép generálása a jelentésből
|
|
function generate_svg_plot($report, $mobile = false, $timing = 8): string
|
|
{
|
|
$pie_orig = [SVG_REPORT_WIDTH / 2, SVG_REPORT_HEIGHT / 2];
|
|
$pie_radius = SVG_REPORT_PIE_RADIUS;
|
|
$cum_percent = 0;
|
|
$legend_font_size = SVG_REPORT_LEGEND_FONT_SIZE;
|
|
|
|
$last_segment_midpoint = [0, 0];
|
|
$last_segment_midpoint_angle = 0;
|
|
|
|
// százalékértéknyi körív rajzolása
|
|
$svg_curve = function ($percent) use (&$cum_percent, $pie_radius, $pie_orig, &$last_segment_midpoint, &$last_segment_midpoint_angle) {
|
|
$x_rot = $cum_percent / 100.0 * 360; // x-tengely forgatása
|
|
|
|
$start_angle = -$cum_percent / 100.0 * 2 * M_PI; // radián
|
|
$end_angle = -($cum_percent + $percent) / 100.0 * 2 * M_PI; // radián
|
|
$middle_point_angle = -($cum_percent + $percent / 2); // fok
|
|
|
|
$start_x = $pie_orig[0] + cos($start_angle) * $pie_radius;
|
|
$start_y = $pie_orig[1] + sin($start_angle) * $pie_radius;
|
|
|
|
$end_x = $pie_orig[0] + cos($end_angle) * $pie_radius;
|
|
$end_y = $pie_orig[1] + sin($end_angle) * $pie_radius;
|
|
|
|
$last_segment_midpoint[0] = $pie_orig[0] + cos($middle_point_angle / 100.0 * 2 * M_PI) * $pie_radius;
|
|
$last_segment_midpoint[1] = $pie_orig[1] + sin($middle_point_angle / 100.0 * 2 * M_PI) * $pie_radius;
|
|
$last_segment_midpoint_angle = $middle_point_angle * 3.6;
|
|
|
|
$cum_percent += $percent;
|
|
return "M $start_x $start_y
|
|
A $pie_radius $pie_radius $x_rot 0 0 $end_x $end_y\n";
|
|
};
|
|
|
|
// kurzor mozgatása
|
|
$svg_move_cursor = function ($x, $y) {
|
|
return "M $x $y\n";
|
|
};
|
|
|
|
// útvonal generálása
|
|
$svg_path = function ($d, $color, $id, $class = "sector", $stroke_width = SVG_REPORT_STROKE_WIDTH) {
|
|
return "<path id='$id' d='$d' stroke='$color' fill='none' stroke-width='$stroke_width' class='$class' />\n";
|
|
};
|
|
|
|
// jelmagyarázat generálása
|
|
$svg_legend = function ($color, $text, $x, $y, $mark_size = SVG_REPORT_LEGEND_MARK_SIZE) use ($legend_font_size, $pie_orig, $mobile) {
|
|
$rect_x = $x;
|
|
$rect_y = $y;
|
|
$circ_radius = $mark_size / 2;
|
|
$anchor_cond = ($x > $pie_orig[0] || $mobile);
|
|
$text_x_offset_sign = $anchor_cond ? 1 : -1;
|
|
$text_anchor = $anchor_cond ? "start" : "end";
|
|
$text_x = $x + $text_x_offset_sign * ($circ_radius + 0.5 * $legend_font_size);
|
|
$text_y = $y; // + ($mark_size - 0.4 * $legend_font_size) / 2;
|
|
|
|
//return "<rect x='$rect_x' y='$rect_y' width='$mark_size' height='$mark_size' fill='$color' rx='4' />
|
|
return "<circle cx='$rect_x' cy='$rect_y' r='$circ_radius' fill='$color' cursor='pointer'/>
|
|
<text class='legend' x='$text_x' y='$text_y' text-anchor='$text_anchor'>$text</text>\n";
|
|
};
|
|
|
|
// színpaletta előállítása
|
|
$generate_palette = function ($start_hex, $end_hex, $cnt): array {
|
|
if (gettype($start_hex) === "string") {
|
|
$start_hex = hexdec($start_hex);
|
|
}
|
|
if (gettype($end_hex) === "string") {
|
|
$end_hex = hexdec($end_hex);
|
|
}
|
|
|
|
// szétválasztás komponensekre
|
|
$start_rgb = [($start_hex & 0xFF0000) >> 16, ($start_hex & 0x00FF00) >> 8, ($start_hex & 0x0000FF)];
|
|
$end_rgb = [($end_hex & 0xFF0000) >> 16, ($end_hex & 0x00FF00) >> 8, ($end_hex & 0x0000FF)];
|
|
|
|
$start_hsv = RGBToHSLV($start_rgb[0], $start_rgb[1], $start_rgb[2], true);
|
|
$end_hsv = RGBToHSLV($end_rgb[0], $end_rgb[1], $end_rgb[2], true);
|
|
|
|
// mindig a hosszabb görbe választása
|
|
$hue_diff = $end_hsv[0] - $start_hsv[0];
|
|
if (abs($hue_diff) < 180) {
|
|
$start_hsv[0] -= 360;
|
|
}
|
|
|
|
// interpoláció
|
|
$step = [($end_hsv[0] - $start_hsv[0]) / ($cnt - 1),
|
|
($end_hsv[1] - $start_hsv[1]) / ($cnt - 1),
|
|
($end_hsv[2] - $start_hsv[2]) / ($cnt - 1)];
|
|
|
|
$palette = [];
|
|
for ($i = 0; $i < $cnt; $i++) {
|
|
$hue = $start_hsv[0] + $step[0] * $i % 360;
|
|
$hue += ($hue < 0) ? 360 : 0;
|
|
$color = [$hue, $start_hsv[1] + $step[1] * $i, $start_hsv[2] + $step[2] * $i];
|
|
$color_rgb = HSVtoRGB($color[0], $color[1], $color[2]);
|
|
//$palette[] = "hsl(" . (floor($color[0])) . "," . (floor($color[1])) . "%," . (floor($color[2])) . "%)";
|
|
//$palette[] = "rgb(" . (round($color_rgb[0] * 255)) . "," . (round($color_rgb[1] * 255)) . "," . (round($color_rgb[2] * 255)) . ")";
|
|
$palette[] = RGBtoHex((round($color_rgb[0] * 255)), (round($color_rgb[1] * 255)), (round($color_rgb[2] * 255)));
|
|
}
|
|
|
|
return $palette;
|
|
};
|
|
|
|
// -------------------
|
|
|
|
// küszöbérték feletti blokkok számának meghatározása
|
|
$sector_count = 0;
|
|
foreach ($report["records"] as $record) {
|
|
$sector_count++;
|
|
if ($record["ratio"] * 100.0 < SVG_REPORT_PLOT_THRESHOLD_PERCENT) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
// színpaletta előállítása
|
|
$palette = $generate_palette(SVG_REPORT_START_COLOR, SVG_REPORT_END_COLOR, $sector_count);
|
|
//$palette = [ "#003f5c", "#424d83", "#8e5092", "#d24f81", "#fa6756", "#f89a17" ];
|
|
|
|
// svg-kép előállítása
|
|
$svg = "";
|
|
|
|
// stíluslap hozzáadása
|
|
$hlsw = SVG_REPORT_HIGHLIGHT_STROKE_WIDTH;
|
|
$animdur = SVG_REPORT_ANIMATION_DUR;
|
|
$svg .= "<style>
|
|
@import url('https://fonts.googleapis.com/css2?family=Faustina');
|
|
.legend { font-family: 'Faustina', serif; font-size: ${legend_font_size}px; dominant-baseline: middle; cursor: pointer; }
|
|
.sector { cursor: pointer; }
|
|
.sector:hover { stroke-width: ${hlsw}px }
|
|
.sectorlabel { font-family: 'Faustina', serif; fill: white; text-anchor: middle; dominant-baseline: middle; cursor: pointer; }
|
|
g.datagroup { animation: fadeIn ${animdur}s forwards; opacity: 0; }
|
|
g.datagroup:hover .sector { stroke-width: ${hlsw}px; }
|
|
@keyframes fadeIn {
|
|
0% { opacity: 0 }
|
|
100% { opacity: 1 }
|
|
}
|
|
</style>";
|
|
|
|
$total = $report["total_votes"];
|
|
$last_legend_anchor = [];
|
|
foreach ($report["records"] as $idx => $record) {
|
|
// kördiagram cikkei
|
|
$percent = $record["count"] * 100.0 / $total;
|
|
$joined_sector = $percent < SVG_REPORT_PLOT_THRESHOLD_PERCENT; // az küszöbérték alatti szavaztokat egybe gyűjtjük
|
|
if ($joined_sector) {
|
|
$percent = 100.0 - $cum_percent; // az összes maradék helyet kitöltjük
|
|
}
|
|
|
|
//$color = "rgb(" . random_int(0, 255) . "," . random_int(0, 255) . "," . random_int(0, 255) . ")";
|
|
$color = $palette[$idx];
|
|
|
|
$anim_delay = $timing * ($sector_count - $idx - 1);
|
|
//$anim_delay = SVG_REPORT_ANIMATION_DUR * 2 / 3 * ($sector_count - $idx - 1);
|
|
$svg .= "<g class='datagroup' style='animation-delay: ${anim_delay}s' id='datagroup${idx}'>";
|
|
$sector_id = "sector$idx";
|
|
$svg .= $svg_path($svg_curve($percent), $color, "$sector_id"); // körcikk
|
|
$svg .= "<text><textPath class='sectorlabel' href='#$sector_id' startOffset='50%'>" // szöveg rajta
|
|
. number_format((float)$percent, 1, ',', '') .
|
|
"%</textPath></text>\n";
|
|
|
|
// jelmagyarázat
|
|
$legend_anchor = [];
|
|
if (!$mobile) { // normál megjelenítés
|
|
$legend_anchor = $pie_orig;
|
|
$legend_anchor[0] += ($last_segment_midpoint[0] - $pie_orig[0]) * (1 + 1.2 * SVG_REPORT_STROKE_WIDTH / $pie_radius);
|
|
$legend_anchor[1] += ($last_segment_midpoint[1] - $pie_orig[1]) * (1 + 1.2 * SVG_REPORT_STROKE_WIDTH / $pie_radius);
|
|
} else { // mobilos megjelenítés
|
|
$legend_anchor[0] = $pie_orig[0] - SVG_REPORT_PIE_RADIUS;
|
|
$legend_anchor[1] = $pie_orig[1] + SVG_REPORT_PIE_RADIUS + SVG_REPORT_HIGHLIGHT_STROKE_WIDTH * 1.5 + 1.5 * $legend_font_size * $idx;
|
|
}
|
|
|
|
if (!$joined_sector) {
|
|
$legend_text = "${record['name']} (${record['count']})";
|
|
} else {
|
|
$legend_text = "Többiek";
|
|
}
|
|
$svg .= $svg_legend($color, $legend_text, $legend_anchor[0], $legend_anchor[1]);
|
|
$svg .= "</g>";
|
|
|
|
$last_legend_anchor = $legend_anchor;
|
|
|
|
if ($joined_sector) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
// viewBox beállítása
|
|
$viewBox = [];
|
|
if (!$mobile) { // asztali kép
|
|
$viewBox = "0 0 " . SVG_REPORT_WIDTH . " " . SVG_REPORT_HEIGHT;
|
|
$style = "style='width: " . SVG_REPORT_WIDTH . "px;'";
|
|
} else { // mobilos kép
|
|
$vBx = SVG_REPORT_WIDTH / 2 - (SVG_REPORT_PIE_RADIUS + SVG_REPORT_HIGHLIGHT_STROKE_WIDTH + 1);
|
|
$vBy = SVG_REPORT_HEIGHT / 2 - (SVG_REPORT_PIE_RADIUS + SVG_REPORT_HIGHLIGHT_STROKE_WIDTH + 1);
|
|
$vBw = (SVG_REPORT_PIE_RADIUS + SVG_REPORT_HIGHLIGHT_STROKE_WIDTH) * 2;
|
|
$vBh = $last_legend_anchor[1] + 2 * $legend_font_size;
|
|
$viewBox = "$vBx $vBy $vBw $vBh";
|
|
$style = "";
|
|
}
|
|
|
|
$svg = "<svg viewBox='$viewBox' xmlns='http://www.w3.org/2000/svg' $style>" . $svg;
|
|
|
|
|
|
$svg .= "</svg>";
|
|
|
|
return $svg;
|
|
}
|
|
|
|
// azonosító ellenőrzése
|
|
function verify_id($id)
|
|
{
|
|
if (trim($id) === "") {
|
|
return ["valid" => false, "used" => false];
|
|
}
|
|
$ids = file_get_contents(DATA_DIR . DIRECTORY_SEPARATOR . LOGIN_ID_FILE_NAME);
|
|
$used_hashes = file_get_contents(DATA_DIR . DIRECTORY_SEPARATOR . USED_ELECTOR_HASH_FILENAME);
|
|
$needle = sha1($id) . "\n"; // teljes sort kersünk, mert különben elég lenne egy részét ismerni az adatnak
|
|
$id_valid = strpos($ids, $needle) !== false;
|
|
$id_used = strpos($used_hashes, $needle) !== false;
|
|
return ["valid" => $id_valid, "used" => $id_used];
|
|
}
|
|
|
|
// --------------------------------
|
|
|
|
define("CP_LOGIN_ID_FILENAME", "cp_login_hashes.dat");
|
|
|
|
// Vezérlőpult kezelése
|
|
|
|
// bejelentkezés a vezérlőpultba
|
|
function cp_verify_id($id)
|
|
{
|
|
if (trim($id) === "") {
|
|
return false;
|
|
}
|
|
$ids = file_get_contents(DATA_DIR . DIRECTORY_SEPARATOR . CP_LOGIN_ID_FILENAME);
|
|
return strpos($ids, sha1($id) . "\n") !== false;
|
|
}
|
|
|
|
define("STATE_LOCK_FILENAME", "state.lock");
|
|
define("PUBLIC_DIR", "public");
|
|
define("FULL_RESULT_RECORDS_FILENAME", "full_results.json");
|
|
|
|
// szavazásállapot módosítása
|
|
function cp_modify_state($command)
|
|
{
|
|
$fp = fopen(DATA_DIR . DIRECTORY_SEPARATOR . STATE_LOCK_FILENAME, "w");
|
|
flock($fp, LOCK_EX);
|
|
|
|
$state = load_state();
|
|
switch ($command) {
|
|
case "open_election":
|
|
$state["state"] = "open";
|
|
break;
|
|
case "close_election":
|
|
$state["state"] = "closed";
|
|
break;
|
|
case "enable_submitting":
|
|
$state["open_for_submit"] = true;
|
|
break;
|
|
case "disable_submitting":
|
|
$state["open_for_submit"] = false;
|
|
break;
|
|
case "publish_results":
|
|
{
|
|
// grafikon rajzolása
|
|
$state["results_public"] = true;
|
|
$uniqid = uniqid();
|
|
$plot_filename_normal = PUBLIC_DIR . DIRECTORY_SEPARATOR . "results_$uniqid.svg";
|
|
$plot_filename_mobile = PUBLIC_DIR . DIRECTORY_SEPARATOR . "m_results_$uniqid.svg";
|
|
$report = generate_report(false);
|
|
$svg_normal = generate_svg_plot($report);
|
|
file_put_contents($plot_filename_normal, $svg_normal);
|
|
$svg_mobile = generate_svg_plot($report, true);
|
|
file_put_contents($plot_filename_mobile, $svg_mobile);
|
|
$state["public_results_plot_filename"] = $plot_filename_normal;
|
|
$state["public_results_plot_filename_mobile"] = $plot_filename_mobile;
|
|
|
|
// szavazat-eredmények mentése
|
|
$records_without_group = [];
|
|
foreach ($report["records"] as $record) { // csoport törlése (privacy and like that)
|
|
$records_without_group[] = ["name" => $record["name"], "count" => $record["count"], "ratio" => $record["ratio"]];
|
|
}
|
|
file_put_contents(PUBLIC_DIR . DIRECTORY_SEPARATOR . FULL_RESULT_RECORDS_FILENAME, json_encode($records_without_group));
|
|
}
|
|
break;
|
|
case "unpublish_results":
|
|
$state["results_public"] = false;
|
|
unlink($state["public_results_plot_filename"]);
|
|
unlink($state["public_results_plot_filename_mobile"]);
|
|
$state["public_results_plot_filename"] = "";
|
|
$state["public_results_plot_filename_mobile"] = "";
|
|
break;
|
|
}
|
|
save_state($state);
|
|
|
|
fclose($fp);
|
|
|
|
return $state;
|
|
}
|
|
|
|
define("TEMPDIR", "temp");
|
|
|
|
// jelentés előállítása csv-ként
|
|
function generate_csv_report($report)
|
|
{
|
|
$csv_report_filename = TEMPDIR . DIRECTORY_SEPARATOR . uniqid("report_") . ".csv";
|
|
|
|
$csvfp = fopen($csv_report_filename, "w");
|
|
if ($csvfp === false) {
|
|
return false;
|
|
}
|
|
|
|
// fejléc mentése
|
|
$header = ["Név", "Csoport", "Szavazatok", "Arány", "Össz_szavazat", "Össz_szavazó", "Részv_arány"];
|
|
fputcsv($csvfp, $header);
|
|
|
|
// első sor mentése
|
|
$records = $report["records"];
|
|
if (count($records) === 0) { // egy sort legalább hozzáadunk
|
|
$records[0] = ["name" => "", "group" => "", "count" => "", "ratio" => ""];
|
|
}
|
|
$total_votes = $report["total_votes"];
|
|
$total_electors = $report["total_electors"];
|
|
$firstLine = [$records[0]["name"], $records[0]["group"], $records[0]["count"], number_format($records[0]["ratio"] * 100, 2) . "%",
|
|
$total_votes, $total_electors, number_format($total_votes / $total_electors * 100, 2) . "%"];
|
|
fputcsv($csvfp, $firstLine);
|
|
|
|
// többi sor mentése
|
|
for ($i = 1; $i < count($records); $i++) {
|
|
$records[$i]["ratio"] = number_format($records[$i]["ratio"] * 100, 2) . "%";
|
|
fputcsv($csvfp, $records[$i]);
|
|
}
|
|
|
|
fclose($csvfp);
|
|
|
|
return $csv_report_filename;
|
|
}
|
|
|
|
function cp_add_access_code(string $ac) {
|
|
$hash = sha1($ac);
|
|
file_put_contents(DATA_DIR . DIRECTORY_SEPARATOR . LOGIN_ID_FILE_NAME, "$hash\n", FILE_APPEND);
|
|
}
|
|
|
|
// --------------------------------
|
|
|
|
define("ELECTABLE_PEOPLE_DATABASE", "data/electable_people.md");
|
|
|
|
$action = json_decode($_POST["action"]) ?? "none";
|
|
$req_data = json_decode($_POST["data"] ?? "[]", true);
|
|
|
|
$res = "";
|
|
$disable_jsoning = false;
|
|
|
|
// Belépés nélküli feldolgozás, ha nem sikerül, akkor processed = false;
|
|
$processed = true;
|
|
switch ($action) {
|
|
case "login":
|
|
{
|
|
$elector_id = $req_data["elector_id"] ?? "";
|
|
$res = verify_id($elector_id);
|
|
if ($res["valid"]) {
|
|
$state = load_state();
|
|
$res["open"] = $state["state"] === "open";
|
|
if ($res["open"]) {
|
|
$res["open_for_submit"] = $state["open_for_submit"];
|
|
$res["results_public"] = $state["results_public"];
|
|
}
|
|
} else if (cp_verify_id($elector_id)) { // megnézzük, hogy admin-e, ha igen, akkor a szavazás indítása előtt beléphet
|
|
$res["valid"] = true;
|
|
$res["used"] = false;
|
|
$state = load_state();
|
|
$res["open"] = $state["state"] === "open";
|
|
$res["open_for_submit"] = true;
|
|
$res["results_public"] = false;
|
|
}
|
|
}
|
|
break;
|
|
case "cp_login":
|
|
{
|
|
$cp_id = $req_data["cp_id"] ?? "";
|
|
$res = cp_verify_id($cp_id);
|
|
}
|
|
break;
|
|
default:
|
|
$processed = false;
|
|
break;
|
|
}
|
|
|
|
// ha feldolgozta, akkor ugrás a végére (inkább a goto, mint 46 zárójel... :D)
|
|
if ($processed) {
|
|
goto exit_point; // --->
|
|
}
|
|
|
|
// Belépéses (azonosítóhoz kötött) feldolgozás
|
|
$elector_id = json_decode($_POST["elector_id"] ?? "");
|
|
$state = load_state();
|
|
$admin_login = cp_verify_id($elector_id);
|
|
if (((verify_id($elector_id)["valid"]) || $admin_login) && $state["state"] === "open") {
|
|
$processed = true;
|
|
switch ($action) {
|
|
case "search":
|
|
{
|
|
if (!$state["open_for_submit"] && !$admin_login) {
|
|
break;
|
|
}
|
|
|
|
$needle = $req_data["needle"] ?? "";
|
|
$group = $req_data["group"] ?? "";
|
|
$p = read_electable_people(ELECTABLE_PEOPLE_DATABASE);
|
|
$hits = search_in_names($p, $needle, $group);
|
|
$res = $hits;
|
|
}
|
|
break;
|
|
case "submit":
|
|
{
|
|
if (!$state["open_for_submit"] && !$admin_login) {
|
|
break;
|
|
}
|
|
|
|
$name = $req_data["needle"] ?? "";
|
|
$group = $req_data["group"] ?? "";
|
|
$res = submit_name($name, $group, $elector_id);
|
|
}
|
|
break;
|
|
case "results":
|
|
{
|
|
$mobile = $req_data["mobile"] ?? false;
|
|
$state = load_state();
|
|
$res = ["available" => $state["results_public"]];
|
|
if ($state["results_public"]) {
|
|
$res["plot_url"] = $mobile ? $state["public_results_plot_filename_mobile"] : $state["public_results_plot_filename"];
|
|
$res["details"] = json_decode(file_get_contents(PUBLIC_DIR . DIRECTORY_SEPARATOR . FULL_RESULT_RECORDS_FILENAME), true);
|
|
}
|
|
}
|
|
break;
|
|
case "none":
|
|
break;
|
|
default:
|
|
$processed = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ($processed) {
|
|
goto exit_point;
|
|
}
|
|
|
|
// Control panel parancsok
|
|
$processed = true;
|
|
$elector_id = json_decode($_POST["cp_id"] ?? "");
|
|
if (cp_verify_id($elector_id)) {
|
|
switch ($action) {
|
|
case "cp_short_info":
|
|
{
|
|
$res = generate_report(true); // az összesített szavazatokat hagyja ki!
|
|
}
|
|
break;
|
|
case "cp_mod_state":
|
|
{
|
|
$command = $req_data["command"];
|
|
$res = cp_modify_state($command);
|
|
}
|
|
break;
|
|
case "cp_generate_plot":
|
|
{
|
|
$disable_jsoning = true;
|
|
$res = generate_svg_plot(generate_report(false), false, 0);
|
|
}
|
|
break;
|
|
case "cp_csv_report":
|
|
{
|
|
$report = generate_report(false); // teljes jelentés generálása
|
|
$res = generate_csv_report($report); // csv generálása
|
|
}
|
|
break;
|
|
case "cp_add_access_code":
|
|
{
|
|
$ac = $req_data["access_code"] ?? "";
|
|
if ($ac !== "") {
|
|
cp_add_access_code($ac);
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
$processed = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
exit_point:
|
|
echo $disable_jsoning ? $res : json_encode($res);
|
|
|
|
//$p = read_electable_people("data/electable_people.md");
|
|
|
|
return;
|