Compare commits

..

5 Commits

Author SHA1 Message Date
8b3ddec283 Improve LGA template
- Use columns instead of grid so it looks better on mobile
- Add message if there are no candidates for a particular ward
- Tweak spacing of candidate name and ticks
2024-08-18 23:43:58 +10:00
7950bab0c9 Left align opening paragraphs of lga pages 2024-08-18 23:42:11 +10:00
1d012f5ff1 Allow generating page when a candidates.csv file doesn't exist 2024-08-18 23:41:50 +10:00
95b7228778 Created script to capture a map for each council 2024-08-17 23:59:25 +10:00
7436c808e3 Fix handling of MultiPolygon objects 2024-08-17 23:57:25 +10:00
8 changed files with 196 additions and 78 deletions

View 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);
}
})();

File diff suppressed because one or more lines are too long

View File

@@ -5,6 +5,8 @@
"packages": { "packages": {
"": { "": {
"dependencies": { "dependencies": {
"child_process": "^1.0.2",
"fs": "^0.0.1-security",
"polylabel": "^2.0.1", "polylabel": "^2.0.1",
"puppeteer-core": "^23.1.0" "puppeteer-core": "^23.1.0"
} }
@@ -201,6 +203,11 @@
"node": "*" "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": { "node_modules/chromium-bidi": {
"version": "0.6.4", "version": "0.6.4",
"resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.6.4.tgz", "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.6.4.tgz",
@@ -386,6 +393,11 @@
"pend": "~1.2.0" "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": { "node_modules/fs-extra": {
"version": "11.2.0", "version": "11.2.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz",

View File

@@ -1,5 +1,7 @@
{ {
"dependencies": { "dependencies": {
"child_process": "^1.0.2",
"fs": "^0.0.1-security",
"polylabel": "^2.0.1", "polylabel": "^2.0.1",
"puppeteer-core": "^23.1.0" "puppeteer-core": "^23.1.0"
} }

View File

@@ -13,3 +13,5 @@ To automatically compile the `src.js` file after every edit, run `webpack-cli --
Changes to `dist/main.js` should be committed so that other users don't need to install node. 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-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.

View File

@@ -38,6 +38,24 @@ fetch("wards_withboundaries.json")
"north": 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 = []; var labelFeatures = [];
filteredWardData.forEach(wardData => { filteredWardData.forEach(wardData => {
@@ -51,23 +69,18 @@ fetch("wards_withboundaries.json")
] ]
}; };
featureCollection.features[0].geometry.coordinates[0].forEach(coordinate => { if (featureCollection.features[0].geometry.type == "Polygon") {
if (bounds.west == undefined || coordinate[0] < bounds.west) { featureCollection.features[0].geometry.coordinates[0].forEach(coordinate => {
bounds.west = coordinate[0]; addToBounds(coordinate);
} });
}
if (bounds.south == undefined || coordinate[1] < bounds.south) { if (featureCollection.features[0].geometry.type == "MultiPolygon") {
bounds.south = coordinate[1]; featureCollection.features[0].geometry.coordinates.forEach(polygon => {
} polygon[0].forEach(coordinate => {
addToBounds(coordinate);
if (bounds.east == undefined || coordinate[0] > bounds.east) { });
bounds.east = coordinate[0]; });
} }
if (bounds.north == undefined || coordinate[1] > bounds.north) {
bounds.north = coordinate[1];
}
});
// Add data // Add data
map.addSource("data_"+wardData.electorateId, { map.addSource("data_"+wardData.electorateId, {
@@ -87,7 +100,19 @@ fetch("wards_withboundaries.json")
} }
}); });
const centrePoint = polylabel(featureCollection.features[0].geometry.coordinates, 0.000001); 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(' ')) { if (wardData.electorateName.includes(' ')) {
// Breaking long names into newlines looks better // Breaking long names into newlines looks better

View File

@@ -27,23 +27,23 @@ 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"] . ".");
exit(1);
}
if (empty($candidateData)) {
error_log("Failed to load any candidates for " . $councilData['shortName']);
} }
if (isset($options['media-file'])) { if (isset($options['media-file'])) {

View File

@@ -1,9 +1,9 @@
<!-- wp:paragraph {"align":"center"} --> <!-- wp:paragraph -->
<p class="has-text-align-center">The Streets People Love campaign has created scorecards for candidates in the 2024 council elections. Scorecards have been generated based on a candidate's engagement with the Streets People Love campaign, their commitment to our pledge, their responses to a survey and input from campaign members located in the local government area in which they are running.</p> <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 -->
<!-- wp:paragraph {"align":"center"} --> <!-- wp:paragraph -->
<p class="has-text-align-center">Have candidates in your local government area not yet taken part? Send your local candidates the <a href="https://forms.gle/gnDNyBiVC64tDo2Y7">Streets People Love Pledge and Survey</a> and ask them to complete it so that local residents can vote for the candidates who want to build the streets people love.</p> <p>Have candidates in your local government area not yet taken part? Send your local candidates the <a href="https://forms.gle/gnDNyBiVC64tDo2Y7">Streets People Love Pledge and Survey</a> and ask them to complete it so that local residents can vote for the candidates who want to build the streets people love.</p>
<!-- /wp:paragraph --> <!-- /wp:paragraph -->
<?php if (isset($media["header.jpg"])): ?> <?php if (isset($media["header.jpg"])): ?>
@@ -13,55 +13,80 @@
<?php endif ?> <?php endif ?>
<?php foreach ($config['wardNames'] as $index => $wardName): ?> <?php foreach ($config['wardNames'] as $index => $wardName): ?>
<!-- wp:heading {"level":3,"className":"is-style-default"} --> <!-- wp:heading {"level":3,"className":"is-style-default"} -->
<?php $wardSlug = strtolower(str_replace(' ', '-', $wardName)); ?> <?php $wardSlug = strtolower(str_replace(' ', '-', $wardName)); ?>
<h3 class="wp-block-heading is-style-default" id="<?php echo $wardSlug; ?>"><a style="text-decoration: none;" href="#<?php echo $wardSlug; ?>"><?php echo $wardName; ?></a></h3> <h3 class="wp-block-heading is-style-default" id="<?php echo $wardSlug; ?>"><a style="text-decoration: none;" href="#<?php echo $wardSlug; ?>"><?php echo $wardName; ?></a></h3>
<!-- /wp:heading --> <!-- /wp:heading -->
<?php <?php
$wardCandidates = array_filter($candidates, function ($candidate) use ($wardName) { $wardCandidates = array_filter($candidates, function ($candidate) use ($wardName) {
return isset($candidate["Ward"]) && $candidate["Ward"] === $wardName; return isset($candidate["Ward"]) && $candidate["Ward"] === $wardName;
}); });
usort($wardCandidates, function($a, $b) { usort($wardCandidates, function($a, $b) {
if ($a == $b) { if ($a == $b) {
return 0; return 0;
} }
return (((int) $a) < ((int) $b)) ? 1 : -1; return (((int) $a) < ((int) $b)) ? 1 : -1;
}); });
if (count($wardCandidates) > 0):
?>
if (count($wardCandidates) == 0) continue;
?>
<!-- wp:group {"style":{"spacing":{"padding":{"top":"0","bottom":"7rem"}}},"layout":{"type":"grid","columnCount":4}} -->
<div class="wp-block-group" style="padding-top:0;padding-bottom:7rem">
<?php foreach ($wardCandidates as $index => $candidate): ?>
<!-- wp:group {"layout":{"type":"flex","orientation":"vertical","justifyContent":"center"}} -->
<div class="wp-block-group">
<?php <?php
if (isset($candidate['Picture']) && isset($media[$candidate['Picture']])) { $columnCount = 4;
$candidate_image = $media[$candidate['Picture']];
} else { $chunkedWardCandidates = array_chunk($wardCandidates, $columnCount);
$candidate_image = $media['default.png'];
}
?> ?>
<!-- wp:image {"id":<?php echo $candidate_image['id']; ?>,"width":"200px","height":"200px","scale":"cover","style":{"color":{}},"className":"is-resized"} --> <?php foreach($chunkedWardCandidates as $chunk): ?>
<figure class="wp-block-image is-resized"><img src="<?php echo $candidate_image['url']; ?>" alt="" class="wp-image-<?php echo $candidate_image['id']; ?>" style="object-fit:cover;width:200px;height:200px"/></figure> <!-- wp:columns -->
<!-- /wp:image --> <div class="wp-block-columns">
<!-- wp:heading {"fontSize":"medium"} --> <?php for ($columnIdx = 0; $columnIdx < $columnCount; $columnIdx++): ?>
<h2 class="wp-block-heading has-medium-font-size"><strong><?php echo $candidate['Candidate Name']; ?></strong></h2> <!-- wp:column -->
<!-- /wp:heading --> <div class="wp-block-column">
<!-- wp:paragraph {"style":{"layout":{"selfStretch":"fit","flexSize":null}},"fontSize":"large"} --> <?php if (array_key_exists($columnIdx, $chunk)): ?>
<p class="has-large-font-size" style="color: rgba(100%, 0%, 0%, 0); text-shadow: 0 0 0 green;"><?php echo str_repeat("✔️", $candidate['Rating']); ?></p> <?php
$candidate = $chunk[$columnIdx];
if (isset($candidate['Picture']) && isset($media[$candidate['Picture']])) {
$candidate_image = $media[$candidate['Picture']];
} else {
$candidate_image = $media['default.png'];
}
?>
<!-- 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"><img src="<?php echo $candidate_image['url']; ?>" alt="" class="wp-image-<?php echo $candidate_image['id']; ?>" style="object-fit:cover;width:200px;height:200px"/></figure>
<!-- /wp:image -->
<!-- wp:heading {"textAlign":"center","className":"wp-block-heading has-text-align-center has-medium-font-size","style":{"spacing":{"margin":{"top":"1rem"}}}} -->
<h2 class="wp-block-heading has-text-align-center has-medium-font-size" style="margin-top:1rem"><strong><?php echo $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" style="margin-top:0.5rem;margin-bottom:1.5rem;line-height:1;color: rgba(100%, 0%, 0%, 0);text-shadow: 0 0 0 green;"><?php echo str_repeat("✔️", $candidate['Rating']); ?></p>
<!-- /wp:paragraph -->
<?php endif; ?>
</div>
<!-- /wp:column -->
<?php endfor; ?>
</div>
<!-- /wp:columns -->
<?php endforeach; ?>
<?php else: ?>
<!-- wp:paragraph -->
<p>We don't know who the candidates are for <?php echo $wardName; ?> ward yet.</p>
<!-- /wp:paragraph --> <!-- /wp:paragraph -->
</div>
<!-- /wp:group -->
<?php endforeach; ?>
</div> <?php endif; ?>
<!-- /wp:group -->
<?php endforeach; ?> <?php endforeach; ?>
<?php if (isset($config['footer'])): ?> <?php if (isset($config['footer'])): ?>