Compare commits
164 Commits
f1aca43042
...
council_20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | |||
| fea2f69686 | |||
| 8ec704df11 | |||
| e08308377c | |||
| 3eb22d1068 | |||
| c6ff0e24b1 | |||
| 2980678e65 | |||
| 543531c57d | |||
| d2373369f3 | |||
| f9cd3d1427 | |||
| 25d0eecb3f | |||
| a293178250 | |||
| ec103e8ef5 | |||
| 6ec1ca8866 | |||
| 54023b372e | |||
| 1ef647ae94 | |||
| 436cda0c72 | |||
| 25224bbae8 | |||
| 2cab09f734 | |||
| fd7f698c06 | |||
| 6adc9eb7a7 | |||
| 01e1e2f33e | |||
| 74f4787cc5 | |||
| d04602332f | |||
| 78cb6bf077 | |||
| acb1c47d68 | |||
| db1a86c0b9 | |||
| f8fabe6dc1 | |||
| 7d613f7f0d | |||
| 295cb0e66d | |||
| 6d8e023807 | |||
| 8a1983a0f3 | |||
| 5257ca24dc | |||
| 397fdb1ede | |||
| e9a7de83cd | |||
| 4d9d633676 | |||
| 9d5ba8bd38 |
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"`)
|
||||||
30
bulk-upload-media.sh
Executable file
30
bulk-upload-media.sh
Executable file
@@ -0,0 +1,30 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Tries to upload media to wordpress and stores a json file with the ID and url of the media.
|
||||||
|
|
||||||
|
# This script uses the jq, and wp commands, make sure they are installed before running this script.
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
WP_FLAGS="--allow-root --path=/var/www/html"
|
||||||
|
|
||||||
|
path="$1"
|
||||||
|
|
||||||
|
if test -d "$path"; then
|
||||||
|
echo "Found $path, starting upload."
|
||||||
|
for file in "$path"/*/*.jpg; do
|
||||||
|
./upload-media.sh "$file"
|
||||||
|
done
|
||||||
|
for file in "$path"/*/*.jpeg; do
|
||||||
|
./upload-media.sh "$file"
|
||||||
|
done
|
||||||
|
for file in "$path"/*/*.png; do
|
||||||
|
./upload-media.sh "$file"
|
||||||
|
done
|
||||||
|
for file in "$path"/*/*.gif; do
|
||||||
|
./upload-media.sh "$file"
|
||||||
|
done
|
||||||
|
else
|
||||||
|
echo "Could not find $path"
|
||||||
|
fi
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
Council,Ward,Candidate Name,Rating
|
|
||||||
Brimbank,Harvester,Joe Blogs,1
|
|
||||||
Brimbank,Harvester,Jane Doe,5
|
|
||||||
|
@@ -1,3 +0,0 @@
|
|||||||
Council,Ward,Candidate Name,Rating
|
|
||||||
Brimbank,Harvester,Joe Blogs,1
|
|
||||||
Brimbank,Harvester,Jane Doe,5
|
|
||||||
|
19
copy-config.sh
Executable file
19
copy-config.sh
Executable file
@@ -0,0 +1,19 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Splits council_names.json into separate config.json files for the spl-data repo.
|
||||||
|
|
||||||
|
# This script uses the jq command, make sure it is installed before running this script.
|
||||||
|
|
||||||
|
data_path="$1"
|
||||||
|
|
||||||
|
if test -d "$data_path"; then
|
||||||
|
jq -c '.[] | .' ./council_names.json | while IFS=' ' read -r council_block; do
|
||||||
|
slug=$(echo "$council_block" | jq -r '.slug')
|
||||||
|
if ! test -d "$data_path"/"$slug"; then
|
||||||
|
mkdir "$data_path"/"$slug"
|
||||||
|
fi
|
||||||
|
echo "$council_block" | jq > "$data_path"/"$slug"/config.json
|
||||||
|
done
|
||||||
|
else
|
||||||
|
echo "Could not find $data_path"
|
||||||
|
fi
|
||||||
@@ -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",
|
||||||
@@ -973,16 +981,17 @@
|
|||||||
"slug": "yarra",
|
"slug": "yarra",
|
||||||
"councilName": "Yarra City Council",
|
"councilName": "Yarra City Council",
|
||||||
"wardNames": [
|
"wardNames": [
|
||||||
|
"MacKillop",
|
||||||
"Yarra Bend",
|
"Yarra Bend",
|
||||||
"Lennox",
|
"Lennox",
|
||||||
"Nicholls",
|
"Nicholls",
|
||||||
"Boulevard",
|
"Boulevard",
|
||||||
"Langridge",
|
"Langridge",
|
||||||
"MacKillop",
|
|
||||||
"Melba",
|
"Melba",
|
||||||
"Curtain",
|
"Curtain",
|
||||||
"Hoddle"
|
"Hoddle"
|
||||||
]
|
],
|
||||||
|
"footer": "Your local community group in the City of Yarra is <a href=\"https://streets-alive-yarra.org/journal/election-2024/\">Streets Alive Yarra Inc.</a>"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"shortName": "Yarra Ranges",
|
"shortName": "Yarra Ranges",
|
||||||
|
|||||||
144
csv-generic/gen-generic.php
Normal file
144
csv-generic/gen-generic.php
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
<?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['Pledge'] === "y") $score++;
|
||||||
|
if ($candidate['q1'] === "Yes") $score++;
|
||||||
|
if ($candidate['q3'] === "Yes") $score++;
|
||||||
|
if ($candidate['q4'] === "Yes") $score++;
|
||||||
|
if ($candidate['q7'] === "Yes") $score++;
|
||||||
|
|
||||||
|
$candidate_data[$key]['Score'] = $score;
|
||||||
|
}
|
||||||
|
|
||||||
|
$header = ["Ward", "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_lga'] === $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['match_ward'],
|
||||||
|
$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_lga'] = $candidate['match_lga'];
|
||||||
|
$image_map[$candidate['Photo']] = $map;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$json_data = json_encode($image_map);
|
||||||
|
|
||||||
|
print_r($json_data);
|
||||||
|
|
||||||
|
exit(0);
|
||||||
107
csv-generic/parse_generic_csv.php
Normal file
107
csv-generic/parse_generic_csv.php
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
<?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 ($value === "Verified") {
|
||||||
|
$is_question = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strstr($value, "candidate photo")) $value = "Photo";
|
||||||
|
if (strstr($value, "In which Local Government Area")) $value = "LGA";
|
||||||
|
if (strstr($value, "In which Ward")) $value = "Ward";
|
||||||
|
if (strstr($value, "Political Party")) $value = "Party";
|
||||||
|
|
||||||
|
if ($value === "Pledge") {
|
||||||
|
if (strstr($data[$key], "I pledge")) $data[$key] = "y";
|
||||||
|
else $data[$key] = "n";
|
||||||
|
}
|
||||||
|
|
||||||
|
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 === "Pledge") {
|
||||||
|
$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;
|
||||||
|
foreach ($lga_list as $lga) {
|
||||||
|
$aa = preg_split("/[^a-z]/", strtolower($candidate['LGA']));
|
||||||
|
$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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$candidate['match_lga'] = $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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
34
csv-normaliser/.devcontainer/devcontainer.json
Normal file
34
csv-normaliser/.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||||
|
// README at: https://github.com/devcontainers/templates/tree/main/src/php
|
||||||
|
{
|
||||||
|
"name": "PHP",
|
||||||
|
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
|
||||||
|
"image": "mcr.microsoft.com/devcontainers/php:1-8.3",
|
||||||
|
|
||||||
|
// Features to add to the dev container. More info: https://containers.dev/features.
|
||||||
|
// "features": {},
|
||||||
|
|
||||||
|
// Configure tool-specific properties.
|
||||||
|
// "customizations": {},
|
||||||
|
|
||||||
|
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||||
|
// "forwardPorts": [8000],
|
||||||
|
|
||||||
|
// Use 'portsAttributes' to set default properties for specific forwarded ports. More info: https://code.visualstudio.com/docs/remote/devcontainerjson-reference.
|
||||||
|
"portsAttributes": {
|
||||||
|
"8000": {
|
||||||
|
"label": "Hello Remote World",
|
||||||
|
"onAutoForward": "notify"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 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"
|
||||||
|
|
||||||
|
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
||||||
|
// "remoteUser": "root"
|
||||||
|
|
||||||
|
"mounts": [
|
||||||
|
"source=${localWorkspaceFolder}/../../spl-data,target=/workspaces/spl-data,type=bind,consistency=consistent"
|
||||||
|
]
|
||||||
|
}
|
||||||
14
csv-normaliser/.vscode/launch.json
vendored
Normal file
14
csv-normaliser/.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Launch application",
|
||||||
|
"type": "php",
|
||||||
|
"request": "launch",
|
||||||
|
"program": "${workspaceFolder}/main.php",
|
||||||
|
"args": ["--input", "${workspaceFolder}/test-input.csv", "--media", "${workspaceFolder}/test-media", "--output", "${workspaceFolder}/test-output.csv",],
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
"port": 9000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
211
csv-normaliser/main.php
Normal file
211
csv-normaliser/main.php
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
<?php
|
||||||
|
$options = getopt("", ["folder:", "input:", "output:", "media:"]);
|
||||||
|
|
||||||
|
if (isset($options['folder'])) {
|
||||||
|
$folder = $options['folder'];
|
||||||
|
if (is_dir($folder)) {
|
||||||
|
$expectedInputFileNames = [];
|
||||||
|
$expectedInputFileNames[] = str_replace("-", " ", strtoupper(basename($folder))) . ".csv";
|
||||||
|
$expectedInputFileNames[] = strtoupper(basename($folder)) . ".csv";
|
||||||
|
if (!isset($options['input'])) {
|
||||||
|
foreach ($expectedInputFileNames as $expectedInputFileName) {
|
||||||
|
$expectedInputFile = $folder . DIRECTORY_SEPARATOR . $expectedInputFileName;
|
||||||
|
if (is_file($expectedInputFile)) {
|
||||||
|
$options['input'] = $expectedInputFile;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!isset($options['output'])) {
|
||||||
|
$options['output'] = $folder . DIRECTORY_SEPARATOR . "candidates.csv";
|
||||||
|
}
|
||||||
|
if (!isset($options['media'])) {
|
||||||
|
$options['media'] = $folder;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
error_log("Error: Specified folder is not valid.");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($options['input'])) {
|
||||||
|
$inputFile = $options['input'];
|
||||||
|
} else {
|
||||||
|
error_log("Error: Missing required option '--input'.");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fclose($handle);
|
||||||
|
} else {
|
||||||
|
error_log('Error opening input file');
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($candidates)) {
|
||||||
|
error_log("Failed to find any candidates");
|
||||||
|
exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
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
|
||||||
|
62
get-generic.sh
Executable file
62
get-generic.sh
Executable file
@@ -0,0 +1,62 @@
|
|||||||
|
#!/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"
|
||||||
|
|
||||||
|
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_2024/Streets People Love council election candidate pledge and survey (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_lga\"]" <<< $image_map)
|
||||||
|
dst="$DATA_PATH/$lga/$key$suffix"
|
||||||
|
|
||||||
|
if [ -f $dst ] ; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Resizing $dst"
|
||||||
|
convert $IMAGES/$key -resize 400x400 $dst
|
||||||
|
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>") | .[]
|
||||||
50
lga-page.php
Normal file
50
lga-page.php
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<?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: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['councilName'] . "</a></li>\n");
|
||||||
|
print('<!-- /wp:list-item -->' . "\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
print('</ul> <!-- /wp:list -->');
|
||||||
|
|
||||||
|
exit(0);
|
||||||
65
make-council-pages.sh
Normal file → Executable file
65
make-council-pages.sh
Normal file → Executable file
@@ -2,16 +2,12 @@
|
|||||||
|
|
||||||
# This script uses the jq, wp, and php commands, make sure they are installed before running this script.
|
# This script uses the jq, wp, and php commands, make sure they are installed before running this script.
|
||||||
|
|
||||||
# The council_names.json file must contain a list of objects (one for each council).
|
# The folder containing data for each council.
|
||||||
# Each object must have the following fields: "shortName", "slug", "councilName", and "wardNames"
|
# Includes the list of candidates and any media.
|
||||||
# The "shortName" field must be a string.
|
DATA_PATH="../spl-data"
|
||||||
# The "slug" field must be a string.
|
|
||||||
# The "councilName" field must be a string.
|
|
||||||
# The "wardNames" field must be a list of strings.
|
|
||||||
JSON_FILE="council_names.json"
|
|
||||||
|
|
||||||
# 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"
|
||||||
@@ -21,7 +17,34 @@ function create_or_update_page() {
|
|||||||
|
|
||||||
slug=$(echo "$council_block" | jq -r '.slug')
|
slug=$(echo "$council_block" | jq -r '.slug')
|
||||||
|
|
||||||
content=$(echo "$council_block" | jq -c | php php-template/main.php --council-file "php://stdin" --candidates-file "candidates/$slug.csv")
|
media_inputs=()
|
||||||
|
for file in "$DATA_PATH"/$slug/*.{jpeg,jpg,png,gif}.json; do
|
||||||
|
if test -f "$file"; then
|
||||||
|
media_inputs+=("$file")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
for file in "$DATA_PATH"/*.{jpeg,jpg,png,gif}.json; do
|
||||||
|
if test -f "$file"; then
|
||||||
|
media_inputs+=("$file")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
jq -n '[inputs | { (input_filename | sub("\\.json$"; "") | sub("^.+/"; "")): . }] | reduce .[] as $item ({}; . + $item)' "${media_inputs[@]}" > "$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
|
||||||
|
|
||||||
@@ -30,23 +53,29 @@ 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
|
||||||
echo "Failed to generate page content for $short_name"
|
echo "Failed to generate page content for $short_name"
|
||||||
fi
|
fi
|
||||||
}
|
|
||||||
|
|
||||||
# Read JSON data
|
rm "$DATA_PATH"/$slug/media.json
|
||||||
data=$(cat "$JSON_FILE")
|
}
|
||||||
|
|
||||||
# Get all page IDs in one go because the wp command is pretty slow
|
# Get all page IDs in one go because the wp command is pretty slow
|
||||||
wp_posts=$(wp post list --post_type=page --format=json $WP_FLAGS)
|
wp_posts=$(wp post list --post_type=page --format=json $WP_FLAGS)
|
||||||
|
|
||||||
# Iterate over JSON objects
|
selected_council="$1"
|
||||||
jq -c '.[] | .' <<< "$data" | while IFS=' ' read -r council_block; do
|
|
||||||
short_name=$(echo "$council_block" | jq -r '.shortName')
|
# Iterate over folders in data path
|
||||||
page_id=$(echo $wp_posts | jq '.[] | select(.post_title == "'"$short_name"'") | .ID' | head -n 1)
|
for folder in "$DATA_PATH"/*; do
|
||||||
create_or_update_page "$council_block" "$page_id"
|
if test -f "$folder"/config.json; then
|
||||||
|
council_block=$(cat "$folder"/config.json | jq -c)
|
||||||
|
short_name=$(echo "$council_block" | jq -r '.shortName')
|
||||||
|
page_id=$(echo $wp_posts | jq '.[] | select(.post_title == "'"$short_name"'") | .ID' | head -n 1)
|
||||||
|
if [ ! "$selected_council" ] || [ "$short_name" = "$selected_council" ]; then
|
||||||
|
create_or_update_page "$council_block" "$page_id"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
done
|
done
|
||||||
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"
|
||||||
|
|
||||||
|
# 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 465 -
|
||||||
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"
|
||||||
|
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 12106 -
|
||||||
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.
|
||||||
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("wards_withboundaries.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[1], 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:"]);
|
$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,31 +27,67 @@ 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)) {
|
||||||
$headers = fgetcsv($handle);
|
if (($handle = fopen($candidatesFile, "r")) !== FALSE) {
|
||||||
while (($data = fgetcsv($handle)) !== FALSE) {
|
$headers = fgetcsv($handle);
|
||||||
$candidate = [];
|
while (($data = fgetcsv($handle)) !== FALSE) {
|
||||||
foreach ($headers as $key => $value) {
|
$candidate = [];
|
||||||
$candidate[$value] = $data[$key];
|
foreach ($headers as $key => $value) {
|
||||||
|
$candidate[$value] = $data[$key];
|
||||||
|
}
|
||||||
|
$candidateData[] = $candidate;
|
||||||
}
|
}
|
||||||
$candidateData[] = $candidate;
|
fclose($handle);
|
||||||
|
} else {
|
||||||
|
error_log('Error opening candidates file');
|
||||||
|
exit(1);
|
||||||
}
|
}
|
||||||
fclose($handle);
|
|
||||||
} else {
|
} else {
|
||||||
error_log('Error opening candidates file');
|
error_log("The specified candidates.csv file does not exist, will not show 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);
|
exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
$candidateData = array_filter($candidateData, function ($candidate) use ($councilData) {
|
$mediaData = json_decode($mediaFileContents, true);
|
||||||
return isset($candidate["Council"]) && $candidate["Council"] === $councilData['shortName'];
|
|
||||||
});
|
|
||||||
|
|
||||||
if (empty($candidateData)) {
|
// Merge elected data (if present) into candidate objects
|
||||||
error_log("Failed to load any candidates for " . $councilData['shortName']);
|
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);
|
$pageContent = $renderer->renderCouncilPage($councilData, $candidateData, $mediaData);
|
||||||
if ($pageContent === null) {
|
if ($pageContent === null) {
|
||||||
exit(2);
|
exit(2);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
class SPLPageRenderer {
|
class SPLPageRenderer {
|
||||||
public function renderCouncilPage($config, $candidates) {
|
public function renderCouncilPage($config, $candidates, $media) {
|
||||||
ob_start();
|
ob_start();
|
||||||
|
|
||||||
set_error_handler(function($errno, $errstr, $errfile, $errline) {
|
$didError = false;
|
||||||
// Clear any output if an error occurred
|
|
||||||
ob_clean();
|
set_error_handler(function($errno, $errstr, $errfile, $errline) use(&$didError) {
|
||||||
|
$didError = true;
|
||||||
error_log("Error: $errstr in $errfile on line $errline");
|
error_log("Error: $errstr in $errfile on line $errline");
|
||||||
return true; // Prevent default error handling
|
return true; // Prevent default error handling
|
||||||
});
|
});
|
||||||
@@ -17,8 +18,8 @@ class SPLPageRenderer {
|
|||||||
|
|
||||||
$content = ob_get_clean();
|
$content = ob_get_clean();
|
||||||
|
|
||||||
// Explictly return null if we didn't generate any content
|
// Explictly return null if we didn't generate any content or if there was an error
|
||||||
if (!empty($content)) {
|
if (!empty($content) && !$didError) {
|
||||||
return $content;
|
return $content;
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -1,39 +1,217 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
function sluggify($input) {
|
||||||
|
return strtolower(str_replace(' ', '-', $input));
|
||||||
|
}
|
||||||
|
|
||||||
|
$surveyLink = "<a href=\"https://forms.gle/gnDNyBiVC64tDo2Y7\">Streets People Love Pledge and Survey</a>";
|
||||||
|
if (isset($config["survey"])) {
|
||||||
|
$surveyLink = $config["survey"];
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
||||||
|
|
||||||
<!-- wp:paragraph -->
|
<!-- wp:paragraph -->
|
||||||
<p><?php echo $config['councilName']; ?></p>
|
<?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 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>
|
||||||
<!-- /wp:paragraph -->
|
<!-- /wp:paragraph -->
|
||||||
|
|
||||||
<?php foreach ($config['wardNames'] as $index => $wardName): ?>
|
<!-- wp:paragraph -->
|
||||||
<!-- wp:heading {"className":"is-style-default"} -->
|
<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>
|
||||||
<h2 class="wp-block-heading is-style-default" id="<?php echo strtolower(str_replace(' ', '-', $wardName)); ?>"><?php echo $wardName; ?></h2>
|
|
||||||
<!-- /wp:heading -->
|
<?php endif; ?>
|
||||||
|
<!-- /wp:paragraph -->
|
||||||
|
|
||||||
|
<?php if (isset($media["header.jpg"])): ?>
|
||||||
|
<!-- wp:image {"id":<?php echo $media["header.jpg"]['id']; ?>,"aspectRatio":"16/9","scale":"cover","sizeSlug":"full","linkDestination":"none"} -->
|
||||||
|
<figure class="wp-block-image size-full"><img src="<?php echo $media["header.jpg"]['url']; ?>" alt="" class="wp-image-<?php echo $media["header.jpg"]['id']; ?>" style="aspect-ratio:16/9;object-fit:cover"/></figure>
|
||||||
|
<!-- /wp:image -->
|
||||||
|
<?php endif ?>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$wardCount = count($config['wardNames']);
|
||||||
|
|
||||||
|
if ($wardCount > 1) {
|
||||||
|
$wardsDescription = $config['councilName'] . " is divided into " . $wardCount . " wards:";
|
||||||
|
} else {
|
||||||
|
$wardsDescription = $config['councilName'] . " is unsubdivided and does not contain any wards.";
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!-- wp:paragraph -->
|
||||||
|
<p><?php echo $wardsDescription; ?></p>
|
||||||
|
<!-- /wp:paragraph -->
|
||||||
|
|
||||||
|
<?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 {
|
||||||
if (count($wardCandidates) == 0) continue;
|
$groupNames = $config['wardNames'];
|
||||||
|
$propertyOnCandidate = 'Ward';
|
||||||
|
}
|
||||||
?>
|
?>
|
||||||
<!-- wp:group {"layout":{"type":"grid","columnCount":3}} -->
|
|
||||||
<div class="wp-block-group">
|
|
||||||
<?php foreach ($wardCandidates as $index => $candidate): ?>
|
|
||||||
<!-- wp:group {"layout":{"type":"flex","orientation":"vertical"}} -->
|
|
||||||
<div class="wp-block-group">
|
|
||||||
<!-- wp:heading {"fontSize":"medium"} -->
|
|
||||||
<h2 class="wp-block-heading has-medium-font-size"><?php echo $candidate['Candidate Name']; ?></h2>
|
|
||||||
<!-- /wp:heading -->
|
|
||||||
|
|
||||||
<!-- wp:image {"aspectRatio":"1","scale":"cover","style":{"color":{}}} -->
|
<?php foreach ($groupNames as $index => $groupName): ?>
|
||||||
<figure class="wp-block-image"><img alt="" style="aspect-ratio:1;object-fit:cover"/></figure>
|
<!-- wp:heading {"level":3,"className":"is-style-default"} -->
|
||||||
<!-- /wp:image -->
|
<?php $groupSlug = sluggify($groupName); ?>
|
||||||
|
<h3 class="wp-block-heading is-style-default" id="<?php echo $groupSlug; ?>"><a style="text-decoration: none;" href="#<?php echo $groupSlug; ?>"><?php echo htmlspecialchars($groupName); ?></a></h3>
|
||||||
|
<!-- /wp:heading -->
|
||||||
|
|
||||||
|
<?php
|
||||||
|
$groupCandidates = array_filter($candidates, function ($candidate) use ($groupName, $propertyOnCandidate) {
|
||||||
|
return isset($candidate[$propertyOnCandidate]) &&
|
||||||
|
strtolower($candidate[$propertyOnCandidate]) === strtolower($groupName);
|
||||||
|
});
|
||||||
|
|
||||||
|
usort($groupCandidates, function($a, $b) {
|
||||||
|
if ($a == $b) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return (((int) $a['Rating']) < ((int) $b['Rating'])) ? 1 : -1;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (count($groupCandidates) > 0):
|
||||||
|
?>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
$columnCount = 4;
|
||||||
|
|
||||||
|
$chunkedWardCandidates = array_chunk($groupCandidates, $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']])) {
|
||||||
|
$candidate_image = $media[$candidate['Picture']];
|
||||||
|
} else {
|
||||||
|
$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","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 -->
|
||||||
|
|
||||||
|
<!-- 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-text-align-center has-large-font-size candidate-ticks"><?php echo $candidate_rating; ?></p>
|
||||||
|
<!-- /wp:paragraph -->
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<!-- /wp:column -->
|
||||||
|
<?php endfor; ?>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<!-- /wp:columns -->
|
||||||
|
<?php endforeach; ?>
|
||||||
|
|
||||||
|
<?php else: ?>
|
||||||
|
|
||||||
<!-- wp:paragraph -->
|
<!-- wp:paragraph -->
|
||||||
<p>Lorem Ipsum</p>
|
<p>No candidates in this ward 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 -->
|
<!-- /wp:paragraph -->
|
||||||
</div>
|
|
||||||
<!-- /wp:group -->
|
|
||||||
<?php endforeach; ?>
|
|
||||||
|
|
||||||
</div>
|
<?php endif; ?>
|
||||||
<!-- /wp:group -->
|
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
|
|
||||||
|
<?php if (isset($config['footer'])): ?>
|
||||||
|
<!-- wp:paragraph -->
|
||||||
|
<p><?php echo $config['footer']; ?></p>
|
||||||
|
<!-- /wp:paragraph -->
|
||||||
|
<?php endif; ?>
|
||||||
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'] === 'y' && $candidate['Picture'] !== "";
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Select 9 random candidates */
|
||||||
|
$pledgeKeys = array_rand($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['councilName'];
|
||||||
|
$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'] === 'y';
|
||||||
|
});
|
||||||
|
|
||||||
|
$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 council 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 Councillor, I pledge to allocate budget and street space to build streets people love, and ensure that residents of all ages and abilities can safely move around our council area, irrespective of whether they choose to walk, cycle, wheel, use public transport or drive.</p>
|
||||||
|
<!-- /wp:paragraph -->
|
||||||
|
|
||||||
|
<!-- wp:paragraph -->
|
||||||
|
<p>Candidates from these councils 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 $councilName): ?>
|
||||||
|
<!-- wp:list-item -->
|
||||||
|
<li><a href="#<?php echo sluggify($councilName); ?>"><?php echo $councilName; ?></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"
|
||||||
|
|
||||||
|
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 -
|
||||||
31
upload-media.sh
Executable file
31
upload-media.sh
Executable file
@@ -0,0 +1,31 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Tries to upload media to wordpress and stores a json file with the ID and url of the media.
|
||||||
|
|
||||||
|
# This script uses the jq, and wp commands, make sure they are installed before running this script.
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
#WP_FLAGS="--allow-root --path=/var/www/html"
|
||||||
|
|
||||||
|
media_path="$1"
|
||||||
|
|
||||||
|
if test -f "$media_path"; then
|
||||||
|
if test -f "$media_path.json"; then
|
||||||
|
echo "Found $media_path.json, skipping uploading media."
|
||||||
|
else
|
||||||
|
echo "Could not find $media_path.json, uploading media!"
|
||||||
|
id=$(wp media import "$media_path" --porcelain $WP_FLAGS)
|
||||||
|
raw_url=$(wp rest attachment get "$id" --field=source_url $WP_FLAGS)
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
url=$(echo "$raw_url" | sed 's/127.0.0.1/streetspeoplelove.org/')
|
||||||
|
jq -n --arg id $id --arg url "$url" '{"id": $id, "url": $url}' > "$media_path.json"
|
||||||
|
cat "$media_path.json"
|
||||||
|
else
|
||||||
|
echo "Failed to upload $media_path"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Could not find $media_path"
|
||||||
|
fi
|
||||||
Reference in New Issue
Block a user