Compare commits
145 Commits
fea2f69686
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 35812791f5 | |||
| 72c7413195 | |||
|
|
2cf3d0bd26 | ||
|
|
babf8f81b6 | ||
|
|
1e3e4a99e6 | ||
|
|
60a33194e1 | ||
|
|
d8684760e5 | ||
|
|
1735d52781 | ||
|
|
675058201a | ||
|
|
05b0898979 | ||
|
|
c172222e15 | ||
|
|
5e3d22de3e | ||
|
|
9b9c1a6869 | ||
|
|
f5f04d01eb | ||
|
|
0bddcb7520 | ||
|
|
608ed0f92c | ||
|
|
2a0ba8eed2 | ||
|
|
fde39bcb1b | ||
| 5791273f56 | |||
|
|
45a5afc5f9 | ||
|
|
7af17f0e0b | ||
|
|
c9ec1933f6 | ||
|
|
f8fd1cc20c | ||
|
|
464d617ecc | ||
|
|
af457dbd8c | ||
|
|
dea1ccfe86 | ||
|
|
abf5147c79 | ||
|
|
6eef04e89f | ||
|
|
bac9be7003 | ||
| 910bfe85c1 | |||
|
|
b8a29a1de2 | ||
|
|
c47eb353a7 | ||
|
|
51af861f38 | ||
|
|
74c24a37a2 | ||
|
|
802c091ba2 | ||
|
|
ccae561fda | ||
|
|
34e0297947 | ||
|
|
abb4f5675c | ||
|
|
3708a694ed | ||
|
|
9f25e4039e | ||
|
|
84e7d472a9 | ||
|
|
73e4ed85d0 | ||
|
|
decc12f381 | ||
|
|
8543bc1c53 | ||
|
|
620d1bf06d | ||
|
|
1f5dc18e81 | ||
| 94ea4f6a5f | |||
|
|
72eba5fd58 | ||
|
|
68938751d4 | ||
|
|
8b2e66b044 | ||
|
|
db2d4fcbc7 | ||
|
|
d8591d5625 | ||
|
|
a30855ac30 | ||
|
|
6df6cc2d5e | ||
| 03652e1285 | |||
|
|
c27cc2831b | ||
| fff24136a1 | |||
| 45eba00d5d | |||
| 0f05fe20ec | |||
|
|
f9c151bfae | ||
|
|
219b242503 | ||
|
|
2f5806a227 | ||
|
|
e56fc253ba | ||
|
|
17f2bf0d9a | ||
|
|
efecbe1d76 | ||
|
|
e2fbd1b1ef | ||
|
|
5e8170ecef | ||
|
|
bbffeb89f1 | ||
|
|
cba26faf31 | ||
|
|
82f65a2050 | ||
| 7b7317cd55 | |||
| 1f80ec41db | |||
| bb66d8c50e | |||
|
|
2453d550ce | ||
|
|
af53825c05 | ||
|
|
2e444113c8 | ||
|
|
1412c268b5 | ||
|
|
0f89ddf779 | ||
| e73afcc366 | |||
| e8e338aeef | |||
| 0867d7916d | |||
| 80ad01a1b1 | |||
|
|
48647998a3 | ||
|
|
b46e331a53 | ||
|
|
1ab14ba8c9 | ||
|
|
b0985b6d60 | ||
|
|
18910638a1 | ||
|
|
ca0bab5071 | ||
|
|
90a0fee735 | ||
| 61833aacb4 | |||
| 8043e8c45d | |||
| 033b30cb78 | |||
| b0b09178e8 | |||
| 6aca0bc039 | |||
| 1cadbcffb4 | |||
| 1ea5048790 | |||
| 46bad101e0 | |||
| 91978de573 | |||
| 40dc385d7b | |||
| 771d8e7aca | |||
| 36897255a4 | |||
| bf5aee2f00 | |||
| b4fec83039 | |||
| bd6360177f | |||
| c4f8098894 | |||
| f1f78c69db | |||
| e8ce924b8a | |||
| 1b5f41319b | |||
| 239f2781b4 | |||
| 5d0cfb3c1b | |||
| c7d9465ba0 | |||
| 6d4a49208a | |||
| b21d9d8534 | |||
| 77dc711f12 | |||
| eef70107e4 | |||
| 460b54cdd8 | |||
| 530554e118 | |||
| 517bc1caf5 | |||
| 1e45dc212b | |||
| df513aca8e | |||
| b4ddd94c93 | |||
| 019090089b | |||
| 8cd1463fef | |||
| 86e0e07f0e | |||
| 42ff10d02f | |||
| 26a9ed4ad4 | |||
| 6a7291e677 | |||
| 2af0779e65 | |||
| 8b3ddec283 | |||
| 7950bab0c9 | |||
| 1d012f5ff1 | |||
| 95b7228778 | |||
| 7436c808e3 | |||
| 3718378545 | |||
| 0d2c42da82 | |||
| 18257be406 | |||
| 2028cbbe61 | |||
| e1a1ff4af3 | |||
| f60b1802d5 | |||
| 0a7af5c65b | |||
| 56f0f73278 | |||
| 2ab9306fff | |||
| 1ce4e9b3b8 | |||
| b8ea6e8436 | |||
| d9cb183b2a |
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
*.swp
|
||||||
39
Readme.md
39
Readme.md
@@ -2,6 +2,30 @@
|
|||||||
|
|
||||||
SPL tools is a collection of tools to assist in building the Streets People Love website.
|
SPL tools is a collection of tools to assist in building the Streets People Love website.
|
||||||
|
|
||||||
|
## csv-normaliser
|
||||||
|
|
||||||
|
A PHP script for converting the raw .csv files received from volunteers into a .csv file that can be used by the php-template tool. Ensures that ward names are capitalised correctly, and identifies which picture to use for each candidate.
|
||||||
|
|
||||||
|
## map-generator
|
||||||
|
|
||||||
|
A webpage built using Mapbox for showing the boundaries of the wards in a particular council. Also contains some JS scripts for generating a .jpg image of each council's map.
|
||||||
|
|
||||||
|
## php-template
|
||||||
|
|
||||||
|
A PHP script for generating the HTML to use in each council page on the Streets People Love website. Handles reading each of the relevant configuration files and exposing them in a format that can easily be consumed in a regular PHP template. This is used by the make-council-page.sh script.
|
||||||
|
|
||||||
|
## VEC Data
|
||||||
|
|
||||||
|
This folder contains the raw data downloaded from the VEC Map website.
|
||||||
|
|
||||||
|
## bulk-upload-media.sh
|
||||||
|
|
||||||
|
A script for uploading all images in the spl-data folder to WordPress. Uses the `upload-media.sh` script to handle uploading each image.
|
||||||
|
|
||||||
|
## copy-config.sh
|
||||||
|
|
||||||
|
A script for splitting up `council_names.json` into separate `config.json` files for each council. Shouldn't need to be used again.
|
||||||
|
|
||||||
## council_names.json
|
## council_names.json
|
||||||
|
|
||||||
Contains the name of each council, a "short" name, and the names of each ward in the council.
|
Contains the name of each council, a "short" name, and the names of each ward in the council.
|
||||||
@@ -11,7 +35,15 @@ The "short" named is created by taking the electorate name and removing the word
|
|||||||
The file can be generated using the `jq` tool and the VEC data:
|
The file can be generated using the `jq` tool and the VEC data:
|
||||||
|
|
||||||
```
|
```
|
||||||
jq '[.[] | {name: .electorateName, electorateId: .electorateId, shortName: .parentElectorateName | match("(.*?)(?:(?: Rural)?(?: City| Shire) Council)").captures[0].string, parentElectorateId: .parentElectorateId, councilName: .parentElectorateName }] | group_by(.parentElectorateId) | map({shortName: .[0].shortName, slug: .[0].shortName | ascii_downcase | split(" ") | join("-"), councilName: .[0].councilName, wardNames: . | map(.name) }) | sort_by(.shortName)' "VEC Data\wards.json" > council_names.json
|
jq '[.[] | {name: .electorateName, electorateId: .electorateId, shortName: .parentElectorateName | match("(.*?)(?:(?: Rural)?(?: City| Shire| Borough) Council)").captures[0].string, parentElectorateId: .parentElectorateId, councilName: .parentElectorateName }] | group_by(.parentElectorateId) | map({shortName: .[0].shortName, slug: .[0].shortName | ascii_downcase | split(" ") | join("-"), councilName: .[0].councilName, wardNames: . | map(.name) }) | sort_by(.shortName)' "VEC Data\wards.json" > council_names.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## lga-links-filter
|
||||||
|
|
||||||
|
This is a jq filter that will output a HTML anchor tag for each council. Can be used like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
jq -f -r .\lga-links-filter .\council_names.json
|
||||||
```
|
```
|
||||||
|
|
||||||
## make-council-pages.sh
|
## make-council-pages.sh
|
||||||
@@ -21,3 +53,8 @@ This is a bash script for creating a page in WordPress for each council.
|
|||||||
If a page for a council already exists, the page will be updated instead. The source of councils for this script is the "council_names.json" file.
|
If a page for a council already exists, the page will be updated instead. The source of councils for this script is the "council_names.json" file.
|
||||||
|
|
||||||
The script needs the [`jq`](https://jqlang.github.io/jq/), [`php`](https://www.php.net/) and [`wp`](https://wp-cli.org/) tools.
|
The script needs the [`jq`](https://jqlang.github.io/jq/), [`php`](https://www.php.net/) and [`wp`](https://wp-cli.org/) tools.
|
||||||
|
|
||||||
|
## upload-media.sh
|
||||||
|
|
||||||
|
Tries to upload a file to WordPress and stores the media ID and URL in a .json file next to the file. If the .json file is already present, the upload will be skipped.
|
||||||
|
|
||||||
|
|||||||
16
Updating SPL Website.md
Normal file
16
Updating SPL Website.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# Updating SPL Website
|
||||||
|
|
||||||
|
1) Download any candidate pictures provided by volunteer to the appropriate council folder in the `spl-data` repo
|
||||||
|
1) Ensure that the file name of each picture contains the candidate's full name (eg. for a candidate named `Joe Bloggs`, the file could be named `JoeBloggs.jpg` or `WardName_Joe_Bloggs.png`, etc)
|
||||||
|
1) Resize the pictures to 400px x 400px
|
||||||
|
1) Commit the pictures to the `spl-data` repo
|
||||||
|
1) Download CSV provided by a volunteer containing the candidate scores
|
||||||
|
1) Normalise the CSV using the "CSV Normaliser" tool (eg. `php csv-normaliser/main.php --input ~/Downloads/COUNCIL_NAME.csv --media ../spl-data/council-name --output ../spl-data/council-name/candidates.csv`)
|
||||||
|
1) Commit the candidates.csv file to the `spl-data` repo
|
||||||
|
1) Push commits in the `spl-data` repo to the Git server
|
||||||
|
1) SSH into the Streets People Love WordPress server
|
||||||
|
1) Pull the latest changes into the `spl-data` repo
|
||||||
|
1) Run the `bulk-media-upload.sh` script (eg `sudo ./bulk-media-upload.sh ../spl-data`)
|
||||||
|
1) Commit any added media .json files in the `spl-data` repo
|
||||||
|
1) Push commits in the `spl-data` repo to the Git server (use the `spl` git user)
|
||||||
|
1) Run the `make-council-pages.sh` script to regenerate all pages (eg `sudo ./make-council-pages.sh`) OR regenerate a single page (eg `sudo ./make-council-pages.sh "Council Name"`)
|
||||||
10
bulk-upload-media.sh
Normal file → Executable file
10
bulk-upload-media.sh
Normal file → Executable file
@@ -11,20 +11,18 @@ WP_FLAGS="--allow-root --path=/var/www/html"
|
|||||||
|
|
||||||
path="$1"
|
path="$1"
|
||||||
|
|
||||||
wp package install wp-cli/restful $WP_FLAGS
|
|
||||||
|
|
||||||
if test -d "$path"; then
|
if test -d "$path"; then
|
||||||
echo "Found $path, starting upload."
|
echo "Found $path, starting upload."
|
||||||
for file in "$path"/*.jpg; do
|
for file in "$path"/*/*.jpg; do
|
||||||
./upload-media.sh "$file"
|
./upload-media.sh "$file"
|
||||||
done
|
done
|
||||||
for file in "$path"/*.jpeg; do
|
for file in "$path"/*/*.jpeg; do
|
||||||
./upload-media.sh "$file"
|
./upload-media.sh "$file"
|
||||||
done
|
done
|
||||||
for file in "$path"/*.png; do
|
for file in "$path"/*/*.png; do
|
||||||
./upload-media.sh "$file"
|
./upload-media.sh "$file"
|
||||||
done
|
done
|
||||||
for file in "$path"/*.gif; do
|
for file in "$path"/*/*.gif; do
|
||||||
./upload-media.sh "$file"
|
./upload-media.sh "$file"
|
||||||
done
|
done
|
||||||
else
|
else
|
||||||
|
|||||||
0
copy-config.sh
Normal file → Executable file
0
copy-config.sh
Normal file → Executable file
@@ -786,6 +786,14 @@
|
|||||||
"Beaufort"
|
"Beaufort"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"shortName": "Queenscliffe",
|
||||||
|
"slug": "queenscliffe",
|
||||||
|
"councilName": "Queenscliffe Borough Council",
|
||||||
|
"wardNames": [
|
||||||
|
"Unsubdivided"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"shortName": "South Gippsland",
|
"shortName": "South Gippsland",
|
||||||
"slug": "south-gippsland",
|
"slug": "south-gippsland",
|
||||||
|
|||||||
145
csv-generic/gen-generic.php
Normal file
145
csv-generic/gen-generic.php
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once("parse_generic_csv.php");
|
||||||
|
|
||||||
|
$options = getopt("", ["generic-csv:", "config-files:"]);
|
||||||
|
|
||||||
|
if (isset($options['generic-csv'])) {
|
||||||
|
$generic_csv = $options['generic-csv'];
|
||||||
|
} else {
|
||||||
|
error_log("Error: Missing required option '--generic-csv'.");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($options['config-files'])) {
|
||||||
|
$config_files = $options['config-files'];
|
||||||
|
} else {
|
||||||
|
error_log("Error: Missing required option '--config-files'.");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$config_files = explode(" ", $config_files);
|
||||||
|
$candidate_data = parse_generic_csv($generic_csv);
|
||||||
|
|
||||||
|
$lga_list = [];
|
||||||
|
/* Generate dictionary of LGAs and Wards */
|
||||||
|
foreach ($config_files as $config_file) {
|
||||||
|
$config_string = file_get_contents($config_file);
|
||||||
|
if ($config_string !== FALSE) {
|
||||||
|
$config = json_decode($config_string, true);
|
||||||
|
} else {
|
||||||
|
error_log("Error opening config.json.");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
$config['config-file'] = $config_file;
|
||||||
|
$lga_list[] = $config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Match user typed LGA/Ward to our database */
|
||||||
|
match_lga($candidate_data, $lga_list);
|
||||||
|
|
||||||
|
/* Calculate score for candidate */
|
||||||
|
foreach ($candidate_data as $key => $candidate) {
|
||||||
|
$score = 0;
|
||||||
|
|
||||||
|
if ($candidate['q0'] === "Yes") $score++;
|
||||||
|
if ($candidate['q1'] === "Yes") $score++;
|
||||||
|
if ($candidate['q2'] === "Yes") $score++;
|
||||||
|
if ($candidate['q3'] === "Yes") $score++;
|
||||||
|
if ($candidate['q4'] === "Yes") $score++;
|
||||||
|
|
||||||
|
$candidate_data[$key]['Score'] = $score;
|
||||||
|
|
||||||
|
$candidate_data[$key]['Pledge'] = $candidate['q4'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$header = ["Candidate Name", "Rating", "Pledge", "Picture"];
|
||||||
|
|
||||||
|
/* Generate candidates-generic.csv */
|
||||||
|
foreach ($lga_list as $lga) {
|
||||||
|
$lga_candidates = array_filter($candidate_data, function ($candidate) use ($lga) {
|
||||||
|
return $candidate['match_division'] === $lga['slug'];
|
||||||
|
});
|
||||||
|
|
||||||
|
if (count($lga_candidates) === 0) continue;
|
||||||
|
|
||||||
|
remove_duplicates($lga_candidates);
|
||||||
|
|
||||||
|
$dir = dirname($lga['config-file']);
|
||||||
|
$dir_files = scandir($dir);
|
||||||
|
$output_file = $dir."/candidates-generic.csv";
|
||||||
|
$override_file = $dir."/candidates-override.csv";
|
||||||
|
|
||||||
|
if (($handle = fopen($output_file, "w")) === FALSE) {
|
||||||
|
error_log('Error opening output file');
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fputcsv($handle, $header) === FALSE) {
|
||||||
|
error_log('Error writing headers to output file');
|
||||||
|
exit(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
$lines = [];
|
||||||
|
foreach ($lga_candidates as $candidate) {
|
||||||
|
/* Add extension to photo hash */
|
||||||
|
if (strlen($candidate['Photo'])) {
|
||||||
|
foreach ($dir_files as $file) {
|
||||||
|
if (preg_match("/\.json$/", $file)) continue;
|
||||||
|
if (strstr($file, $candidate['Photo'])) {
|
||||||
|
$candidate['Photo'] = $file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$lines[] = [
|
||||||
|
$candidate['Name'],
|
||||||
|
$candidate['Score'],
|
||||||
|
$candidate['Pledge'],
|
||||||
|
$candidate['Photo'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Apply overrides if they exist */
|
||||||
|
$overrides = [];
|
||||||
|
if (file_exists($override_file)) {
|
||||||
|
if (($ovr_handle = fopen($override_file, "r")) !== FALSE) {
|
||||||
|
$headers = fgetcsv($ovr_handle);
|
||||||
|
while (($data = fgetcsv($ovr_handle)) !== FALSE) {
|
||||||
|
$override = [];
|
||||||
|
foreach ($headers as $key => $value) {
|
||||||
|
$override[$value] = $data[$key];
|
||||||
|
}
|
||||||
|
$overrides[] = $override;
|
||||||
|
}
|
||||||
|
fclose($ovr_handle);
|
||||||
|
} else {
|
||||||
|
error_log('Error opening overrides file');
|
||||||
|
exit(3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($overrides as $override) {
|
||||||
|
foreach ($lines as $line_key => $line) {
|
||||||
|
$match_index = array_search($override['Match Field'], $header);
|
||||||
|
$replace_index = array_search($override['Replace Field'], $header);
|
||||||
|
if ($line[$match_index] === $override['Match Value']) {
|
||||||
|
if ($replace_index !== false)
|
||||||
|
$lines[$line_key][$replace_index] = $override['Replace Value'];
|
||||||
|
else /* If 'Replace Field' is not matched - delete this entry */
|
||||||
|
$lines[$line_key]['Delete'] = 'y';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
if (isset($line['Delete'])) continue;
|
||||||
|
if (fputcsv($handle, $line) === FALSE) {
|
||||||
|
error_log('Error writing candidate to output file');
|
||||||
|
exit(3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fclose($handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
exit(0);
|
||||||
54
csv-generic/gen-image-map.php
Normal file
54
csv-generic/gen-image-map.php
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once("parse_generic_csv.php");
|
||||||
|
|
||||||
|
$options = getopt("", ["generic-csv:", "config-files:"]);
|
||||||
|
|
||||||
|
if (isset($options['generic-csv'])) {
|
||||||
|
$generic_csv = $options['generic-csv'];
|
||||||
|
} else {
|
||||||
|
error_log("Error: Missing required option '--generic-csv'.");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($options['config-files'])) {
|
||||||
|
$config_files = $options['config-files'];
|
||||||
|
} else {
|
||||||
|
error_log("Error: Missing required option '--config-files'.");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$config_files = explode(" ", $config_files);
|
||||||
|
$candidate_data = parse_generic_csv($generic_csv);
|
||||||
|
|
||||||
|
$lga_list = [];
|
||||||
|
/* Generate dictionary of LGAs and Wards */
|
||||||
|
foreach ($config_files as $config_file) {
|
||||||
|
$config_string = file_get_contents($config_file);
|
||||||
|
if ($config_string !== FALSE) {
|
||||||
|
$config = json_decode($config_string, true);
|
||||||
|
} else {
|
||||||
|
error_log("Error opening config.json.");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
$config['config-file'] = $config_file;
|
||||||
|
$lga_list[] = $config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Match user typed LGA/Ward to our database */
|
||||||
|
match_lga($candidate_data, $lga_list);
|
||||||
|
|
||||||
|
$image_map = [];
|
||||||
|
foreach ($candidate_data as $candidate) {
|
||||||
|
if (strlen($candidate['photo_url'])) {
|
||||||
|
$map['url'] = $candidate['photo_url'];
|
||||||
|
$map['match_division'] = $candidate['match_division'];
|
||||||
|
$image_map[$candidate['Photo']] = $map;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$json_data = json_encode($image_map);
|
||||||
|
|
||||||
|
print_r($json_data);
|
||||||
|
|
||||||
|
exit(0);
|
||||||
106
csv-generic/parse_generic_csv.php
Normal file
106
csv-generic/parse_generic_csv.php
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
function parse_generic_csv($generic_csv) {
|
||||||
|
$candidate_data = [];
|
||||||
|
if (($handle = fopen($generic_csv, "r")) !== FALSE) {
|
||||||
|
$headers = fgetcsv($handle);
|
||||||
|
while (($data = fgetcsv($handle)) !== FALSE) {
|
||||||
|
$candidate = [];
|
||||||
|
$question_no = 0;
|
||||||
|
$is_question = false;
|
||||||
|
foreach ($headers as $key => $value) {
|
||||||
|
/* Override key name for questions */
|
||||||
|
if (strstr($value, "candidate photo")) $value = "Photo";
|
||||||
|
if (strstr($value, "In which federal Division are")) continue;
|
||||||
|
if (strstr($value, "In which Federal Division are")) $value = "Division";
|
||||||
|
if (strstr($value, "Political Party")) $value = "Party";
|
||||||
|
if (strstr($value, "Protected bike lanes provide")) continue;
|
||||||
|
|
||||||
|
if ($value === "Photo") {
|
||||||
|
$candidate['photo_url'] = $data[$key];
|
||||||
|
$data[$key] = preg_filter("/.*id=/", "", $data[$key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($is_question) {
|
||||||
|
$candidate['q'.$question_no++] = $data[$key];
|
||||||
|
} else {
|
||||||
|
$candidate[$value] = $data[$key];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($value === "Party") {
|
||||||
|
$is_question = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$candidate_data[] = $candidate;
|
||||||
|
}
|
||||||
|
fclose($handle);
|
||||||
|
} else {
|
||||||
|
error_log('Error opening candidates file');
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $candidate_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
function match_lga(&$candidate_data, $lga_list) {
|
||||||
|
foreach ($candidate_data as &$candidate) {
|
||||||
|
/* Match user typed LGA/Ward to our database */
|
||||||
|
$max_score = 0;
|
||||||
|
$match_lga = null;
|
||||||
|
foreach ($lga_list as $lga) {
|
||||||
|
$aa = preg_split("/[^a-z]/", strtolower($candidate['Division']));
|
||||||
|
$bb = preg_split("/[^a-z]/", $lga['slug']);
|
||||||
|
|
||||||
|
$score_sum = 0;
|
||||||
|
foreach ($aa as $a) {
|
||||||
|
foreach ($bb as $b) {
|
||||||
|
similar_text($a, $b, $score);
|
||||||
|
if ($score > 70) $score_sum += $score;
|
||||||
|
else $score_sum -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($score_sum > $max_score) {
|
||||||
|
$max_score = $score_sum;
|
||||||
|
$match_lga = $lga;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
$max_score = 0;
|
||||||
|
foreach ($match_lga['wardNames'] as $ward) {
|
||||||
|
similar_text(strtolower($ward), strtolower($candidate['Ward']), $score);
|
||||||
|
if ($score >= $max_score) {
|
||||||
|
$max_score = $score;
|
||||||
|
$match_ward = $ward;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
if ($match_lga === null) {
|
||||||
|
$candidate['match_division'] = "no_match";
|
||||||
|
} else {
|
||||||
|
$candidate['match_division'] = $match_lga['slug'];
|
||||||
|
}
|
||||||
|
//$candidate['match_ward'] = $match_ward;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function remove_duplicates(&$candidate_data) {
|
||||||
|
$names = [];
|
||||||
|
$duplicates = [];
|
||||||
|
foreach ($candidate_data as $candidate_key => $candidate) {
|
||||||
|
/* If we've already had this name, remove the old entry */
|
||||||
|
foreach ($names as $name_key => $name) {
|
||||||
|
similar_text(strtolower($name), strtolower($candidate['Name']), $score);
|
||||||
|
if ($score > 90) {
|
||||||
|
$duplicates[] = $name_key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$names[$candidate_key] = $candidate['Name'];
|
||||||
|
}
|
||||||
|
$duplicates = array_unique($duplicates);
|
||||||
|
foreach ($duplicates as $duplicate) {
|
||||||
|
unset($candidate_data[$duplicate]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,11 +20,15 @@
|
|||||||
"label": "Hello Remote World",
|
"label": "Hello Remote World",
|
||||||
"onAutoForward": "notify"
|
"onAutoForward": "notify"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
// Use 'postCreateCommand' to run commands after the container is created.
|
// Use 'postCreateCommand' to run commands after the container is created.
|
||||||
// "postCreateCommand": "sudo chmod a+x \"$(pwd)\" && sudo rm -rf /var/www/html && sudo ln -s \"$(pwd)\" /var/www/html"
|
// "postCreateCommand": "sudo chmod a+x \"$(pwd)\" && sudo rm -rf /var/www/html && sudo ln -s \"$(pwd)\" /var/www/html"
|
||||||
|
|
||||||
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
||||||
// "remoteUser": "root"
|
// "remoteUser": "root"
|
||||||
|
|
||||||
|
"mounts": [
|
||||||
|
"source=${localWorkspaceFolder}/../../spl-data,target=/workspaces/spl-data,type=bind,consistency=consistent"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
2
csv-normaliser/.vscode/launch.json
vendored
2
csv-normaliser/.vscode/launch.json
vendored
@@ -6,7 +6,7 @@
|
|||||||
"type": "php",
|
"type": "php",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"program": "${workspaceFolder}/main.php",
|
"program": "${workspaceFolder}/main.php",
|
||||||
"args": [],
|
"args": ["--input", "${workspaceFolder}/test-input.csv", "--media", "${workspaceFolder}/test-media", "--output", "${workspaceFolder}/test-output.csv",],
|
||||||
"cwd": "${workspaceFolder}",
|
"cwd": "${workspaceFolder}",
|
||||||
"port": 9000
|
"port": 9000
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,69 +1,211 @@
|
|||||||
<?php
|
<?php
|
||||||
require_once "page_renderer.php";
|
$options = getopt("", ["folder:", "input:", "output:", "media:"]);
|
||||||
|
|
||||||
$options = getopt("", ["council-file:", "candidates-file:", "media-file:"]);
|
if (isset($options['folder'])) {
|
||||||
|
$folder = $options['folder'];
|
||||||
if (isset($options['council-file'])) {
|
if (is_dir($folder)) {
|
||||||
$councilFileContents = file_get_contents($options['council-file']);
|
$expectedInputFileNames = [];
|
||||||
} else {
|
$expectedInputFileNames[] = str_replace("-", " ", strtoupper(basename($folder))) . ".csv";
|
||||||
error_log("Error: Missing required option '--council-file'.");
|
$expectedInputFileNames[] = strtoupper(basename($folder)) . ".csv";
|
||||||
exit(1);
|
if (!isset($options['input'])) {
|
||||||
}
|
foreach ($expectedInputFileNames as $expectedInputFileName) {
|
||||||
|
$expectedInputFile = $folder . DIRECTORY_SEPARATOR . $expectedInputFileName;
|
||||||
$councilData = json_decode($councilFileContents, true);
|
if (is_file($expectedInputFile)) {
|
||||||
|
$options['input'] = $expectedInputFile;
|
||||||
// Check for decoding errors
|
}
|
||||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
}
|
||||||
error_log('Error decoding council file: ' . json_last_error_msg());
|
}
|
||||||
exit(1);
|
if (!isset($options['output'])) {
|
||||||
}
|
$options['output'] = $folder . DIRECTORY_SEPARATOR . "candidates.csv";
|
||||||
|
}
|
||||||
if (isset($options['candidates-file'])) {
|
if (!isset($options['media'])) {
|
||||||
$candidatesFile = $options['candidates-file'];
|
$options['media'] = $folder;
|
||||||
} else {
|
}
|
||||||
error_log("Error: Missing required option '--candidates-file'.");
|
} else {
|
||||||
exit(1);
|
error_log("Error: Specified folder is not valid.");
|
||||||
}
|
exit(1);
|
||||||
|
}
|
||||||
// Convert CSV into an array of dictionaries. Use the header as the key in the dictionary.
|
}
|
||||||
$candidateData = [];
|
|
||||||
if (($handle = fopen($candidatesFile, "r")) !== FALSE) {
|
if (isset($options['input'])) {
|
||||||
$headers = fgetcsv($handle);
|
$inputFile = $options['input'];
|
||||||
while (($data = fgetcsv($handle)) !== FALSE) {
|
} else {
|
||||||
$candidate = [];
|
error_log("Error: Missing required option '--input'.");
|
||||||
foreach ($headers as $key => $value) {
|
exit(1);
|
||||||
$candidate[$value] = $data[$key];
|
}
|
||||||
|
|
||||||
|
if (isset($options['output'])) {
|
||||||
|
$outputFile = $options['output'];
|
||||||
|
} else {
|
||||||
|
error_log("Error: Missing required option '--output'.");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($options['media'])) {
|
||||||
|
$mediaFolder = $options['media'];
|
||||||
|
} else {
|
||||||
|
error_log("Error: Missing required option '--media'.");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$mediaFiles = scandir($mediaFolder);
|
||||||
|
if ($mediaFiles === FALSE) {
|
||||||
|
error_log("Failed to list files in media folder");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$candidates = [];
|
||||||
|
if (($handle = fopen($inputFile, "r")) !== FALSE) {
|
||||||
|
$currentWard = null;
|
||||||
|
$currentLine = 0;
|
||||||
|
while (($data = fgetcsv($handle)) !== FALSE) {
|
||||||
|
$currentLine++;
|
||||||
|
//echo var_dump($data);
|
||||||
|
if ($data[0] == "Ward") {
|
||||||
|
// CSV contains ward names in uppercase, convert them to a more readable form
|
||||||
|
$currentWard = ucwords(strtolower($data[1]));
|
||||||
|
|
||||||
|
// Handle some special cases where the above logic doesn't match the expected names
|
||||||
|
// Note that we cannot just convert every letter after a '-' character to uppercase
|
||||||
|
// because there are some ward names like "Bulleke-bek"
|
||||||
|
if ($currentWard == "Warrk-warrk") {
|
||||||
|
$currentWard = "Warrk-Warrk";
|
||||||
|
}
|
||||||
|
if ($currentWard == "Djirri-djirri") {
|
||||||
|
$currentWard = "Djirri-Djirri";
|
||||||
|
}
|
||||||
|
if ($currentWard == "Coastal-promontory") {
|
||||||
|
$currentWard = "Coastal-Promontory";
|
||||||
|
}
|
||||||
|
if ($currentWard == "Alpine Shire") $currentWard = "Unsubdivided";
|
||||||
|
if ($currentWard == "Ararat Rural City") $currentWard = "Unsubdivided";
|
||||||
|
if ($currentWard == "Benalla Rural City") $currentWard = "Unsubdivided";
|
||||||
|
if ($currentWard == "Campaspe Shire") $currentWard = "Unsubdivided";
|
||||||
|
if ($currentWard == "Colac Otway Shire") $currentWard = "Unsubdivided";
|
||||||
|
if ($currentWard == "East Gippsland Shire") $currentWard = "Unsubdivided";
|
||||||
|
if ($currentWard == "Gannawarra Shire") $currentWard = "Unsubdivided";
|
||||||
|
if ($currentWard == "Glenelg Shire") $currentWard = "Unsubdivided";
|
||||||
|
if ($currentWard == "Golden Plains Shire") $currentWard = "Unsubdivided";
|
||||||
|
if ($currentWard == "Hepburn Shire") $currentWard = "Unsubdivided";
|
||||||
|
if ($currentWard == "Indigo Shire") $currentWard = "Unsubdivided";
|
||||||
|
if ($currentWard == "Mansfield Shire") $currentWard = "Unsubdivided";
|
||||||
|
if ($currentWard == "Melbourne City") $currentWard = "Unsubdivided";
|
||||||
|
if ($currentWard == "Moira Shire") $currentWard = "Unsubdivided";
|
||||||
|
if ($currentWard == "Moorabool Shire") $currentWard = "Unsubdivided";
|
||||||
|
if ($currentWard == "Moyne Shire") $currentWard = "Unsubdivided";
|
||||||
|
if ($currentWard == "Queenscliffe Borough") $currentWard = "Unsubdivided";
|
||||||
|
if ($currentWard == "Southern Grampians Shire") $currentWard = "Unsubdivided";
|
||||||
|
if ($currentWard == "Strathbogie Shire") $currentWard = "Unsubdivided";
|
||||||
|
if ($currentWard == "Swan Hill Rural City") $currentWard = "Unsubdivided";
|
||||||
|
if ($currentWard == "Towong Shire") $currentWard = "Unsubdivided";
|
||||||
|
if ($currentWard == "West Wimmera Shire") $currentWard = "Unsubdivided";
|
||||||
|
}
|
||||||
|
if (trim($data[0]) == "Candidate" || trim($data[0]) == "") {
|
||||||
|
if ($currentWard == null) {
|
||||||
|
error_log("No ward found, skipping data on line " . $currentLine);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$candidateName = trim($data[1]);
|
||||||
|
|
||||||
|
if ($candidateName == "example name" || $candidateName == "") {
|
||||||
|
error_log("Skipping line ". $currentLine);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
//print("Adding candidate " . $candidateName . " to ". $currentWard . "\n");
|
||||||
|
|
||||||
|
$name_split = array_values(array_filter(explode(" ", str_replace(",", "", str_replace("'", "_", $data[1]))), function($value) { return !is_null($value) && $value !== ''; }));
|
||||||
|
|
||||||
|
$name_patterns = [
|
||||||
|
".*" . implode(".*", $name_split) . ".*",
|
||||||
|
".*" . implode(".*", array_reverse($name_split)) . ".*"
|
||||||
|
];
|
||||||
|
|
||||||
|
$first_name = $name_split[0];
|
||||||
|
$last_name = $name_split[array_key_last($name_split)];
|
||||||
|
|
||||||
|
if (strlen($first_name) > 3) {
|
||||||
|
$name_patterns[] = "^" . $first_name . ".*";
|
||||||
|
}
|
||||||
|
if ($last_name != $first_name && strlen($last_name)) {
|
||||||
|
$name_patterns[] = "^" . $last_name . ".*";
|
||||||
|
}
|
||||||
|
|
||||||
|
$regex_groups = array_map(function($x) { return "(?:" . $x . ")"; }, $name_patterns);
|
||||||
|
|
||||||
|
$regex_pattern = "/" . implode("|", $regex_groups) . "/i";
|
||||||
|
|
||||||
|
$picture = "";
|
||||||
|
foreach ($mediaFiles as $mediaFile) {
|
||||||
|
if ($mediaFile == ".") continue;
|
||||||
|
if ($mediaFile == "..") continue;
|
||||||
|
if (preg_match($regex_pattern, $mediaFile)) {
|
||||||
|
$picture = $mediaFile;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($picture === "") {
|
||||||
|
print("\033[31mFailed to identify picture for " . $candidateName . "\033[0m\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
$rating = $data[2];
|
||||||
|
if ($rating == "score" || $rating == "") {
|
||||||
|
// Don't include candidates who haven't been given a score
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$rating = (int)$rating;
|
||||||
|
|
||||||
|
array_push(
|
||||||
|
$candidates,
|
||||||
|
[
|
||||||
|
"Ward" => $currentWard,
|
||||||
|
"Candidate Name" => $candidateName,
|
||||||
|
"Rating" => $rating,
|
||||||
|
"Picture" => $picture
|
||||||
|
]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
$candidateData[] = $candidate;
|
|
||||||
}
|
}
|
||||||
fclose($handle);
|
fclose($handle);
|
||||||
} else {
|
} else {
|
||||||
error_log('Error opening candidates file');
|
error_log('Error opening input file');
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
$candidateData = array_filter($candidateData, function ($candidate) use ($councilData) {
|
if (empty($candidates)) {
|
||||||
return isset($candidate["Council"]) && $candidate["Council"] === $councilData['shortName'];
|
error_log("Failed to find any candidates");
|
||||||
});
|
|
||||||
|
|
||||||
if (empty($candidateData)) {
|
|
||||||
error_log("Failed to load any candidates for " . $councilData['shortName']);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($options['media-file'])) {
|
|
||||||
$mediaFileContents = file_get_contents($options['media-file']);
|
|
||||||
} else {
|
|
||||||
error_log("Error: Missing required option '--media-file'.");
|
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
$mediaData = json_decode($mediaFileContents, true);
|
|
||||||
|
|
||||||
$renderer = new SPLPageRenderer();
|
|
||||||
$pageContent = $renderer->renderCouncilPage($councilData, $candidateData, $mediaData);
|
|
||||||
if ($pageContent === null) {
|
|
||||||
exit(2);
|
exit(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
echo $pageContent;
|
if (($handle = fopen($outputFile, "w")) !== FALSE) {
|
||||||
|
$headers = array(
|
||||||
|
"Ward",
|
||||||
|
"Candidate Name",
|
||||||
|
"Rating",
|
||||||
|
"Picture"
|
||||||
|
);
|
||||||
|
if (fputcsv($handle, $headers) === FALSE) {
|
||||||
|
error_log('Error writing headers to output file');
|
||||||
|
exit(3);
|
||||||
|
}
|
||||||
|
foreach ($candidates as $candidate) {
|
||||||
|
$fields = array(
|
||||||
|
$candidate["Ward"],
|
||||||
|
$candidate["Candidate Name"],
|
||||||
|
$candidate["Rating"],
|
||||||
|
$candidate["Picture"]
|
||||||
|
);
|
||||||
|
if (fputcsv($handle, $fields) === FALSE) {
|
||||||
|
error_log('Error writing candidate to output file');
|
||||||
|
exit(3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
error_log('Error opening output file');
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
print("Data written to " . $outputFile . "\n");
|
||||||
|
|
||||||
exit(0);
|
exit(0);
|
||||||
|
|||||||
30
csv-normaliser/test-input.csv
Normal file
30
csv-normaliser/test-input.csv
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
Data for TEST,,,,
|
||||||
|
Password,,Obtain a password to upload this file from the webmaster of https://streetspeoplelove.org/,,
|
||||||
|
Your phone number,,"Not for publication, campaign use only",,
|
||||||
|
Your email,,"Not for publication, campaign use only",,
|
||||||
|
Ward,BABABI DJINANANG,,,
|
||||||
|
Candidate,Joe Blogs,4,Party or Independent,"replace ""example name"" with a candidate's name. Score should be an integer from 0 to your highest score. Make more of these rows as required. "
|
||||||
|
Candidate,Jane Doe,Good,Party or Independent,"replace ""example name"" with a candidate's name. Score should be an integer from 0 to your highest score. Make more of these rows as required. "
|
||||||
|
Ward,BOX FOREST,,,
|
||||||
|
Candidate,First Last,,Party or Independent,"replace ""example name"" with a candidate's name. Score should be an integer from 0 to your highest score. Make more of these rows as required. "
|
||||||
|
Candidate, example name,score,Party or Independent,"replace ""example name"" with a candidate's name. Score should be an integer from 0 to your highest score. Make more of these rows as required. "
|
||||||
|
Ward,BRUNSWICK WEST,,,
|
||||||
|
Candidate, example name,score,Party or Independent,"replace ""example name"" with a candidate's name. Score should be an integer from 0 to your highest score. Make more of these rows as required. "
|
||||||
|
Ward,BULLEKE-BEK,,,
|
||||||
|
Candidate, example name,score,Party or Independent,"replace ""example name"" with a candidate's name. Score should be an integer from 0 to your highest score. Make more of these rows as required. "
|
||||||
|
Ward,DJIRRI-DJIRRI,,,
|
||||||
|
Candidate, example name,score,Party or Independent,"replace ""example name"" with a candidate's name. Score should be an integer from 0 to your highest score. Make more of these rows as required. "
|
||||||
|
Ward,HARMONY PARK,,,
|
||||||
|
Candidate, example name,score,Party or Independent,"replace ""example name"" with a candidate's name. Score should be an integer from 0 to your highest score. Make more of these rows as required. "
|
||||||
|
Ward,PASCOE VALE SOUTH,,,
|
||||||
|
Candidate, example name,score,Party or Independent,"replace ""example name"" with a candidate's name. Score should be an integer from 0 to your highest score. Make more of these rows as required. "
|
||||||
|
Ward,PENTRIDGE,,,
|
||||||
|
Candidate, example name,score,Party or Independent,"replace ""example name"" with a candidate's name. Score should be an integer from 0 to your highest score. Make more of these rows as required. "
|
||||||
|
Ward,RANDAZZO,,,
|
||||||
|
Candidate, example name,score,Party or Independent,"replace ""example name"" with a candidate's name. Score should be an integer from 0 to your highest score. Make more of these rows as required. "
|
||||||
|
Ward,WARRK-WARRK,,,
|
||||||
|
Candidate, example name,score,Party or Independent,"replace ""example name"" with a candidate's name. Score should be an integer from 0 to your highest score. Make more of these rows as required. "
|
||||||
|
Ward,WESTBREEN,,,
|
||||||
|
Candidate, example name,score,Party or Independent,"replace ""example name"" with a candidate's name. Score should be an integer from 0 to your highest score. Make more of these rows as required. "
|
||||||
|
,,,,
|
||||||
|
Get help filling in this form email webmaster@streetspeoplelove.org,,,,
|
||||||
|
0
csv-normaliser/test-media/JaneDoe square.png
Normal file
0
csv-normaliser/test-media/JaneDoe square.png
Normal file
0
csv-normaliser/test-media/Joe Blogs.jpg
Normal file
0
csv-normaliser/test-media/Joe Blogs.jpg
Normal file
0
csv-normaliser/test-media/LAST_FIRST.jpeg
Normal file
0
csv-normaliser/test-media/LAST_FIRST.jpeg
Normal file
4
csv-normaliser/test-output.csv
Normal file
4
csv-normaliser/test-output.csv
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
Ward,"Candidate Name",Rating,Picture
|
||||||
|
"Bababi Djinanang","Joe Blogs",4,"Joe Blogs.jpg"
|
||||||
|
"Bababi Djinanang","Jane Doe",Good,"JaneDoe square.png"
|
||||||
|
"Box Forest","First Last",,LAST_FIRST.jpeg
|
||||||
|
71
get-generic.sh
Executable file
71
get-generic.sh
Executable file
@@ -0,0 +1,71 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
#rclone sync --progress bikewest:spl_generic_survey_2024 $DATA_LOC/google-data
|
||||||
|
|
||||||
|
GENERIC_SURVEY=../generic-survey/responses.csv
|
||||||
|
IMAGES=../generic-survey/images
|
||||||
|
|
||||||
|
DATA_PATH="../spl-data/federal_2025"
|
||||||
|
|
||||||
|
echo "Fetching latest responses to generic survey."
|
||||||
|
rm -f $GENERIC_SURVEY # Force re-fetch
|
||||||
|
rclone -v copyto --drive-export-formats csv 'bikewest:spl_generic_survey_federal_2025/Streets People Love Federal Election candidate pledge (Responses).csv' $GENERIC_SURVEY
|
||||||
|
|
||||||
|
config_files=()
|
||||||
|
for folder in "$DATA_PATH"/*; do
|
||||||
|
if test -f "$folder"/config.json; then
|
||||||
|
config_files+=("$folder"/config.json)
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
image_map=$(php csv-generic/gen-image-map.php --generic-csv $GENERIC_SURVEY --config-files "${config_files[*]}")
|
||||||
|
|
||||||
|
img_list=()
|
||||||
|
for key in $(jq -r 'keys[]' <<< $image_map) ; do
|
||||||
|
if [ -f $IMAGES/$key ] ; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
img_list+=($key)
|
||||||
|
img_list+=($IMAGES/$key)
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ ${#img_list[*]} -gt 0 ] ; then
|
||||||
|
echo "Downloading $((${#img_list[*]}/2)) image(s)..."
|
||||||
|
rclone -v backend copyid bikewest: ${img_list[*]}
|
||||||
|
fi
|
||||||
|
|
||||||
|
for key in $(jq -r 'keys[]' <<< $image_map) ; do
|
||||||
|
format=$(identify $IMAGES/$key | awk '{print $2}')
|
||||||
|
|
||||||
|
case $format in
|
||||||
|
PNG ) suffix=.png ;;
|
||||||
|
JPEG ) suffix=.jpg ;;
|
||||||
|
HEIC ) suffix=.jpg ;;
|
||||||
|
WEBP ) suffix=.png ;;
|
||||||
|
*)
|
||||||
|
echo "Error: Unknown image format: $IMAGES/$key"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
lga=$(jq -r ".[\"$key\"][\"match_division\"]" <<< $image_map)
|
||||||
|
|
||||||
|
if [ ! -d $"$DATA_PATH/$lga" ] ; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
dst="$DATA_PATH/$lga/$key$suffix"
|
||||||
|
|
||||||
|
if [ -f $dst ] ; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Resizing $dst"
|
||||||
|
if command -v magick >/dev/null 2>&1; then
|
||||||
|
magick $IMAGES/$key -resize 400x400 $dst
|
||||||
|
else
|
||||||
|
convert $IMAGES/$key -resize 400x400 $dst
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Generating candidates-generic.csv files."
|
||||||
|
php csv-generic/gen-generic.php --generic-csv $GENERIC_SURVEY --config-files "${config_files[*]}"
|
||||||
1
lga-links-filter
Normal file
1
lga-links-filter
Normal file
@@ -0,0 +1 @@
|
|||||||
|
. | map("<a href=\"" + .slug + "\">" + .councilName + "</a>") | .[]
|
||||||
57
lga-page.php
Normal file
57
lga-page.php
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
$options = getopt("", ["candidates-files:", "lga-files:"]);
|
||||||
|
|
||||||
|
if (isset($options['candidates-files'])) {
|
||||||
|
$candidates_files = explode(" ", $options['candidates-files']);
|
||||||
|
} else {
|
||||||
|
error_log("Error: Missing required option '--candidates-files'.");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($options['lga-files'])) {
|
||||||
|
$lga_files = explode(" ", $options['lga-files']);
|
||||||
|
} else {
|
||||||
|
error_log("Error: Missing required option '--lga-files'.");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$lgas_with_data = [];
|
||||||
|
foreach ($candidates_files as $file) {
|
||||||
|
$lgas_with_data[] = basename(dirname($file));
|
||||||
|
}
|
||||||
|
|
||||||
|
print('<!-- wp:paragraph -->' . "\n");
|
||||||
|
print('<p>The Streets People Love campaign has created scorecards for candidates in the 2025 federal election. Scorecards have been generated based on a candidate\'s engagement with the Streets People Love campaign, their commitment to our pledge, their responses to a survey and input from campaign members located in the division in which they are running.</p>' . "\n");
|
||||||
|
print('<!-- /wp:paragraph -->' . "\n");
|
||||||
|
print('<!-- wp:paragraph -->' . "\n");
|
||||||
|
print('<p>Can\'t see a candidate you know is running? Candidates who don\'t take our survey won\'t appear on this page. Feel free to send your local candidates the <a href="https://forms.gle/CeGbJF11SUkATjfN8">Streets People Love Pledge and Survey</a> and let them know it\'s important to local residents that they do take part, so that we can vote for those who want to build the streets people love.</p>' . "\n");
|
||||||
|
print('<!-- /wp:paragraph -->' . "\n");
|
||||||
|
|
||||||
|
print('<!-- wp:list --> <ul class="wp-block-list">' . "\n");
|
||||||
|
|
||||||
|
foreach ($lga_files as $config_file) {
|
||||||
|
$config_string = file_get_contents($config_file);
|
||||||
|
|
||||||
|
if ($config_string !== FALSE) {
|
||||||
|
$config = json_decode($config_string, true);
|
||||||
|
} else {
|
||||||
|
error_log("Error opening config.json.");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (array_search($config['slug'], $lgas_with_data) === false)
|
||||||
|
$font_weight = 300;
|
||||||
|
else
|
||||||
|
$font_weight = 700;
|
||||||
|
|
||||||
|
print('<!-- wp:list-item {"style":{"typography":{"fontStyle":"normal","fontWeight":"');
|
||||||
|
print($font_weight . '"}}} -->' . "\n");
|
||||||
|
print('<li style="font-style:normal;font-weight:' . $font_weight . '">');
|
||||||
|
print('<a href="' . $config['slug'] . '">' . $config['divisionName'] . "</a></li>\n");
|
||||||
|
print('<!-- /wp:list-item -->' . "\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
print('</ul> <!-- /wp:list -->');
|
||||||
|
|
||||||
|
exit(0);
|
||||||
23
make-council-pages.sh
Normal file → Executable file
23
make-council-pages.sh
Normal file → Executable file
@@ -4,10 +4,10 @@
|
|||||||
|
|
||||||
# The folder containing data for each council.
|
# The folder containing data for each council.
|
||||||
# Includes the list of candidates and any media.
|
# Includes the list of candidates and any media.
|
||||||
DATA_PATH="../spl-data"
|
DATA_PATH="../spl-data/federal_2025"
|
||||||
|
|
||||||
# Controls the flags that are passed to every usage of the wp command.
|
# Controls the flags that are passed to every usage of the wp command.
|
||||||
WP_FLAGS="--allow-root --path=/var/www/html"
|
#WP_FLAGS="--allow-root --path=/var/www/html"
|
||||||
|
|
||||||
function create_or_update_page() {
|
function create_or_update_page() {
|
||||||
local council_block="$1"
|
local council_block="$1"
|
||||||
@@ -23,7 +23,7 @@ function create_or_update_page() {
|
|||||||
media_inputs+=("$file")
|
media_inputs+=("$file")
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
for file in "$DATA_PATH"/*.{jpeg,jpg,png,gif}.json; do
|
for file in "$DATA_PATH"/../*.{jpeg,jpg,png,gif}.json; do
|
||||||
if test -f "$file"; then
|
if test -f "$file"; then
|
||||||
media_inputs+=("$file")
|
media_inputs+=("$file")
|
||||||
fi
|
fi
|
||||||
@@ -31,7 +31,20 @@ function create_or_update_page() {
|
|||||||
|
|
||||||
jq -n '[inputs | { (input_filename | sub("\\.json$"; "") | sub("^.+/"; "")): . }] | reduce .[] as $item ({}; . + $item)' "${media_inputs[@]}" > "$DATA_PATH"/$slug/media.json
|
jq -n '[inputs | { (input_filename | sub("\\.json$"; "") | sub("^.+/"; "")): . }] | reduce .[] as $item ({}; . + $item)' "${media_inputs[@]}" > "$DATA_PATH"/$slug/media.json
|
||||||
|
|
||||||
content=$(echo "$council_block" | jq -c | php php-template/main.php --council-file "php://stdin" --candidates-file "$DATA_PATH"/$slug/candidates.csv --media-file "$DATA_PATH"/$slug/media.json )
|
# Community groups get priority
|
||||||
|
if test -f "$DATA_PATH"/$slug/candidates.csv; then
|
||||||
|
candidates_file="$DATA_PATH"/$slug/candidates.csv
|
||||||
|
else
|
||||||
|
candidates_file="$DATA_PATH"/$slug/candidates-generic.csv
|
||||||
|
fi
|
||||||
|
|
||||||
|
if test -f "$DATA_PATH"/$slug/candidates-elected.csv; then
|
||||||
|
candidates_elected_file="$DATA_PATH"/$slug/candidates-elected.csv
|
||||||
|
echo "Found candidates-elected.csv"
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
content=$(echo "$council_block" | jq -c | php php-template/main.php --council-file "php://stdin" --candidates-file "$candidates_file" --media-file "$DATA_PATH"/$slug/media.json --candidates-elected-file "$candidates_elected_file" )
|
||||||
|
|
||||||
if [ $? -eq 0 ]; then
|
if [ $? -eq 0 ]; then
|
||||||
|
|
||||||
@@ -40,7 +53,7 @@ function create_or_update_page() {
|
|||||||
echo "$content" | wp post update "$page_id" --post_content="$content" $WP_FLAGS -
|
echo "$content" | wp post update "$page_id" --post_content="$content" $WP_FLAGS -
|
||||||
else
|
else
|
||||||
echo "Create page $short_name"
|
echo "Create page $short_name"
|
||||||
echo "$content" | wp post create --post_type=page --post_title="$short_name" --post_status=publish $WP_FLAGS -
|
echo "$content" | wp post create --post_type=page --post_title="$short_name" --post_status=draft $WP_FLAGS -
|
||||||
fi
|
fi
|
||||||
|
|
||||||
else
|
else
|
||||||
|
|||||||
25
make-lga-page.sh
Executable file
25
make-lga-page.sh
Executable file
@@ -0,0 +1,25 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# This script uses the jq, wp, and php commands, make sure they are installed before running this script.
|
||||||
|
|
||||||
|
# The folder containing data for each council.
|
||||||
|
# Includes the list of candidates and any media.
|
||||||
|
DATA_PATH="../spl-data/federal_2025"
|
||||||
|
|
||||||
|
# Iterate over folders in data path
|
||||||
|
candidates_files=()
|
||||||
|
for folder in "$DATA_PATH"/*; do
|
||||||
|
if test -f "$folder"/candidates-generic.csv; then
|
||||||
|
candidates_files+=("$folder"/candidates-generic.csv)
|
||||||
|
elif test -f "$folder"/candidates.csv; then
|
||||||
|
candidates_files+=("$folder"/candidates.csv)
|
||||||
|
fi
|
||||||
|
if test -f "$folder"/config.json; then
|
||||||
|
lga_files+=("$folder"/config.json)
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
content=$(php lga-page.php --candidates-files "${candidates_files[*]}" \
|
||||||
|
--lga-files "${lga_files[*]}")
|
||||||
|
|
||||||
|
echo "$content" | wp post update 245813 -
|
||||||
25
make-pledge-page.sh
Executable file
25
make-pledge-page.sh
Executable file
@@ -0,0 +1,25 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# This script uses the jq, wp, and php commands, make sure they are installed before running this script.
|
||||||
|
|
||||||
|
# The folder containing data for each council.
|
||||||
|
# Includes the list of candidates and any media.
|
||||||
|
DATA_PATH="../spl-data/federal_2025"
|
||||||
|
DEFAULT_IMAGE="../spl-data/default.png.json"
|
||||||
|
|
||||||
|
# Iterate over folders in data path
|
||||||
|
candidates_files=()
|
||||||
|
for folder in "$DATA_PATH"/*; do
|
||||||
|
if test -f "$folder"/candidates-generic.csv; then
|
||||||
|
candidates_files+=("$folder"/candidates-generic.csv)
|
||||||
|
fi
|
||||||
|
# Community groups get priority
|
||||||
|
if test -f "$folder"/candidates.csv; then
|
||||||
|
candidates_files+=("$folder"/candidates.csv)
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
content=$(php pledge-update/pledge-page.php --candidates-files "${candidates_files[*]}" \
|
||||||
|
--default-image $DEFAULT_IMAGE)
|
||||||
|
|
||||||
|
echo "$content" | wp post update 245816 -
|
||||||
1
map-generator/.gitignore
vendored
Normal file
1
map-generator/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
node_modules
|
||||||
52
map-generator/capture-all-maps.js
Normal file
52
map-generator/capture-all-maps.js
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const { exec } = require('child_process');
|
||||||
|
|
||||||
|
var widthArgument = process.argv.at(2);
|
||||||
|
if (!widthArgument) {
|
||||||
|
console.log("Defaulting to width of 1080")
|
||||||
|
width = 1080;
|
||||||
|
} else {
|
||||||
|
width = parseInt(widthArgument);
|
||||||
|
if (isNaN(width)) {
|
||||||
|
console.log("Invalid width provided");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var heightArgument = process.argv.at(3);
|
||||||
|
if (!heightArgument) {
|
||||||
|
console.log("Defaulting to height of 720")
|
||||||
|
height = 720;
|
||||||
|
} else {
|
||||||
|
height = parseInt(heightArgument);
|
||||||
|
if (isNaN(height)) {
|
||||||
|
console.log("Invalid height provided");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var dataPathArgument = process.argv.at(4);
|
||||||
|
if (!dataPathArgument) {
|
||||||
|
console.log("Invalid data path provided");
|
||||||
|
exit(1);
|
||||||
|
} else {
|
||||||
|
dataPath = dataPathArgument;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sleep(ms) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, ms);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
var councils = JSON.parse(fs.readFileSync('../council_names.json', 'utf8'));
|
||||||
|
|
||||||
|
for (const council of councils) {
|
||||||
|
console.log("Generating map for " + council.slug + "...");
|
||||||
|
exec("node ./capture-map.js " + council.slug + " " + width + " " + height + " " + dataPath + "/" + council.slug + "/map.jpg");
|
||||||
|
|
||||||
|
// Need to slow down requests to avoid overloading the system...
|
||||||
|
await sleep(1000);
|
||||||
|
}
|
||||||
|
})();
|
||||||
61
map-generator/capture-map.js
Normal file
61
map-generator/capture-map.js
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
const path = require('path');
|
||||||
|
const puppeteer = require('puppeteer-core');
|
||||||
|
|
||||||
|
const councilName = process.argv.at(2);
|
||||||
|
if (!councilName) {
|
||||||
|
console.log("No council name provided");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
var widthArgument = process.argv.at(3);
|
||||||
|
if (!widthArgument) {
|
||||||
|
console.log("Defaulting to width of 1080")
|
||||||
|
width = 1080;
|
||||||
|
} else {
|
||||||
|
width = parseInt(widthArgument);
|
||||||
|
if (isNaN(width)) {
|
||||||
|
console.log("Invalid width provided");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var heightArgument = process.argv.at(4);
|
||||||
|
if (!heightArgument) {
|
||||||
|
console.log("Defaulting to height of 720")
|
||||||
|
height = 720;
|
||||||
|
} else {
|
||||||
|
height = parseInt(heightArgument);
|
||||||
|
if (isNaN(height)) {
|
||||||
|
console.log("Invalid height provided");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var outputPathArgument = process.argv.at(5);
|
||||||
|
if (!outputPathArgument) {
|
||||||
|
outputPath = councilName + '.jpg'
|
||||||
|
console.log("Defaulting to path of '" + path + "'")
|
||||||
|
} else {
|
||||||
|
outputPath = outputPathArgument;
|
||||||
|
}
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
// Launch the browser and open a new blank page
|
||||||
|
const browser = await puppeteer.launch({channel: 'chrome'});
|
||||||
|
const page = await browser.newPage();
|
||||||
|
|
||||||
|
// Set screen size.
|
||||||
|
await page.setViewport({width: width, height: height});
|
||||||
|
|
||||||
|
// Navigate the page to a URL. Wait until network activity is finished to ensure map is loaded before taking screenshot.
|
||||||
|
await page.goto('http://localhost:8000/?council=' + encodeURIComponent(councilName), {
|
||||||
|
waitUntil: 'networkidle2',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Capture screenshot
|
||||||
|
await page.screenshot({
|
||||||
|
path: outputPath
|
||||||
|
});
|
||||||
|
|
||||||
|
await browser.close();
|
||||||
|
})();
|
||||||
BIN
map-generator/circle.png
Normal file
BIN
map-generator/circle.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 354 B |
4
map-generator/circle.svg
Normal file
4
map-generator/circle.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg version="1.1" id="circle" xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 15 15">
|
||||||
|
<path d="M14,7.5c0,3.5899-2.9101,6.5-6.5,6.5S1,11.0899,1,7.5S3.9101,1,7.5,1S14,3.9101,14,7.5z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 254 B |
106
map-generator/dist/main.js
vendored
Normal file
106
map-generator/dist/main.js
vendored
Normal file
File diff suppressed because one or more lines are too long
21
map-generator/index.html
Normal file
21
map-generator/index.html
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Council Wards 2024</title>
|
||||||
|
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no">
|
||||||
|
<link href="https://api.mapbox.com/mapbox-gl-js/v3.6.0/mapbox-gl.css" rel="stylesheet">
|
||||||
|
<script src="https://api.mapbox.com/mapbox-gl-js/v3.6.0/mapbox-gl.js"></script>
|
||||||
|
<style>
|
||||||
|
body { margin: 0; padding: 0; }
|
||||||
|
#map { position: absolute; top: 0; bottom: 0; width: 100%; }
|
||||||
|
.mapbox-improve-map { display: none; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="map"></div>
|
||||||
|
|
||||||
|
<script src="dist/main.js"></script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
948
map-generator/package-lock.json
generated
Normal file
948
map-generator/package-lock.json
generated
Normal file
@@ -0,0 +1,948 @@
|
|||||||
|
{
|
||||||
|
"name": "map-generator",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"dependencies": {
|
||||||
|
"child_process": "^1.0.2",
|
||||||
|
"fs": "^0.0.1-security",
|
||||||
|
"polylabel": "^2.0.1",
|
||||||
|
"puppeteer-core": "^23.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@puppeteer/browsers": {
|
||||||
|
"version": "2.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.3.1.tgz",
|
||||||
|
"integrity": "sha512-uK7o3hHkK+naEobMSJ+2ySYyXtQkBxIH8Gn4MK9ciePjNV+Pf+PgY/W7iPzn2MTjl3stcYB5AlcTmPYw7AXDwA==",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "^4.3.6",
|
||||||
|
"extract-zip": "^2.0.1",
|
||||||
|
"progress": "^2.0.3",
|
||||||
|
"proxy-agent": "^6.4.0",
|
||||||
|
"semver": "^7.6.3",
|
||||||
|
"tar-fs": "^3.0.6",
|
||||||
|
"unbzip2-stream": "^1.4.3",
|
||||||
|
"yargs": "^17.7.2"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"browsers": "lib/cjs/main-cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tootallnate/quickjs-emscripten": {
|
||||||
|
"version": "0.23.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz",
|
||||||
|
"integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA=="
|
||||||
|
},
|
||||||
|
"node_modules/@types/node": {
|
||||||
|
"version": "22.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.4.0.tgz",
|
||||||
|
"integrity": "sha512-49AbMDwYUz7EXxKU/r7mXOsxwFr4BYbvB7tWYxVuLdb2ibd30ijjXINSMAHiEEZk5PCRBmW1gUeisn2VMKt3cQ==",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~6.19.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/yauzl": {
|
||||||
|
"version": "2.10.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
|
||||||
|
"integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/agent-base": {
|
||||||
|
"version": "7.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz",
|
||||||
|
"integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "^4.3.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ansi-regex": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ansi-styles": {
|
||||||
|
"version": "4.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||||
|
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||||
|
"dependencies": {
|
||||||
|
"color-convert": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ast-types": {
|
||||||
|
"version": "0.13.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz",
|
||||||
|
"integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/b4a": {
|
||||||
|
"version": "1.6.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.6.tgz",
|
||||||
|
"integrity": "sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg=="
|
||||||
|
},
|
||||||
|
"node_modules/bare-events": {
|
||||||
|
"version": "2.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.4.2.tgz",
|
||||||
|
"integrity": "sha512-qMKFd2qG/36aA4GwvKq8MxnPgCQAmBWmSyLWsJcbn8v03wvIPQ/hG1Ms8bPzndZxMDoHpxez5VOS+gC9Yi24/Q==",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"node_modules/bare-fs": {
|
||||||
|
"version": "2.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-2.3.1.tgz",
|
||||||
|
"integrity": "sha512-W/Hfxc/6VehXlsgFtbB5B4xFcsCl+pAh30cYhoFyXErf6oGrwjh8SwiPAdHgpmWonKuYpZgGywN0SXt7dgsADA==",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"bare-events": "^2.0.0",
|
||||||
|
"bare-path": "^2.0.0",
|
||||||
|
"bare-stream": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/bare-os": {
|
||||||
|
"version": "2.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/bare-os/-/bare-os-2.4.0.tgz",
|
||||||
|
"integrity": "sha512-v8DTT08AS/G0F9xrhyLtepoo9EJBJ85FRSMbu1pQUlAf6A8T0tEEQGMVObWeqpjhSPXsE0VGlluFBJu2fdoTNg==",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"node_modules/bare-path": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/bare-path/-/bare-path-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-lh/eITfU8hrj9Ru5quUp0Io1kJWIk1bTjzo7JH1P5dWmQ2EL4hFUlfI8FonAhSlgIfhn63p84CDY/x+PisgcXA==",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"bare-os": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/bare-stream": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-tiDAH9H/kP+tvNO5sczyn9ZAA7utrSMobyDchsnyyXBuUe2FSQWbxhtuHB8jwpHYYevVo2UJpcmvvjrbHboUUQ==",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"streamx": "^2.18.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/base64-js": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||||
|
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/basic-ftp": {
|
||||||
|
"version": "5.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz",
|
||||||
|
"integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/buffer": {
|
||||||
|
"version": "5.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
||||||
|
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"base64-js": "^1.3.1",
|
||||||
|
"ieee754": "^1.1.13"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/buffer-crc32": {
|
||||||
|
"version": "0.2.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
|
||||||
|
"integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/child_process": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/child_process/-/child_process-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-Wmza/JzL0SiWz7kl6MhIKT5ceIlnFPJX+lwUGj7Clhy5MMldsSoJR0+uvRzOS5Kv45Mq7t1PoE8TsOA9bzvb6g=="
|
||||||
|
},
|
||||||
|
"node_modules/chromium-bidi": {
|
||||||
|
"version": "0.6.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.6.4.tgz",
|
||||||
|
"integrity": "sha512-8zoq6ogmhQQkAKZVKO2ObFTl4uOkqoX1PlKQX3hZQ5E9cbUotcAb7h4pTNVAGGv8Z36PF3CtdOriEp/Rz82JqQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"mitt": "3.0.1",
|
||||||
|
"urlpattern-polyfill": "10.0.0",
|
||||||
|
"zod": "3.23.8"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"devtools-protocol": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cliui": {
|
||||||
|
"version": "8.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||||
|
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"string-width": "^4.2.0",
|
||||||
|
"strip-ansi": "^6.0.1",
|
||||||
|
"wrap-ansi": "^7.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/color-convert": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"color-name": "~1.1.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/color-name": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||||
|
},
|
||||||
|
"node_modules/data-uri-to-buffer": {
|
||||||
|
"version": "6.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz",
|
||||||
|
"integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/debug": {
|
||||||
|
"version": "4.3.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz",
|
||||||
|
"integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "2.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/degenerator": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"ast-types": "^0.13.4",
|
||||||
|
"escodegen": "^2.1.0",
|
||||||
|
"esprima": "^4.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/devtools-protocol": {
|
||||||
|
"version": "0.0.1312386",
|
||||||
|
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1312386.tgz",
|
||||||
|
"integrity": "sha512-DPnhUXvmvKT2dFA/j7B+riVLUt9Q6RKJlcppojL5CoRywJJKLDYnRlw0gTFKfgDPHP5E04UoB71SxoJlVZy8FA=="
|
||||||
|
},
|
||||||
|
"node_modules/emoji-regex": {
|
||||||
|
"version": "8.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||||
|
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
|
||||||
|
},
|
||||||
|
"node_modules/end-of-stream": {
|
||||||
|
"version": "1.4.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
|
||||||
|
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
|
||||||
|
"dependencies": {
|
||||||
|
"once": "^1.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/escalade": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/escodegen": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==",
|
||||||
|
"dependencies": {
|
||||||
|
"esprima": "^4.0.1",
|
||||||
|
"estraverse": "^5.2.0",
|
||||||
|
"esutils": "^2.0.2"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"escodegen": "bin/escodegen.js",
|
||||||
|
"esgenerate": "bin/esgenerate.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"source-map": "~0.6.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/esprima": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
|
||||||
|
"bin": {
|
||||||
|
"esparse": "bin/esparse.js",
|
||||||
|
"esvalidate": "bin/esvalidate.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/estraverse": {
|
||||||
|
"version": "5.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
|
||||||
|
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/esutils": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/extract-zip": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "^4.1.1",
|
||||||
|
"get-stream": "^5.1.0",
|
||||||
|
"yauzl": "^2.10.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"extract-zip": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.17.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@types/yauzl": "^2.9.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fast-fifo": {
|
||||||
|
"version": "1.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
|
||||||
|
"integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="
|
||||||
|
},
|
||||||
|
"node_modules/fd-slicer": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
|
||||||
|
"dependencies": {
|
||||||
|
"pend": "~1.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fs": {
|
||||||
|
"version": "0.0.1-security",
|
||||||
|
"resolved": "https://registry.npmjs.org/fs/-/fs-0.0.1-security.tgz",
|
||||||
|
"integrity": "sha512-3XY9e1pP0CVEUCdj5BmfIZxRBTSDycnbqhIOGec9QYtmVH2fbLpj86CFWkrNOkt/Fvty4KZG5lTglL9j/gJ87w=="
|
||||||
|
},
|
||||||
|
"node_modules/fs-extra": {
|
||||||
|
"version": "11.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz",
|
||||||
|
"integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==",
|
||||||
|
"dependencies": {
|
||||||
|
"graceful-fs": "^4.2.0",
|
||||||
|
"jsonfile": "^6.0.1",
|
||||||
|
"universalify": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/get-caller-file": {
|
||||||
|
"version": "2.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||||
|
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||||
|
"engines": {
|
||||||
|
"node": "6.* || 8.* || >= 10.*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/get-stream": {
|
||||||
|
"version": "5.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
|
||||||
|
"integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
|
||||||
|
"dependencies": {
|
||||||
|
"pump": "^3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/get-uri": {
|
||||||
|
"version": "6.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.3.tgz",
|
||||||
|
"integrity": "sha512-BzUrJBS9EcUb4cFol8r4W3v1cPsSyajLSthNkz5BxbpDcHN5tIrM10E2eNvfnvBn3DaT3DUgx0OpsBKkaOpanw==",
|
||||||
|
"dependencies": {
|
||||||
|
"basic-ftp": "^5.0.2",
|
||||||
|
"data-uri-to-buffer": "^6.0.2",
|
||||||
|
"debug": "^4.3.4",
|
||||||
|
"fs-extra": "^11.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/graceful-fs": {
|
||||||
|
"version": "4.2.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||||
|
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="
|
||||||
|
},
|
||||||
|
"node_modules/http-proxy-agent": {
|
||||||
|
"version": "7.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
|
||||||
|
"integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
|
||||||
|
"dependencies": {
|
||||||
|
"agent-base": "^7.1.0",
|
||||||
|
"debug": "^4.3.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/https-proxy-agent": {
|
||||||
|
"version": "7.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz",
|
||||||
|
"integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==",
|
||||||
|
"dependencies": {
|
||||||
|
"agent-base": "^7.0.2",
|
||||||
|
"debug": "4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ieee754": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/ip-address": {
|
||||||
|
"version": "9.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz",
|
||||||
|
"integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==",
|
||||||
|
"dependencies": {
|
||||||
|
"jsbn": "1.1.0",
|
||||||
|
"sprintf-js": "^1.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/is-fullwidth-code-point": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/jsbn": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A=="
|
||||||
|
},
|
||||||
|
"node_modules/jsonfile": {
|
||||||
|
"version": "6.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
|
||||||
|
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"universalify": "^2.0.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"graceful-fs": "^4.1.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lru-cache": {
|
||||||
|
"version": "7.18.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
|
||||||
|
"integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mitt": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="
|
||||||
|
},
|
||||||
|
"node_modules/ms": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||||
|
},
|
||||||
|
"node_modules/netmask": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/once": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||||
|
"dependencies": {
|
||||||
|
"wrappy": "1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pac-proxy-agent": {
|
||||||
|
"version": "7.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.0.2.tgz",
|
||||||
|
"integrity": "sha512-BFi3vZnO9X5Qt6NRz7ZOaPja3ic0PhlsmCRYLOpN11+mWBCR6XJDqW5RF3j8jm4WGGQZtBA+bTfxYzeKW73eHg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@tootallnate/quickjs-emscripten": "^0.23.0",
|
||||||
|
"agent-base": "^7.0.2",
|
||||||
|
"debug": "^4.3.4",
|
||||||
|
"get-uri": "^6.0.1",
|
||||||
|
"http-proxy-agent": "^7.0.0",
|
||||||
|
"https-proxy-agent": "^7.0.5",
|
||||||
|
"pac-resolver": "^7.0.1",
|
||||||
|
"socks-proxy-agent": "^8.0.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pac-resolver": {
|
||||||
|
"version": "7.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz",
|
||||||
|
"integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==",
|
||||||
|
"dependencies": {
|
||||||
|
"degenerator": "^5.0.0",
|
||||||
|
"netmask": "^2.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pend": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="
|
||||||
|
},
|
||||||
|
"node_modules/polylabel": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/polylabel/-/polylabel-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-B6Yu+Bdl/8SGtjVhyUfZzD3DwciCS9SPVtHiNdt8idHHatvTHp5Ss8XGDRmQFtfF1ZQnfK+Cj5dXdpkUXBbXgA==",
|
||||||
|
"dependencies": {
|
||||||
|
"tinyqueue": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/progress": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/proxy-agent": {
|
||||||
|
"version": "6.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.4.0.tgz",
|
||||||
|
"integrity": "sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"agent-base": "^7.0.2",
|
||||||
|
"debug": "^4.3.4",
|
||||||
|
"http-proxy-agent": "^7.0.1",
|
||||||
|
"https-proxy-agent": "^7.0.3",
|
||||||
|
"lru-cache": "^7.14.1",
|
||||||
|
"pac-proxy-agent": "^7.0.1",
|
||||||
|
"proxy-from-env": "^1.1.0",
|
||||||
|
"socks-proxy-agent": "^8.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/proxy-from-env": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
|
||||||
|
},
|
||||||
|
"node_modules/pump": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
|
||||||
|
"dependencies": {
|
||||||
|
"end-of-stream": "^1.1.0",
|
||||||
|
"once": "^1.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/puppeteer-core": {
|
||||||
|
"version": "23.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-23.1.0.tgz",
|
||||||
|
"integrity": "sha512-SvAsu+xnLN2FMXE/59bp3s3WXp8ewqUGzVV4AQtml/2xmsciZnU/bXcCW+eETHPWQ6Agg2vTI7QzWXPpEARK2g==",
|
||||||
|
"dependencies": {
|
||||||
|
"@puppeteer/browsers": "2.3.1",
|
||||||
|
"chromium-bidi": "0.6.4",
|
||||||
|
"debug": "^4.3.6",
|
||||||
|
"devtools-protocol": "0.0.1312386",
|
||||||
|
"typed-query-selector": "^2.12.0",
|
||||||
|
"ws": "^8.18.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/queue-tick": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag=="
|
||||||
|
},
|
||||||
|
"node_modules/require-directory": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/semver": {
|
||||||
|
"version": "7.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
|
||||||
|
"integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
|
||||||
|
"bin": {
|
||||||
|
"semver": "bin/semver.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/smart-buffer": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6.0.0",
|
||||||
|
"npm": ">= 3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socks": {
|
||||||
|
"version": "2.8.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz",
|
||||||
|
"integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==",
|
||||||
|
"dependencies": {
|
||||||
|
"ip-address": "^9.0.5",
|
||||||
|
"smart-buffer": "^4.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.0.0",
|
||||||
|
"npm": ">= 3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socks-proxy-agent": {
|
||||||
|
"version": "8.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz",
|
||||||
|
"integrity": "sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw==",
|
||||||
|
"dependencies": {
|
||||||
|
"agent-base": "^7.1.1",
|
||||||
|
"debug": "^4.3.4",
|
||||||
|
"socks": "^2.8.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/source-map": {
|
||||||
|
"version": "0.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||||
|
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/sprintf-js": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="
|
||||||
|
},
|
||||||
|
"node_modules/streamx": {
|
||||||
|
"version": "2.18.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/streamx/-/streamx-2.18.0.tgz",
|
||||||
|
"integrity": "sha512-LLUC1TWdjVdn1weXGcSxyTR3T4+acB6tVGXT95y0nGbca4t4o/ng1wKAGTljm9VicuCVLvRlqFYXYy5GwgM7sQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"fast-fifo": "^1.3.2",
|
||||||
|
"queue-tick": "^1.0.1",
|
||||||
|
"text-decoder": "^1.1.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"bare-events": "^2.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/string-width": {
|
||||||
|
"version": "4.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||||
|
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||||
|
"dependencies": {
|
||||||
|
"emoji-regex": "^8.0.0",
|
||||||
|
"is-fullwidth-code-point": "^3.0.0",
|
||||||
|
"strip-ansi": "^6.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/strip-ansi": {
|
||||||
|
"version": "6.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||||
|
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-regex": "^5.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tar-fs": {
|
||||||
|
"version": "3.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz",
|
||||||
|
"integrity": "sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==",
|
||||||
|
"dependencies": {
|
||||||
|
"pump": "^3.0.0",
|
||||||
|
"tar-stream": "^3.1.5"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"bare-fs": "^2.1.1",
|
||||||
|
"bare-path": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tar-stream": {
|
||||||
|
"version": "3.1.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz",
|
||||||
|
"integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"b4a": "^1.6.4",
|
||||||
|
"fast-fifo": "^1.2.0",
|
||||||
|
"streamx": "^2.15.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/text-decoder": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-8zll7REEv4GDD3x4/0pW+ppIxSNs7H1J10IKFZsuOMscumCdM2a+toDGLPA3T+1+fLBql4zbt5z83GEQGGV5VA==",
|
||||||
|
"dependencies": {
|
||||||
|
"b4a": "^1.6.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/through": {
|
||||||
|
"version": "2.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
|
||||||
|
"integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="
|
||||||
|
},
|
||||||
|
"node_modules/tinyqueue": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g=="
|
||||||
|
},
|
||||||
|
"node_modules/tslib": {
|
||||||
|
"version": "2.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz",
|
||||||
|
"integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ=="
|
||||||
|
},
|
||||||
|
"node_modules/typed-query-selector": {
|
||||||
|
"version": "2.12.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz",
|
||||||
|
"integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg=="
|
||||||
|
},
|
||||||
|
"node_modules/unbzip2-stream": {
|
||||||
|
"version": "1.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz",
|
||||||
|
"integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==",
|
||||||
|
"dependencies": {
|
||||||
|
"buffer": "^5.2.1",
|
||||||
|
"through": "^2.3.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/undici-types": {
|
||||||
|
"version": "6.19.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.6.tgz",
|
||||||
|
"integrity": "sha512-e/vggGopEfTKSvj4ihnOLTsqhrKRN3LeO6qSN/GxohhuRv8qH9bNQ4B8W7e/vFL+0XTnmHPB4/kegunZGA4Org==",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"node_modules/universalify": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/urlpattern-polyfill": {
|
||||||
|
"version": "10.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.0.0.tgz",
|
||||||
|
"integrity": "sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg=="
|
||||||
|
},
|
||||||
|
"node_modules/wrap-ansi": {
|
||||||
|
"version": "7.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||||
|
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^4.0.0",
|
||||||
|
"string-width": "^4.1.0",
|
||||||
|
"strip-ansi": "^6.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/wrappy": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
|
||||||
|
},
|
||||||
|
"node_modules/ws": {
|
||||||
|
"version": "8.18.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
|
||||||
|
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": ">=5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/y18n": {
|
||||||
|
"version": "5.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||||
|
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/yargs": {
|
||||||
|
"version": "17.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
||||||
|
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
|
||||||
|
"dependencies": {
|
||||||
|
"cliui": "^8.0.1",
|
||||||
|
"escalade": "^3.1.1",
|
||||||
|
"get-caller-file": "^2.0.5",
|
||||||
|
"require-directory": "^2.1.1",
|
||||||
|
"string-width": "^4.2.3",
|
||||||
|
"y18n": "^5.0.5",
|
||||||
|
"yargs-parser": "^21.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/yargs-parser": {
|
||||||
|
"version": "21.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
|
||||||
|
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/yauzl": {
|
||||||
|
"version": "2.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
|
||||||
|
"integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
|
||||||
|
"dependencies": {
|
||||||
|
"buffer-crc32": "~0.2.3",
|
||||||
|
"fd-slicer": "~1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zod": {
|
||||||
|
"version": "3.23.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
|
||||||
|
"integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
8
map-generator/package.json
Normal file
8
map-generator/package.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"child_process": "^1.0.2",
|
||||||
|
"fs": "^0.0.1-security",
|
||||||
|
"polylabel": "^2.0.1",
|
||||||
|
"puppeteer-core": "^23.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
17
map-generator/readme.md
Normal file
17
map-generator/readme.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Map Generator
|
||||||
|
|
||||||
|
This project displays a map showing the boundaries of wards within a single local government area. Each ward is labelled.
|
||||||
|
|
||||||
|
The data for this comes from the VEC Map API: https://maps.vec.vic.gov.au/api/electorates/4/withboundaries
|
||||||
|
|
||||||
|
To allow the page to download the `wards_withboundaries.json` file, the page needs to be accessed via a `http://` URL (and not a `file:///` URL). One easy way to do this is with the web server built into Python, it can be started by running `python3 -m http.server` in this folder.
|
||||||
|
|
||||||
|
To tell the page which council to load, add a query parameter to the URL eg. `?council=brimbank` or `?council=Melbourne%20City%20Council`
|
||||||
|
|
||||||
|
To automatically compile the `src.js` file after every edit, run `webpack-cli --mode development --watch` in this folder.
|
||||||
|
|
||||||
|
Changes to `dist/main.js` should be committed so that other users don't need to install node.
|
||||||
|
|
||||||
|
The `capture-map.js` script can be used to capture an image of the map. It accepts 4 arguments, the council name, width, height, and output path. For example `node .\capture-map.js brimbank 900 500 ../../spl-data/brimbank/map.jpg` would capture a map of Brimbank City Council and place a file named `map.jpg` in the `../../spl-data/brimbank/` folder.
|
||||||
|
|
||||||
|
The `capture-all-maps.js` script can be used to capture an image for each council. It accepts 3 arguments, width, height, and the path to the `spl-data` repo. For example `node .\capture-all-maps.js 900 500 ../../spl-data` would capture a map of each council and put it in the spl-data repo in the appropriate folder.
|
||||||
11
map-generator/shptojson/Makefile
Normal file
11
map-generator/shptojson/Makefile
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
SHPFILE := ../../../spl-data/federal_2025/aec_data/E_VIC24_region.shp
|
||||||
|
SHAPELIB := ../../../lib/shapelib-1.6.0
|
||||||
|
|
||||||
|
../federal_2025_boundaries.json: $(SHPFILE) shptojson
|
||||||
|
LD_LIBRARY_PATH=$(SHAPELIB)/.libs ./shptojson $(SHPFILE) > $@
|
||||||
|
|
||||||
|
CFLAGS := -I $(SHAPELIB)
|
||||||
|
LDFLAGS := -L $(SHAPELIB)/.libs
|
||||||
|
|
||||||
|
shptojson: shptojson.c
|
||||||
|
gcc -Wall $(CFLAGS) $(LDFLAGS) -lshp $< -o $@
|
||||||
142
map-generator/shptojson/shptojson.c
Normal file
142
map-generator/shptojson/shptojson.c
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
/******************************************************************************
|
||||||
|
*
|
||||||
|
* Project: Shapelib
|
||||||
|
* Purpose: Sample application for dumping contents of a shapefile to
|
||||||
|
* the terminal in human readable form.
|
||||||
|
* Author: Frank Warmerdam, warmerdam@pobox.com
|
||||||
|
*
|
||||||
|
******************************************************************************
|
||||||
|
* Copyright (c) 1999, Frank Warmerdam
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
|
||||||
|
******************************************************************************
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include "shapefil.h"
|
||||||
|
|
||||||
|
int main(int argc, char **argv)
|
||||||
|
{
|
||||||
|
int nPrecision = 15;
|
||||||
|
int first;
|
||||||
|
const char *divison_name;
|
||||||
|
|
||||||
|
/* -------------------------------------------------------------------- */
|
||||||
|
/* Display a usage message. */
|
||||||
|
/* -------------------------------------------------------------------- */
|
||||||
|
if (argc != 2)
|
||||||
|
{
|
||||||
|
printf("Usage: shptojson shp_file\n");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
/* -------------------------------------------------------------------- */
|
||||||
|
/* Open the passed shapefile. */
|
||||||
|
/* -------------------------------------------------------------------- */
|
||||||
|
SHPHandle hSHP = SHPOpen(argv[1], "rb");
|
||||||
|
DBFHandle hDBF = DBFOpen(argv[1], "rb");
|
||||||
|
if (hSHP == NULL || hDBF == NULL)
|
||||||
|
{
|
||||||
|
printf("Unable to open:%s\n", argv[1]);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------------------------------------- */
|
||||||
|
/* Print out the file bounds. */
|
||||||
|
/* -------------------------------------------------------------------- */
|
||||||
|
int nEntities;
|
||||||
|
int nShapeType;
|
||||||
|
double adfMinBound[4];
|
||||||
|
double adfMaxBound[4];
|
||||||
|
SHPGetInfo(hSHP, &nEntities, &nShapeType, adfMinBound, adfMaxBound);
|
||||||
|
|
||||||
|
fprintf(stderr, "Shapefile Type: %s # of Shapes: %d\n\n",
|
||||||
|
SHPTypeName(nShapeType), nEntities);
|
||||||
|
|
||||||
|
/* -------------------------------------------------------------------- */
|
||||||
|
/* Skim over the list of shapes, printing all the vertices. */
|
||||||
|
/* -------------------------------------------------------------------- */
|
||||||
|
printf("[\n");
|
||||||
|
for (int i = 0; i < nEntities; i++)
|
||||||
|
{
|
||||||
|
SHPObject *psShape = SHPReadObject(hSHP, i);
|
||||||
|
|
||||||
|
if (psShape == NULL)
|
||||||
|
{
|
||||||
|
fprintf(stderr,
|
||||||
|
"Unable to read shape %d, terminating object reading.\n",
|
||||||
|
i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
divison_name = DBFReadStringAttribute(hDBF, i, 1);
|
||||||
|
fprintf(stderr, "shape %s has %i parts\n", divison_name, psShape->nParts);
|
||||||
|
|
||||||
|
printf(" {\n");
|
||||||
|
printf(" \"electorateId\": \"%08i\",\n", i);
|
||||||
|
printf(" \"electorateName\": \"%s\",\n", divison_name);
|
||||||
|
printf(" \"electorateType\": \"Division\",\n");
|
||||||
|
printf(" \"parentElectorateName\": \"%s\",\n", divison_name);
|
||||||
|
printf(" \"boundaryJson\": \"{\\\"type\\\": ");
|
||||||
|
|
||||||
|
if (psShape->nParts != 1)
|
||||||
|
{
|
||||||
|
printf("\\\"MultiPolygon\\\",\\\"coordinates\\\":[[[");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
printf("\\\"Polygon\\\",\\\"coordinates\\\":[[");
|
||||||
|
}
|
||||||
|
|
||||||
|
first = 1;
|
||||||
|
for (int j = 0, iPart = 1; j < psShape->nVertices; j++)
|
||||||
|
{
|
||||||
|
if (iPart < psShape->nParts && psShape->panPartStart[iPart] == j)
|
||||||
|
{
|
||||||
|
iPart++;
|
||||||
|
printf("]],[[");
|
||||||
|
}
|
||||||
|
else if (!first)
|
||||||
|
{
|
||||||
|
printf(",");
|
||||||
|
}
|
||||||
|
first = 0;
|
||||||
|
printf("[%.*g,%.*g]", nPrecision, psShape->padfX[j],
|
||||||
|
nPrecision, psShape->padfY[j]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i == (nEntities-1))
|
||||||
|
{
|
||||||
|
if (psShape->nParts != 1)
|
||||||
|
{
|
||||||
|
printf("]]]}\"\n }\n");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
printf("]]}\"\n }\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (psShape->nParts != 1)
|
||||||
|
{
|
||||||
|
printf("]]]}\"\n },\n");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
printf("]]}\"\n },\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SHPDestroyObject(psShape);
|
||||||
|
}
|
||||||
|
printf("]\n");
|
||||||
|
|
||||||
|
SHPClose(hSHP);
|
||||||
|
DBFClose(hDBF);
|
||||||
|
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
196
map-generator/src.js
Normal file
196
map-generator/src.js
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
import polylabel from 'polylabel';
|
||||||
|
|
||||||
|
function normaliseCouncilName(str) {
|
||||||
|
const regex = /(.*?)(?:(?: Rural)?(?: City| Shire| Borough) Council)/g;
|
||||||
|
const matches = str.matchAll(regex);
|
||||||
|
|
||||||
|
// If we get a match, convert to slug format
|
||||||
|
for (const match of matches) {
|
||||||
|
return match[1].toLowerCase().replace(" ", "-");
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we didn't find any matches, try convert input to slug format
|
||||||
|
return str.toLowerCase().replace(" ", "-");
|
||||||
|
};
|
||||||
|
|
||||||
|
const searchParams = new URLSearchParams(window.location.search);
|
||||||
|
const councilName = normaliseCouncilName(searchParams.get("council"));
|
||||||
|
console.log(councilName);
|
||||||
|
|
||||||
|
mapboxgl.accessToken = 'pk.eyJ1IjoibWF0dHl3YXkiLCJhIjoiY2x6eG9vMzZyMHY2cDJqb3M1ODZnNjF4cyJ9.IX8CfYQZUaQhSjWgMXmsEA';
|
||||||
|
const map = new mapboxgl.Map({
|
||||||
|
container: 'map',
|
||||||
|
zoom: 10,
|
||||||
|
style: 'mapbox://styles/mattyway/cm03vw57q00fd01pn93wf2j7p',
|
||||||
|
center: [145.00724,-37.79011]
|
||||||
|
});
|
||||||
|
|
||||||
|
map.on('load', () => {
|
||||||
|
// Load an image from an external URL.
|
||||||
|
map.loadImage('circle.png', (error, image) => {
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
map.addImage('blue-circle', image);
|
||||||
|
|
||||||
|
fetch("federal_2025_boundaries.json")
|
||||||
|
.then(response => {
|
||||||
|
response.json()
|
||||||
|
.then((wardData) => {
|
||||||
|
const filteredWardData = wardData.filter((ward) => normaliseCouncilName(ward.parentElectorateName) == councilName);
|
||||||
|
|
||||||
|
var bounds = {
|
||||||
|
"west": undefined,
|
||||||
|
"south": undefined,
|
||||||
|
"east": undefined,
|
||||||
|
"north": undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
function addToBounds(coordinate) {
|
||||||
|
if (bounds.west == undefined || coordinate[0] < bounds.west) {
|
||||||
|
bounds.west = coordinate[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bounds.south == undefined || coordinate[1] < bounds.south) {
|
||||||
|
bounds.south = coordinate[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bounds.east == undefined || coordinate[0] > bounds.east) {
|
||||||
|
bounds.east = coordinate[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bounds.north == undefined || coordinate[1] > bounds.north) {
|
||||||
|
bounds.north = coordinate[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var labelFeatures = [];
|
||||||
|
|
||||||
|
filteredWardData.forEach(wardData => {
|
||||||
|
const featureCollection = {
|
||||||
|
'type': 'FeatureCollection',
|
||||||
|
'features': [
|
||||||
|
{
|
||||||
|
'type': 'Feature',
|
||||||
|
'geometry': JSON.parse(wardData.boundaryJson)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
if (featureCollection.features[0].geometry.type == "Polygon") {
|
||||||
|
featureCollection.features[0].geometry.coordinates[0].forEach(coordinate => {
|
||||||
|
addToBounds(coordinate);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (featureCollection.features[0].geometry.type == "MultiPolygon") {
|
||||||
|
featureCollection.features[0].geometry.coordinates.forEach(polygon => {
|
||||||
|
polygon[0].forEach(coordinate => {
|
||||||
|
addToBounds(coordinate);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add data
|
||||||
|
map.addSource("data_"+wardData.electorateId, {
|
||||||
|
'type': 'geojson',
|
||||||
|
'data': featureCollection
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add a line along the data
|
||||||
|
map.addLayer({
|
||||||
|
'id': "outline_"+wardData.electorateId,
|
||||||
|
'type': 'line',
|
||||||
|
'source': "data_"+wardData.electorateId,
|
||||||
|
'layout': {},
|
||||||
|
'paint': {
|
||||||
|
'line-color': '#0899fe',
|
||||||
|
'line-width': 3
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var centrePoint;
|
||||||
|
if (featureCollection.features[0].geometry.type == "Polygon") {
|
||||||
|
centrePoint = polylabel(featureCollection.features[0].geometry.coordinates, 0.000001);
|
||||||
|
}
|
||||||
|
if (featureCollection.features[0].geometry.type == "MultiPolygon") {
|
||||||
|
// TODO: Find the biggest polygon in the multipolygon and use that to find the centre point
|
||||||
|
// instead of just picking the second polygon.
|
||||||
|
//
|
||||||
|
// The 2024 set of boundaries only uses 2 MultiPolygon objects (Cathedral in Murrindindi Shire Council and Island in Bass Coast Shire Council)
|
||||||
|
// Luckily, the second polygon for both objects results in a good label placement.
|
||||||
|
centrePoint = polylabel(featureCollection.features[0].geometry.coordinates[0], 0.000001);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (wardData.electorateName.includes(' ')) {
|
||||||
|
// Breaking long names into newlines looks better
|
||||||
|
const parts = wardData.electorateName.split(' ');
|
||||||
|
// Special case if a ward starts with "St" (like "St Albans East")
|
||||||
|
// Join the first two parts
|
||||||
|
if (parts[0] == "St") {
|
||||||
|
parts[0] = parts[0] + ' ' + parts[1];
|
||||||
|
parts.splice(1, 1);
|
||||||
|
}
|
||||||
|
const wardNameNewLines = parts.join('\n');
|
||||||
|
labelFeatures.push({
|
||||||
|
'type': 'Feature',
|
||||||
|
'properties': {
|
||||||
|
'description': wardNameNewLines
|
||||||
|
},
|
||||||
|
'geometry': {
|
||||||
|
'type': 'Point',
|
||||||
|
'coordinates': centrePoint
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
labelFeatures.push({
|
||||||
|
'type': 'Feature',
|
||||||
|
'properties': {
|
||||||
|
'description': wardData.electorateName
|
||||||
|
},
|
||||||
|
'geometry': {
|
||||||
|
'type': 'Point',
|
||||||
|
'coordinates': centrePoint
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
map.addSource('labels', {
|
||||||
|
'type': 'geojson',
|
||||||
|
'data': {
|
||||||
|
'type': 'FeatureCollection',
|
||||||
|
'features': labelFeatures
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
map.addLayer({
|
||||||
|
'id': 'labels',
|
||||||
|
'type': 'symbol',
|
||||||
|
'source': 'labels',
|
||||||
|
'layout': {
|
||||||
|
'text-field': ['get', 'description'],
|
||||||
|
'text-variable-anchor': ['top', 'left', 'bottom', 'right', 'top-left', 'top-right', 'bottom-left', 'bottom-right'],
|
||||||
|
'text-radial-offset': 1,
|
||||||
|
'text-padding': 0,
|
||||||
|
'text-justify': 'auto',
|
||||||
|
'text-allow-overlap': false,
|
||||||
|
'text-ignore-placement': false,
|
||||||
|
'icon-image': 'blue-circle'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
map.fitBounds([
|
||||||
|
[bounds.west, bounds.south],
|
||||||
|
[bounds.east, bounds.north]
|
||||||
|
], {
|
||||||
|
padding: 25,
|
||||||
|
animate: false
|
||||||
|
});
|
||||||
|
|
||||||
|
}).catch(err => {
|
||||||
|
console.log(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
1
map-generator/wards_withboundaries.json
Normal file
1
map-generator/wards_withboundaries.json
Normal file
File diff suppressed because one or more lines are too long
2
php-template/.vscode/launch.json
vendored
2
php-template/.vscode/launch.json
vendored
@@ -6,7 +6,7 @@
|
|||||||
"type": "php",
|
"type": "php",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"program": "${workspaceFolder}/main.php",
|
"program": "${workspaceFolder}/main.php",
|
||||||
"args": ["--council-file", "${workspaceFolder}/example-config.json", "--candidates-file", "${workspaceFolder}/example-candidates.csv"],
|
"args": ["--council-file", "${workspaceFolder}/example-config.json", "--candidates-file", "${workspaceFolder}/example-candidates.csv", "--media-file", "${workspaceFolder}/example-media.json"],
|
||||||
"cwd": "${workspaceFolder}",
|
"cwd": "${workspaceFolder}",
|
||||||
"port": 9000
|
"port": 9000
|
||||||
}
|
}
|
||||||
|
|||||||
2
php-template/example-candidates-elected.csv
Normal file
2
php-template/example-candidates-elected.csv
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
Ward,Candidate Name,Elected
|
||||||
|
Harvester,Joe Blogs,y
|
||||||
|
10
php-template/example-media.json
Normal file
10
php-template/example-media.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"map.jpg": {
|
||||||
|
"id": 123,
|
||||||
|
"url": "http://localhost/map.png"
|
||||||
|
},
|
||||||
|
"default.png": {
|
||||||
|
"id": 987,
|
||||||
|
"url": "http://localhost/default.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
require_once "page_renderer.php";
|
require_once "page_renderer.php";
|
||||||
|
|
||||||
$options = getopt("", ["council-file:", "candidates-file:", "media-file:"]);
|
$options = getopt("", ["council-file:", "candidates-file:", "media-file:", "candidates-elected-file:"]);
|
||||||
|
|
||||||
if (isset($options['council-file'])) {
|
if (isset($options['council-file'])) {
|
||||||
$councilFileContents = file_get_contents($options['council-file']);
|
$councilFileContents = file_get_contents($options['council-file']);
|
||||||
@@ -27,7 +27,8 @@ if (isset($options['candidates-file'])) {
|
|||||||
|
|
||||||
// Convert CSV into an array of dictionaries. Use the header as the key in the dictionary.
|
// Convert CSV into an array of dictionaries. Use the header as the key in the dictionary.
|
||||||
$candidateData = [];
|
$candidateData = [];
|
||||||
if (($handle = fopen($candidatesFile, "r")) !== FALSE) {
|
if (file_exists($candidatesFile)) {
|
||||||
|
if (($handle = fopen($candidatesFile, "r")) !== FALSE) {
|
||||||
$headers = fgetcsv($handle);
|
$headers = fgetcsv($handle);
|
||||||
while (($data = fgetcsv($handle)) !== FALSE) {
|
while (($data = fgetcsv($handle)) !== FALSE) {
|
||||||
$candidate = [];
|
$candidate = [];
|
||||||
@@ -37,17 +38,12 @@ if (($handle = fopen($candidatesFile, "r")) !== FALSE) {
|
|||||||
$candidateData[] = $candidate;
|
$candidateData[] = $candidate;
|
||||||
}
|
}
|
||||||
fclose($handle);
|
fclose($handle);
|
||||||
} else {
|
} else {
|
||||||
error_log('Error opening candidates file');
|
error_log('Error opening candidates file');
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
$candidateData = array_filter($candidateData, function ($candidate) use ($councilData) {
|
error_log("The specified candidates.csv file does not exist, will not show any candidates for " . $councilData["shortName"] . ".");
|
||||||
return isset($candidate["Council"]) && $candidate["Council"] === $councilData['shortName'];
|
|
||||||
});
|
|
||||||
|
|
||||||
if (empty($candidateData)) {
|
|
||||||
error_log("Failed to load any candidates for " . $councilData['shortName']);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($options['media-file'])) {
|
if (isset($options['media-file'])) {
|
||||||
@@ -59,6 +55,37 @@ if (isset($options['media-file'])) {
|
|||||||
|
|
||||||
$mediaData = json_decode($mediaFileContents, true);
|
$mediaData = json_decode($mediaFileContents, true);
|
||||||
|
|
||||||
|
// Merge elected data (if present) into candidate objects
|
||||||
|
if (isset($options['candidates-elected-file'])) {
|
||||||
|
$candidatesElectedFile = $options['candidates-elected-file'];
|
||||||
|
|
||||||
|
if (file_exists($candidatesElectedFile)) {
|
||||||
|
if (($handle = fopen($candidatesElectedFile, "r")) !== FALSE) {
|
||||||
|
$headers = fgetcsv($handle);
|
||||||
|
while (($data = fgetcsv($handle)) !== FALSE) {
|
||||||
|
$electedCandidate = [];
|
||||||
|
foreach ($headers as $key => $value) {
|
||||||
|
$electedCandidate[$value] = $data[$key];
|
||||||
|
}
|
||||||
|
foreach ($candidateData as &$candidate) {
|
||||||
|
if ($candidate["Candidate Name"] == $electedCandidate["Candidate Name"]) {
|
||||||
|
if ($electedCandidate["Elected"] == "y") {
|
||||||
|
$candidate["Elected"] = True;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fclose($handle);
|
||||||
|
} else {
|
||||||
|
error_log('Error opening candidates elected file');
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
error_log("The specified candidates elected file does not exist, will not show any elected candidates for " . $councilData["shortName"] . ".");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$renderer = new SPLPageRenderer();
|
$renderer = new SPLPageRenderer();
|
||||||
$pageContent = $renderer->renderCouncilPage($councilData, $candidateData, $mediaData);
|
$pageContent = $renderer->renderCouncilPage($councilData, $candidateData, $mediaData);
|
||||||
if ($pageContent === null) {
|
if ($pageContent === null) {
|
||||||
|
|||||||
@@ -1,9 +1,30 @@
|
|||||||
<!-- wp:paragraph {"align":"center"} -->
|
<?php
|
||||||
<p class="has-text-align-center">The Streets People Love campaign has created scorecards for candidates in the 2024 council elections. Scorecards have been generated based on a candidate's engagement with the Streets People Love campaign, their commitment to our pledge, their responses to a survey and input from campaign members located in the local government area in which they are running.</p>
|
|
||||||
|
function sluggify($input) {
|
||||||
|
return strtolower(str_replace(' ', '-', $input));
|
||||||
|
}
|
||||||
|
|
||||||
|
$surveyLink = "<a href=\"https://forms.gle/CeGbJF11SUkATjfN8\">Streets People Love Pledge and Survey</a>";
|
||||||
|
if (isset($config["survey"])) {
|
||||||
|
$surveyLink = $config["survey"];
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!-- wp:paragraph -->
|
||||||
|
<?php if (isset($config['header'])): ?>
|
||||||
|
|
||||||
|
<p><?php echo $config['header']; ?></p>
|
||||||
|
|
||||||
|
<?php else: ?>
|
||||||
|
|
||||||
|
<p>The Streets People Love campaign has created scorecards for candidates in the 2025 federal election. Scorecards have been generated based on a candidate's engagement with the Streets People Love campaign, their commitment to our pledge, their responses to a survey and input from campaign members located in the division in which they are running.</p>
|
||||||
<!-- /wp:paragraph -->
|
<!-- /wp:paragraph -->
|
||||||
|
|
||||||
<!-- wp:paragraph {"align":"center"} -->
|
<!-- wp:paragraph -->
|
||||||
<p class="has-text-align-center">Have candidates in your local government area not yet taken part? Send your local candidates the <a href="https://forms.gle/gnDNyBiVC64tDo2Y7">Streets People Love Pledge and Survey</a> and ask them to complete it so that local residents can vote for the candidates who want to build the streets people love.</p>
|
<p>Can't see a candidate you know is running? Candidates who don't take our survey won't appear on this page. Feel free to send your local candidates the <?php echo $surveyLink; ?> and let them know it's important to local residents that they do take part, so that we can vote for those who want to build the streets people love.</p>
|
||||||
|
|
||||||
|
<?php endif; ?>
|
||||||
<!-- /wp:paragraph -->
|
<!-- /wp:paragraph -->
|
||||||
|
|
||||||
<?php if (isset($media["header.jpg"])): ?>
|
<?php if (isset($media["header.jpg"])): ?>
|
||||||
@@ -12,56 +33,172 @@
|
|||||||
<!-- /wp:image -->
|
<!-- /wp:image -->
|
||||||
<?php endif ?>
|
<?php endif ?>
|
||||||
|
|
||||||
<?php foreach ($config['wardNames'] as $index => $wardName): ?>
|
<?php
|
||||||
<!-- wp:heading {"level":3,"className":"is-style-default"} -->
|
|
||||||
<?php $wardSlug = strtolower(str_replace(' ', '-', $wardName)); ?>
|
$wardCount = 1; //count($config['wardNames']);
|
||||||
<h3 class="wp-block-heading is-style-default" id="<?php echo $wardSlug; ?>"><a style="text-decoration: none;" href="#<?php echo $wardSlug; ?>"><?php echo $wardName; ?></a></h3>
|
|
||||||
<!-- /wp:heading -->
|
if ($wardCount > 1) {
|
||||||
|
$wardsDescription = $config['divisionName'] . " is divided into " . $wardCount . " wards:";
|
||||||
|
} else {
|
||||||
|
$wardsDescription = $config['divisionName'] . " is unsubdivided and does not contain any wards.";
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
||||||
|
|
||||||
|
<?php if (isset($media["map.jpg"])): ?>
|
||||||
|
<!-- wp:image {"id":<?php echo $media["map.jpg"]['id']; ?>,"width":"550px","sizeSlug":"full","linkDestination":"media","className":"is-style-default"} -->
|
||||||
|
<figure class="wp-block-image size-full is-resized is-style-default"><a href="<?php echo $media["map.jpg"]['url']; ?>" target="_blank" rel="noreferrer noopener"><img src="<?php echo $media["map.jpg"]['url']; ?>" alt="" class="wp-image-<?php echo $media["map.jpg"]['id']; ?>" style="width:550px"/></a></figure>
|
||||||
|
<!-- /wp:image -->
|
||||||
|
<?php endif ?>
|
||||||
|
|
||||||
|
<?php if ($wardCount > 1): ?>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
if ($wardCount > 8) {
|
||||||
|
$wardListChunkSize = ceil($wardCount / 2);
|
||||||
|
} else {
|
||||||
|
$wardListChunkSize = $wardCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
$wardChunks = array_chunk($config['wardNames'], $wardListChunkSize);
|
||||||
|
|
||||||
|
?>
|
||||||
|
<!-- wp:columns {"className":"ward-list-columns"} -->
|
||||||
|
<div class="wp-block-columns ward-list-columns">
|
||||||
|
|
||||||
|
<?php for ($columnIdx = 0; $columnIdx < 4; $columnIdx++): ?>
|
||||||
|
<!-- wp:column {"verticalAlignment":"top","style":{"spacing":{"padding":{"top":"0","bottom":"0"}}}} -->
|
||||||
|
<div class="wp-block-column is-vertically-aligned-top" style="padding-top:0;padding-bottom:0">
|
||||||
|
|
||||||
|
<?php if (array_key_exists($columnIdx, $wardChunks)): ?>
|
||||||
|
<!-- wp:list {"style":{"spacing":{"margin":{"top":"0","right":"0","bottom":"0","left":"0"}}}} -->
|
||||||
|
<ul style="margin-top:0;margin-right:0;margin-bottom:0;margin-left:0" class="wp-block-list">
|
||||||
|
|
||||||
|
<?php foreach($wardChunks[$columnIdx] as $wardName): ?>
|
||||||
|
<!-- wp:list-item -->
|
||||||
|
<li><a href="#<?php echo sluggify($wardName); ?>"><?php echo $wardName; ?></a></li>
|
||||||
|
<!-- /wp:list-item -->
|
||||||
|
<?php endforeach; ?>
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
<!-- /wp:list -->
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<!-- /wp:column -->
|
||||||
|
<?php endfor; ?>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<!-- /wp:columns -->
|
||||||
|
<?php else: ?>
|
||||||
|
<!-- wp:paragraph -->
|
||||||
|
<p></p>
|
||||||
|
<!-- /wp:paragraph -->
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
$wardCandidates = array_filter($candidates, function ($candidate) use ($wardName) {
|
if (isset($config['groupNames'])) {
|
||||||
return isset($candidate["Ward"]) && $candidate["Ward"] === $wardName;
|
$groupNames = $config['groupNames'];
|
||||||
});
|
$propertyOnCandidate = 'Group';
|
||||||
|
} else {
|
||||||
|
$groupNames = ['N/A']; // $config['wardNames'];
|
||||||
|
$propertyOnCandidate = 'Ward';
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
usort($wardCandidates, function($a, $b) {
|
<?php foreach ($groupNames as $index => $groupName): ?>
|
||||||
|
<?php
|
||||||
|
$groupCandidates = array_filter($candidates, function ($candidate) use ($groupName, $propertyOnCandidate) {
|
||||||
|
return isset($candidate[$propertyOnCandidate]) &&
|
||||||
|
strtolower($candidate[$propertyOnCandidate]) === strtolower($groupName);
|
||||||
|
});
|
||||||
|
|
||||||
|
usort($candidates, function($a, $b) {
|
||||||
if ($a == $b) {
|
if ($a == $b) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
return (((int) $a) < ((int) $b)) ? 1 : -1;
|
return (((int) $a['Rating']) < ((int) $b['Rating'])) ? 1 : -1;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (count($candidates) > 0):
|
||||||
|
?>
|
||||||
|
|
||||||
if (count($wardCandidates) == 0) continue;
|
|
||||||
?>
|
|
||||||
<!-- wp:group {"style":{"spacing":{"padding":{"top":"0","bottom":"7rem"}}},"layout":{"type":"grid","columnCount":4}} -->
|
|
||||||
<div class="wp-block-group" style="padding-top:0;padding-bottom:7rem">
|
|
||||||
<?php foreach ($wardCandidates as $index => $candidate): ?>
|
|
||||||
<!-- wp:group {"layout":{"type":"flex","orientation":"vertical","justifyContent":"center"}} -->
|
|
||||||
<div class="wp-block-group">
|
|
||||||
<?php
|
<?php
|
||||||
|
$columnCount = 4;
|
||||||
|
|
||||||
|
$chunkedWardCandidates = array_chunk($candidates, $columnCount);
|
||||||
|
?>
|
||||||
|
|
||||||
|
<?php foreach($chunkedWardCandidates as $chunk): ?>
|
||||||
|
<!-- wp:columns -->
|
||||||
|
<div class="wp-block-columns">
|
||||||
|
|
||||||
|
<?php for ($columnIdx = 0; $columnIdx < $columnCount; $columnIdx++): ?>
|
||||||
|
<!-- wp:column -->
|
||||||
|
<div class="wp-block-column">
|
||||||
|
|
||||||
|
<?php if (array_key_exists($columnIdx, $chunk)): ?>
|
||||||
|
<?php
|
||||||
|
$candidate = $chunk[$columnIdx];
|
||||||
|
|
||||||
|
if (isset($candidate["Elected"]) && $candidate["Elected"]) {
|
||||||
|
$candidate_elected = true;
|
||||||
|
} else {
|
||||||
|
$candidate_elected = false;
|
||||||
|
}
|
||||||
|
|
||||||
if (isset($candidate['Picture']) && isset($media[$candidate['Picture']])) {
|
if (isset($candidate['Picture']) && isset($media[$candidate['Picture']])) {
|
||||||
$candidate_image = $media[$candidate['Picture']];
|
$candidate_image = $media[$candidate['Picture']];
|
||||||
} else {
|
} else {
|
||||||
$candidate_image = $media['default.png'];
|
$candidate_image = $media['default.png'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$candidate_rating = str_repeat("✔️", max(0, min(5, $candidate['Rating'])));
|
||||||
|
|
||||||
|
// If string is 5 ticks, insert a zero width space entity between the 3rd and 4th ticks so that it wraps nicer
|
||||||
|
if ($candidate_rating == "✔️✔️✔️✔️✔️") {
|
||||||
|
$candidate_rating = "✔️✔️✔️​✔️✔️";
|
||||||
|
}
|
||||||
|
// If string is 4 ticks, insert a zero width space entity between the 2nd and 3rd ticks so that it wraps nicer
|
||||||
|
if ($candidate_rating == "✔️✔️✔️✔️") {
|
||||||
|
$candidate_rating = "✔️✔️​✔️✔️";
|
||||||
|
}
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<!-- wp:image {"id":<?php echo $candidate_image['id']; ?>,"width":"200px","height":"200px","scale":"cover","style":{"color":{}},"className":"is-resized"} -->
|
<!-- wp:image {"id":<?php echo $candidate_image['id']; ?>,"width":"200px","height":"200px","scale":"cover","align":"center","style":{"color":{}},"className":"is-resized"} -->
|
||||||
<figure class="wp-block-image is-resized"><img src="<?php echo $candidate_image['url']; ?>" alt="" class="wp-image-<?php echo $candidate_image['id']; ?>" style="object-fit:cover;width:200px;height:200px"/></figure>
|
<figure class="wp-block-image aligncenter is-resized <?php if ($candidate_elected) { echo "elected-candidate"; } ?>"><img src="<?php echo $candidate_image['url']; ?>" alt="" class="wp-image-<?php echo $candidate_image['id']; ?>" style="object-fit:cover;width:200px;height:200px;"/>
|
||||||
|
<?php if ($candidate_elected): ?>
|
||||||
|
<figcaption>ELECTED</figcaption>
|
||||||
|
<?php endif; ?>
|
||||||
|
</figure>
|
||||||
<!-- /wp:image -->
|
<!-- /wp:image -->
|
||||||
|
|
||||||
<!-- wp:heading {"fontSize":"medium"} -->
|
<!-- wp:heading {"textAlign":"center","className":"wp-block-heading has-text-align-center has-medium-font-size","style":{"spacing":{"margin":{"top":"1rem","bottom":"0.5rem"}}}} -->
|
||||||
<h2 class="wp-block-heading has-medium-font-size"><strong><?php echo $candidate['Candidate Name']; ?></strong></h2>
|
<h2 class="wp-block-heading has-text-align-center has-medium-font-size" style="margin-top:1rem;margin-bottom:0.5rem"><strong><?php echo htmlspecialchars($candidate['Candidate Name']); ?></strong></h2>
|
||||||
<!-- /wp:heading -->
|
<!-- /wp:heading -->
|
||||||
|
|
||||||
<!-- wp:paragraph {"style":{"layout":{"selfStretch":"fit","flexSize":null}},"fontSize":"large"} -->
|
<!-- wp:paragraph {"align":"center","style":{"layout":{"selfStretch":"fit","flexSize":null},"typography":{"lineHeight":"1"},"spacing":{"margin":{"top":"0.5rem","bottom":"1.5rem"}}},"fontSize":"large"} -->
|
||||||
<p class="has-large-font-size" style="color: rgba(100%, 0%, 0%, 0); text-shadow: 0 0 0 green;"><?php echo str_repeat("✔️", $candidate['Rating']); ?></p>
|
<p class="has-text-align-center has-large-font-size candidate-ticks"><?php echo $candidate_rating; ?></p>
|
||||||
<!-- /wp:paragraph -->
|
<!-- /wp:paragraph -->
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<!-- /wp:group -->
|
<!-- /wp:column -->
|
||||||
|
<?php endfor; ?>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<!-- /wp:columns -->
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
|
|
||||||
</div>
|
<?php else: ?>
|
||||||
<!-- /wp:group -->
|
|
||||||
|
<!-- wp:paragraph -->
|
||||||
|
<p>No candidates in this division have completed the survey. Send your local candidates the <?php echo $surveyLink; ?> and ask them to take part so that local residents can vote for the candidates who want to build the streets people love.</p>
|
||||||
|
<!-- /wp:paragraph -->
|
||||||
|
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
|
|
||||||
<?php if (isset($config['footer'])): ?>
|
<?php if (isset($config['footer'])): ?>
|
||||||
|
|||||||
42
pledge-update/homepage.php
Normal file
42
pledge-update/homepage.php
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once("parse_pledge_data.php");
|
||||||
|
|
||||||
|
$options = getopt("", ["candidates-files:"]);
|
||||||
|
|
||||||
|
if (isset($options['candidates-files'])) {
|
||||||
|
$candidates_files = $options['candidates-files'];
|
||||||
|
} else {
|
||||||
|
error_log("Error: Missing required option '--candidates-files'.");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$candidate_data = parse_pledge_data(explode(" ", $candidates_files), null);
|
||||||
|
|
||||||
|
/* Select people who have taken the pledge (and have an image) */
|
||||||
|
$pledgeCandidates = array_filter($candidate_data, function ($candidate) {
|
||||||
|
return $candidate['Pledge'] === 'Yes' && $candidate['Picture'] !== "";
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Select 9 random candidates */
|
||||||
|
$pledgeKeys = array_rand($pledgeCandidates, min(count($pledgeCandidates), 9));
|
||||||
|
shuffle($pledgeKeys);
|
||||||
|
|
||||||
|
$i = 0;
|
||||||
|
foreach ($pledgeKeys as $key) {
|
||||||
|
$image_url = $pledgeCandidates[$key]['image_url'];
|
||||||
|
$image_id = $pledgeCandidates[$key]['image_id'];
|
||||||
|
|
||||||
|
echo "s|pledge_img_".$i."|".$image_url."|\n";
|
||||||
|
echo "s|pledge_id_".$i."|".$image_id."|\n";
|
||||||
|
|
||||||
|
echo "s|pledge_string_".$i."|";
|
||||||
|
echo $pledgeCandidates[$key]['Candidate Name'].
|
||||||
|
" (".
|
||||||
|
$pledgeCandidates[$key]['Council'].
|
||||||
|
") has taken the pledge!|\n";
|
||||||
|
|
||||||
|
$i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
exit(0);
|
||||||
28
pledge-update/page_renderer.php
Normal file
28
pledge-update/page_renderer.php
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class SPLPageRenderer {
|
||||||
|
public function renderPledgePage($councils, $lga_pages, $candidates) {
|
||||||
|
ob_start();
|
||||||
|
|
||||||
|
$didError = false;
|
||||||
|
|
||||||
|
set_error_handler(function($errno, $errstr, $errfile, $errline) use(&$didError) {
|
||||||
|
$didError = true;
|
||||||
|
error_log("Error: $errstr in $errfile on line $errline");
|
||||||
|
return true; // Prevent default error handling
|
||||||
|
});
|
||||||
|
|
||||||
|
require "template.php";
|
||||||
|
|
||||||
|
restore_error_handler();
|
||||||
|
|
||||||
|
$content = ob_get_clean();
|
||||||
|
|
||||||
|
// Explictly return null if we didn't generate any content or if there was an error
|
||||||
|
if (!empty($content) && !$didError) {
|
||||||
|
return $content;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
110
pledge-update/parse_pledge_data.php
Normal file
110
pledge-update/parse_pledge_data.php
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
function trim_sluggify($input) {
|
||||||
|
return strtolower(str_replace(' ', '-', trim($input)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function parse_pledge_data($candidates_files, $default_image) {
|
||||||
|
$candidate_data = [];
|
||||||
|
foreach ($candidates_files as $key => $file) {
|
||||||
|
$config_file = dirname($file)."/config.json";
|
||||||
|
$config_string = file_get_contents($config_file);
|
||||||
|
$elected_file = dirname($file)."/candidates-elected.csv";
|
||||||
|
|
||||||
|
if ($config_string !== FALSE) {
|
||||||
|
$config = json_decode($config_string, true);
|
||||||
|
} else {
|
||||||
|
error_log("Error opening config.json.");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$elected_data = [];
|
||||||
|
if (file_exists($elected_file)) {
|
||||||
|
if (($elected_handle = fopen($elected_file, "r")) !== FALSE) {
|
||||||
|
$headers = fgetcsv($elected_handle);
|
||||||
|
while (($data = fgetcsv($elected_handle)) !== FALSE) {
|
||||||
|
$candidate = [];
|
||||||
|
foreach ($headers as $key => $value) {
|
||||||
|
$candidate[$value] = $data[$key];
|
||||||
|
}
|
||||||
|
$name_slug = trim_sluggify($candidate['Candidate Name']);
|
||||||
|
$elected_data[$name_slug] = $candidate;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
error_log('Error opening candidates file');
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (($handle = fopen($file, "r")) !== FALSE) {
|
||||||
|
$headers = fgetcsv($handle);
|
||||||
|
while (($data = fgetcsv($handle)) !== FALSE) {
|
||||||
|
$candidate = [];
|
||||||
|
$candidate['Pledge'] = 'n';
|
||||||
|
$candidate['Picture'] = "";
|
||||||
|
if (is_array($default_image)) {
|
||||||
|
$candidate['image_url'] = $default_image['url'];
|
||||||
|
$candidate['image_id'] = $default_image['id'];
|
||||||
|
} else {
|
||||||
|
$candidate['image_url'] = "";
|
||||||
|
$candidate['image_id'] = "";
|
||||||
|
}
|
||||||
|
foreach ($headers as $key => $value) {
|
||||||
|
$candidate[$value] = $data[$key];
|
||||||
|
}
|
||||||
|
$name_slug = trim_sluggify($candidate['Candidate Name']);
|
||||||
|
if (array_key_exists($name_slug, $candidate_data)) {
|
||||||
|
if ($candidate_data[$name_slug]['Pledge'] === 'y') {
|
||||||
|
$candidate['Pledge'] = 'y';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!empty($elected_data) && array_key_exists($name_slug, $elected_data)) {
|
||||||
|
$candidate['Elected'] = $elected_data[$name_slug]['Elected'];
|
||||||
|
}
|
||||||
|
$candidate['Council'] = $config['divisionName'];
|
||||||
|
$candidate['Path'] = dirname($file);
|
||||||
|
$media_desc = $candidate['Path']."/".
|
||||||
|
$candidate['Picture'].".json";
|
||||||
|
if (file_exists($media_desc)) {
|
||||||
|
$media_string = file_get_contents($media_desc);
|
||||||
|
if ($media_string !== FALSE) {
|
||||||
|
$media = json_decode($media_string, true);
|
||||||
|
} else {
|
||||||
|
error_log("Error opening image descriptor.");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
/* Get photo URL and ID */
|
||||||
|
$candidate['image_url'] = $media['url'];
|
||||||
|
$candidate['image_id'] = $media['id'];
|
||||||
|
}
|
||||||
|
$candidate_data[$name_slug] = $candidate;
|
||||||
|
}
|
||||||
|
fclose($handle);
|
||||||
|
} else {
|
||||||
|
error_log('Error opening candidates file');
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Override pledge columns if pledges.csv is present */
|
||||||
|
$pledges_file = dirname($file)."/pledges.csv";
|
||||||
|
if (!file_exists($pledges_file)) continue;
|
||||||
|
|
||||||
|
if (($handle = fopen($pledges_file, "r")) !== FALSE) {
|
||||||
|
$headers = fgetcsv($handle);
|
||||||
|
while (($data = fgetcsv($handle)) !== FALSE) {
|
||||||
|
$candidate = [];
|
||||||
|
foreach ($headers as $key => $value) {
|
||||||
|
$candidate[$value] = $data[$key];
|
||||||
|
}
|
||||||
|
$candidate_data[trim_sluggify($candidate['Candidate Name'])]['Pledge'] =
|
||||||
|
$candidate['Pledge'];
|
||||||
|
}
|
||||||
|
fclose($handle);
|
||||||
|
} else {
|
||||||
|
error_log('Error opening pledges file');
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $candidate_data;
|
||||||
|
}
|
||||||
64
pledge-update/pledge-page.php
Normal file
64
pledge-update/pledge-page.php
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once("parse_pledge_data.php");
|
||||||
|
require_once("page_renderer.php");
|
||||||
|
|
||||||
|
$options = getopt("", ["candidates-files:", "default-image:"]);
|
||||||
|
|
||||||
|
if (isset($options['candidates-files'])) {
|
||||||
|
$candidates_files = $options['candidates-files'];
|
||||||
|
} else {
|
||||||
|
error_log("Error: Missing required option '--candidates-files'.");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($options['default-image'])) {
|
||||||
|
$default_image = $options['default-image'];
|
||||||
|
} else {
|
||||||
|
error_log("Error: Missing required option '--default-image'.");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$default_image = file_get_contents($default_image);
|
||||||
|
|
||||||
|
if ($default_image !== FALSE) {
|
||||||
|
$default_image = json_decode($default_image, true);
|
||||||
|
} else {
|
||||||
|
error_log("Error opening config.json.");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$candidate_data = parse_pledge_data(explode(" ", $candidates_files), $default_image);
|
||||||
|
|
||||||
|
/* Select people who have taken the pledge */
|
||||||
|
$pledgeCandidates = array_filter($candidate_data, function ($candidate) {
|
||||||
|
return $candidate['Pledge'] === 'Yes';
|
||||||
|
});
|
||||||
|
|
||||||
|
$renderer = new SPLPageRenderer();
|
||||||
|
//print_r($pledgeCandidates);
|
||||||
|
|
||||||
|
$councils = [];
|
||||||
|
$lga_pages_unsort = [];
|
||||||
|
foreach ($pledgeCandidates as $key => $candidate) {
|
||||||
|
$councils[] = $candidate['Council'];
|
||||||
|
$lga_pages_unsort[] = basename($candidate['Path']);
|
||||||
|
}
|
||||||
|
$councils = array_unique($councils);
|
||||||
|
asort($councils);
|
||||||
|
|
||||||
|
$lga_pages = [];
|
||||||
|
foreach ($councils as $key => $council) {
|
||||||
|
$lga_pages[$key] = $lga_pages_unsort[$key];
|
||||||
|
}
|
||||||
|
|
||||||
|
//print_r($councils);
|
||||||
|
|
||||||
|
$pageContent = $renderer->renderPledgePage($councils, $lga_pages, $pledgeCandidates);
|
||||||
|
|
||||||
|
if ($pageContent === null) {
|
||||||
|
exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo $pageContent;
|
||||||
|
exit(0);
|
||||||
143
pledge-update/template.php
Normal file
143
pledge-update/template.php
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
<?php
|
||||||
|
function sluggify($input) {
|
||||||
|
return strtolower(str_replace(' ', '-', $input));
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!-- wp:paragraph -->
|
||||||
|
<p>The Streets People Love campaign offers federal candidates the opportunity to take the following pledge:</p>
|
||||||
|
<!-- /wp:paragraph -->
|
||||||
|
|
||||||
|
<!-- wp:paragraph {"style":{"color":{"background":"#10B5B0"}}} -->
|
||||||
|
<p class="has-background" style="background-color:#10B5B0">If elected, I pledge to vote for a national Active Transport Infrastructure Program that invests $400 million annually, equivalent to $15 per person, for the duration of the <a href="https://www.betterstreets.org.au/2025-federal-election">United Nations Decade of Sustainable Transport</a>, from 2026 to 2035.</p>
|
||||||
|
<!-- /wp:paragraph -->
|
||||||
|
|
||||||
|
<!-- wp:paragraph -->
|
||||||
|
<p>Candidates from these divisions have taken the pledge:</p>
|
||||||
|
<!-- /wp:paragraph -->
|
||||||
|
|
||||||
|
<?php
|
||||||
|
$councilCount = count($councils);
|
||||||
|
?>
|
||||||
|
|
||||||
|
<?php if ($councilCount > 1): ?>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
if ($councilCount > 8) {
|
||||||
|
$councilListChunkSize = ceil($councilCount / 2);
|
||||||
|
} else {
|
||||||
|
$councilListChunkSize = $councilCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
$councilChunks = array_chunk($councils, $councilListChunkSize);
|
||||||
|
|
||||||
|
?>
|
||||||
|
<!-- wp:columns {"className":"council-list-columns"} -->
|
||||||
|
<div class="wp-block-columns council-list-columns">
|
||||||
|
|
||||||
|
<?php for ($columnIdx = 0; $columnIdx < 4; $columnIdx++): ?>
|
||||||
|
<!-- wp:column {"verticalAlignment":"top","style":{"spacing":{"padding":{"top":"0","bottom":"0"}}}} -->
|
||||||
|
<div class="wp-block-column is-vertically-aligned-top" style="padding-top:0;padding-bottom:0">
|
||||||
|
|
||||||
|
<?php if (array_key_exists($columnIdx, $councilChunks)): ?>
|
||||||
|
<!-- wp:list {"style":{"spacing":{"margin":{"top":"0","right":"0","bottom":"0","left":"0"}}}} -->
|
||||||
|
<ul style="margin-top:0;margin-right:0;margin-bottom:0;margin-left:0" class="wp-block-list">
|
||||||
|
|
||||||
|
<?php foreach($councilChunks[$columnIdx] as $divisionName): ?>
|
||||||
|
<!-- wp:list-item -->
|
||||||
|
<li><a href="#<?php echo sluggify($divisionName); ?>"><?php echo $divisionName; ?></a></li>
|
||||||
|
<!-- /wp:list-item -->
|
||||||
|
<?php endforeach; ?>
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
<!-- /wp:list -->
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<!-- /wp:column -->
|
||||||
|
<?php endfor; ?>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<!-- /wp:columns -->
|
||||||
|
<?php else: ?>
|
||||||
|
<!-- wp:paragraph -->
|
||||||
|
<p></p>
|
||||||
|
<!-- /wp:paragraph -->
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php foreach ($councils as $key => $council): ?>
|
||||||
|
<!-- wp:heading {"level":3,"className":"is-style-default"} -->
|
||||||
|
<?php $groupSlug = sluggify($council); ?>
|
||||||
|
<h3 class="wp-block-heading is-style-default" id="<?php echo $groupSlug; ?>"><a style="text-decoration: none;" href="/<?php echo $lga_pages[$key]; ?>"><?php echo htmlspecialchars($council); ?></a></h3>
|
||||||
|
<!-- /wp:heading -->
|
||||||
|
|
||||||
|
<?php
|
||||||
|
$groupCandidates = array_filter($candidates, function ($candidate) use ($council) {
|
||||||
|
return $candidate['Council'] === $council;
|
||||||
|
});
|
||||||
|
|
||||||
|
?>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
$columnCount = 4;
|
||||||
|
|
||||||
|
$chunkedCouncilCandidates = array_chunk($groupCandidates, $columnCount);
|
||||||
|
?>
|
||||||
|
|
||||||
|
<?php foreach($chunkedCouncilCandidates as $chunk): ?>
|
||||||
|
<!-- wp:columns -->
|
||||||
|
<div class="wp-block-columns">
|
||||||
|
|
||||||
|
<?php for ($columnIdx = 0; $columnIdx < $columnCount; $columnIdx++): ?>
|
||||||
|
<!-- wp:column -->
|
||||||
|
<div class="wp-block-column">
|
||||||
|
|
||||||
|
<?php if (array_key_exists($columnIdx, $chunk)): ?>
|
||||||
|
<?php
|
||||||
|
$candidate = $chunk[$columnIdx];
|
||||||
|
|
||||||
|
if (isset($candidate['Elected']) && $candidate['Elected'] === 'y') {
|
||||||
|
$candidate_elected = true;
|
||||||
|
} else {
|
||||||
|
$candidate_elected = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($candidate['Picture']) && strlen($candidate['image_url'])) {
|
||||||
|
$candidate_image['url'] = $candidate['image_url'];
|
||||||
|
$candidate_image['id'] = $candidate['image_id'];
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!-- wp:image {"id":<?php echo $candidate_image['id']; ?>,"width":"200px","height":"200px","scale":"cover","align":"center","style":{"color":{}},"className":"is-resized"} -->
|
||||||
|
<figure class="wp-block-image aligncenter is-resized <?php if ($candidate_elected) { echo "elected-candidate"; } ?>"><img src="<?php echo $candidate_image['url']; ?>" alt="" class="wp-image-<?php echo $candidate_image['id']; ?>" style="object-fit:cover;width:200px;height:200px;"/>
|
||||||
|
<?php if ($candidate_elected): ?>
|
||||||
|
<figcaption>ELECTED</figcaption>
|
||||||
|
<?php endif; ?>
|
||||||
|
</figure>
|
||||||
|
<!-- /wp:image -->
|
||||||
|
|
||||||
|
<!-- wp:heading {"textAlign":"center","className":"wp-block-heading has-text-align-center has-medium-font-size","style":{"spacing":{"margin":{"top":"1rem","bottom":"0.5rem"}}}} -->
|
||||||
|
<h2 class="wp-block-heading has-text-align-center has-medium-font-size" style="margin-top:1rem;margin-bottom:0.5rem"><strong><?php echo htmlspecialchars($candidate['Candidate Name']); ?></strong></h2>
|
||||||
|
<!-- /wp:heading -->
|
||||||
|
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<!-- /wp:column -->
|
||||||
|
<?php endfor; ?>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<!-- /wp:columns -->
|
||||||
|
<?php endforeach; ?>
|
||||||
|
|
||||||
|
<?php endforeach; ?>
|
||||||
|
|
||||||
|
<?php if (isset($config['footer'])): ?>
|
||||||
|
<!-- wp:paragraph -->
|
||||||
|
<p><?php echo $config['footer']; ?></p>
|
||||||
|
<!-- /wp:paragraph -->
|
||||||
|
<?php endif; ?>
|
||||||
15
results/fetch.sh
Executable file
15
results/fetch.sh
Executable file
@@ -0,0 +1,15 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
mkdir -p html
|
||||||
|
wget https://www.vec.vic.gov.au/results/2024-council-election-results -O html/lga_list.html
|
||||||
|
|
||||||
|
IFS=$'\n'
|
||||||
|
|
||||||
|
lgas=$(grep 'href="/voting/.*/results"' html/lga_list.html)
|
||||||
|
|
||||||
|
for lga in $lgas ; do
|
||||||
|
lga=$(sed 's|.*href="|https://www.vec.vic.gov.au|' <<< $lga)
|
||||||
|
lga=$(sed 's|">.*||' <<< $lga)
|
||||||
|
file=$(sed 's|.*elections/||' <<< $lga | sed s'|/results||')
|
||||||
|
wget $lga -O html/$file
|
||||||
|
done
|
||||||
142
results/gen-elected.php
Normal file
142
results/gen-elected.php
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
$options = getopt("", ["candidates-files:", "results-file:"]);
|
||||||
|
|
||||||
|
if (isset($options['candidates-files'])) {
|
||||||
|
$candidates_files = $options['candidates-files'];
|
||||||
|
} else {
|
||||||
|
error_log("Error: Missing required option '--candidates-files'.");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($options['results-file'])) {
|
||||||
|
$results_file = $options['results-file'];
|
||||||
|
$results_string = file_get_contents($results_file);
|
||||||
|
|
||||||
|
if ($results_string !== FALSE) {
|
||||||
|
$results = json_decode($results_string, true);
|
||||||
|
} else {
|
||||||
|
error_log("Error opening results.json.");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
error_log("Error: Missing required option '--results-file'.");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function trim_sluggify($input) {
|
||||||
|
return strtolower(str_replace(' ', '-', trim($input)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function match_words($words, $list) {
|
||||||
|
/* Match database names to VEC names */
|
||||||
|
$max_score = 0;
|
||||||
|
$best_match = "no match";
|
||||||
|
foreach ($list as $possible_match) {
|
||||||
|
$aa = preg_split("/[^a-z]/", strtolower($words));
|
||||||
|
$bb = preg_split("/[^a-z]/", strtolower($possible_match));
|
||||||
|
|
||||||
|
$score_sum = 0;
|
||||||
|
foreach ($aa as $a) {
|
||||||
|
foreach ($bb as $b) {
|
||||||
|
similar_text($a, $b, $score);
|
||||||
|
if ($score > 70) $score_sum += $score;
|
||||||
|
else $score_sum -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($score_sum > $max_score) {
|
||||||
|
$max_score = $score_sum;
|
||||||
|
$best_match = $possible_match;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return array($max_score, $best_match);
|
||||||
|
}
|
||||||
|
|
||||||
|
$candidates_files = explode(" ", $candidates_files);
|
||||||
|
|
||||||
|
/* Generate dictionary of candidates and LGAs */
|
||||||
|
$candidate_data = [];
|
||||||
|
foreach ($candidates_files as $file) {
|
||||||
|
$config_file = dirname($file)."/config.json";
|
||||||
|
$config_string = file_get_contents($config_file);
|
||||||
|
|
||||||
|
if ($config_string !== FALSE) {
|
||||||
|
$config = json_decode($config_string, true);
|
||||||
|
} else {
|
||||||
|
error_log("Error opening config.json.");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$candidate_data[$config['councilName']]['_filename'] = $file;
|
||||||
|
|
||||||
|
if (($handle = fopen($file, "r")) !== FALSE) {
|
||||||
|
$headers = fgetcsv($handle);
|
||||||
|
while (($data = fgetcsv($handle)) !== FALSE) {
|
||||||
|
$candidate = [];
|
||||||
|
foreach ($headers as $key => $value) {
|
||||||
|
$candidate[$value] = $data[$key];
|
||||||
|
}
|
||||||
|
$name_slug = trim_sluggify($candidate['Candidate Name']);
|
||||||
|
$candidate_data[$config['councilName']][$name_slug] = $candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$vec_lga_names = [];
|
||||||
|
foreach ($results as $lga => $data) {
|
||||||
|
$vec_lga_names[] = $lga;
|
||||||
|
}
|
||||||
|
|
||||||
|
function was_elected($candidate, $vec_wards) {
|
||||||
|
foreach ($vec_wards as $vec_candidates) {
|
||||||
|
list($score, $match) = match_words($candidate, $vec_candidates);
|
||||||
|
if ($score > 180) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$header = ["Ward", "Candidate Name", "Elected"];
|
||||||
|
|
||||||
|
foreach ($candidate_data as $lga => $db_candidates) {
|
||||||
|
/* Find LGA in results dict */
|
||||||
|
list($score, $vec_lga_name) = match_words($lga, $vec_lga_names);
|
||||||
|
$vec_wards = $results[$vec_lga_name];
|
||||||
|
|
||||||
|
$elected = [];
|
||||||
|
/* Go through database candidates and build list of elected candidates */
|
||||||
|
foreach ($db_candidates as $key => $value) {
|
||||||
|
if ($key === '_filename') {
|
||||||
|
$output_file = dirname($value)."/candidates-elected.csv";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (was_elected($value['Candidate Name'], $vec_wards)) {
|
||||||
|
$elected[] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Don't create file if none were elected. */
|
||||||
|
if (count($elected) === 0) continue;
|
||||||
|
|
||||||
|
if (($handle = fopen($output_file, "w")) === FALSE) {
|
||||||
|
error_log('Error opening output file');
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fputcsv($handle, $header) === FALSE) {
|
||||||
|
error_log('Error writing headers to output file');
|
||||||
|
exit(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($elected as $candidate) {
|
||||||
|
$line = array($candidate['Ward'], $candidate['Candidate Name'], "y");
|
||||||
|
if (fputcsv($handle, $line) === FALSE) {
|
||||||
|
error_log('Error writing candidate to output file');
|
||||||
|
exit(3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fclose($handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
exit(0);
|
||||||
53
results/parser.py
Normal file
53
results/parser.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
from bs4 import BeautifulSoup, Tag as HTMLTag
|
||||||
|
import json, re, argparse
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument('filenames', nargs='*')
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
def get_vacancies(ward):
|
||||||
|
text = ward.parent.parent.h2.text
|
||||||
|
ward_name = re.search("[^\(]*", text)[0].strip()
|
||||||
|
vacancies = int(re.search("\([0-9]+", text)[0].strip("("))
|
||||||
|
return (ward_name, vacancies, ward)
|
||||||
|
|
||||||
|
def get_candidate_names(ward_desc):
|
||||||
|
names = []
|
||||||
|
for sibling in ward_desc[2].parent.next_siblings:
|
||||||
|
if not isinstance(sibling, HTMLTag):
|
||||||
|
continue
|
||||||
|
if not (blocks := sibling.find_all('td', class_="list-item-body")):
|
||||||
|
continue
|
||||||
|
for block in blocks:
|
||||||
|
names.append(re.sub('\n.*', '', block.text.strip()))
|
||||||
|
return names
|
||||||
|
|
||||||
|
def parse_lga(filename):
|
||||||
|
with open(filename, 'r') as results_fp:
|
||||||
|
html_doc = results_fp.read()
|
||||||
|
|
||||||
|
soup = BeautifulSoup(html_doc, 'html.parser')
|
||||||
|
wards0 = soup.find_all(string="Successful candidates")
|
||||||
|
wards1 = soup.find_all(string="Elected candidates")
|
||||||
|
|
||||||
|
ward_info = []
|
||||||
|
for ward in wards0:
|
||||||
|
ward_info.append(get_vacancies(ward))
|
||||||
|
for ward in wards1:
|
||||||
|
ward_info.append(get_vacancies(ward))
|
||||||
|
|
||||||
|
results = {}
|
||||||
|
for ward in ward_info:
|
||||||
|
names = get_candidate_names(ward)
|
||||||
|
assert len(names) == ward[1]
|
||||||
|
results[ward[0]] = names
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
all_results = {}
|
||||||
|
for lga in args.filenames:
|
||||||
|
lga_name = re.sub('html/lgas/', '', lga)
|
||||||
|
results = parse_lga(lga)
|
||||||
|
all_results[lga_name] = results
|
||||||
|
|
||||||
|
print(json.dumps(all_results, indent=4))
|
||||||
22
update-elected.sh
Executable file
22
update-elected.sh
Executable file
@@ -0,0 +1,22 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# This script uses the jq, wp, and php commands, make sure they are installed before running this script.
|
||||||
|
|
||||||
|
# The folder containing data for each council.
|
||||||
|
# Includes the list of candidates and any media.
|
||||||
|
DATA_PATH="../spl-data"
|
||||||
|
|
||||||
|
# Iterate over folders in data path
|
||||||
|
candidates_files=()
|
||||||
|
for folder in "$DATA_PATH"/*; do
|
||||||
|
if test -f "$folder"/candidates-generic.csv; then
|
||||||
|
candidates_files+=("$folder"/candidates-generic.csv)
|
||||||
|
fi
|
||||||
|
# Community groups get priority
|
||||||
|
if test -f "$folder"/candidates.csv; then
|
||||||
|
candidates_files+=("$folder"/candidates.csv)
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
php results/gen-elected.php --candidates-files "${candidates_files[*]}" \
|
||||||
|
--results-file $DATA_PATH/results.json
|
||||||
31
update-pledges.sh
Executable file
31
update-pledges.sh
Executable file
@@ -0,0 +1,31 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# If this runs as a cron job - might want to limit the number of revisions
|
||||||
|
# wordpress stores:
|
||||||
|
# wp-config.php:
|
||||||
|
# define( 'WP_POST_REVISIONS', 3 );
|
||||||
|
|
||||||
|
#wp post list --post_type=page
|
||||||
|
#wp post get 426 --field=content > current-homepage
|
||||||
|
#wp post get 1409 --field=content > movie-homepage
|
||||||
|
#wp post create --post_type=page --post_title="test_pledge" movie-homepage
|
||||||
|
#wp post update 1803 ../spl-data/movie-homepage
|
||||||
|
|
||||||
|
DATA_PATH="../spl-data/federal_2025"
|
||||||
|
|
||||||
|
candidates_files=()
|
||||||
|
for folder in "$DATA_PATH"/*; do
|
||||||
|
if test -f "$folder"/candidates-generic.csv; then
|
||||||
|
candidates_files+=("$folder"/candidates-generic.csv)
|
||||||
|
fi
|
||||||
|
# Community groups get priority
|
||||||
|
if test -f "$folder"/candidates.csv; then
|
||||||
|
candidates_files+=("$folder"/candidates.csv)
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
pledge_sed=$(php pledge-update/homepage.php --candidates-files "${candidates_files[*]}")
|
||||||
|
|
||||||
|
content=$(sed "$pledge_sed" ../spl-data/movie-homepage)
|
||||||
|
|
||||||
|
echo "$content" | wp post update 1803 -
|
||||||
2
upload-media.sh
Normal file → Executable file
2
upload-media.sh
Normal file → Executable file
@@ -7,7 +7,7 @@
|
|||||||
# Additionally, make sure the wp-cli/restful package is installed in the wp command (via "wp package install wp-cli/restful")
|
# Additionally, make sure the wp-cli/restful package is installed in the wp command (via "wp package install wp-cli/restful")
|
||||||
|
|
||||||
# Controls the flags that are passed to every usage of the wp command.
|
# Controls the flags that are passed to every usage of the wp command.
|
||||||
WP_FLAGS="--allow-root --path=/var/www/html"
|
#WP_FLAGS="--allow-root --path=/var/www/html"
|
||||||
|
|
||||||
media_path="$1"
|
media_path="$1"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user