From 608ed0f92c15d0b42cdf6e6057d0e6745678cde7 Mon Sep 17 00:00:00 2001 From: Kim Taylor Date: Sun, 2 Mar 2025 23:14:50 +1100 Subject: [PATCH] Update map generator for federal election. --- map-generator/dist/main.js | 2 +- map-generator/shptojson/Makefile | 11 +++ map-generator/shptojson/shptojson.c | 142 ++++++++++++++++++++++++++++ map-generator/src.js | 4 +- 4 files changed, 156 insertions(+), 3 deletions(-) create mode 100644 map-generator/shptojson/Makefile create mode 100644 map-generator/shptojson/shptojson.c diff --git a/map-generator/dist/main.js b/map-generator/dist/main.js index cd940ce..9dfe755 100644 --- a/map-generator/dist/main.js +++ b/map-generator/dist/main.js @@ -16,7 +16,7 @@ \****************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { -eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var polylabel__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! polylabel */ \"./node_modules/polylabel/polylabel.js\");\n\r\n\r\nfunction normaliseCouncilName(str) {\r\n const regex = /(.*?)(?:(?: Rural)?(?: City| Shire| Borough) Council)/g;\r\n const matches = str.matchAll(regex);\r\n\r\n // If we get a match, convert to slug format\r\n for (const match of matches) {\r\n return match[1].toLowerCase().replace(\" \", \"-\");\r\n }\r\n\r\n // If we didn't find any matches, try convert input to slug format\r\n return str.toLowerCase().replace(\" \", \"-\");\r\n};\r\n\r\nconst searchParams = new URLSearchParams(window.location.search);\r\nconst councilName = normaliseCouncilName(searchParams.get(\"council\"));\r\nconsole.log(councilName);\r\n\r\nmapboxgl.accessToken = 'pk.eyJ1IjoibWF0dHl3YXkiLCJhIjoiY2x6eG9vMzZyMHY2cDJqb3M1ODZnNjF4cyJ9.IX8CfYQZUaQhSjWgMXmsEA';\r\nconst map = new mapboxgl.Map({\r\n container: 'map',\r\n zoom: 10,\r\n style: 'mapbox://styles/mattyway/cm03vw57q00fd01pn93wf2j7p',\r\n center: [145.00724,-37.79011]\r\n});\r\n\r\nmap.on('load', () => {\r\n // Load an image from an external URL.\r\n map.loadImage('circle.png', (error, image) => {\r\n if (error) throw error;\r\n\r\n map.addImage('blue-circle', image);\r\n\r\n fetch(\"wards_withboundaries.json\")\r\n .then(response => {\r\n response.json()\r\n .then((wardData) => {\r\n const filteredWardData = wardData.filter((ward) => normaliseCouncilName(ward.parentElectorateName) == councilName);\r\n\r\n var bounds = {\r\n \"west\": undefined,\r\n \"south\": undefined,\r\n \"east\": undefined,\r\n \"north\": undefined\r\n }\r\n\r\n function addToBounds(coordinate) {\r\n if (bounds.west == undefined || coordinate[0] < bounds.west) {\r\n bounds.west = coordinate[0];\r\n }\r\n\r\n if (bounds.south == undefined || coordinate[1] < bounds.south) {\r\n bounds.south = coordinate[1];\r\n }\r\n\r\n if (bounds.east == undefined || coordinate[0] > bounds.east) {\r\n bounds.east = coordinate[0];\r\n }\r\n\r\n if (bounds.north == undefined || coordinate[1] > bounds.north) {\r\n bounds.north = coordinate[1];\r\n }\r\n }\r\n\r\n var labelFeatures = [];\r\n\r\n filteredWardData.forEach(wardData => {\r\n const featureCollection = {\r\n 'type': 'FeatureCollection',\r\n 'features': [\r\n {\r\n 'type': 'Feature',\r\n 'geometry': JSON.parse(wardData.boundaryJson)\r\n }\r\n ]\r\n };\r\n\r\n if (featureCollection.features[0].geometry.type == \"Polygon\") {\r\n featureCollection.features[0].geometry.coordinates[0].forEach(coordinate => {\r\n addToBounds(coordinate);\r\n });\r\n }\r\n if (featureCollection.features[0].geometry.type == \"MultiPolygon\") {\r\n featureCollection.features[0].geometry.coordinates.forEach(polygon => {\r\n polygon[0].forEach(coordinate => {\r\n addToBounds(coordinate);\r\n });\r\n });\r\n }\r\n\r\n // Add data\r\n map.addSource(\"data_\"+wardData.electorateId, {\r\n 'type': 'geojson',\r\n 'data': featureCollection\r\n });\r\n\r\n // Add a line along the data\r\n map.addLayer({\r\n 'id': \"outline_\"+wardData.electorateId,\r\n 'type': 'line',\r\n 'source': \"data_\"+wardData.electorateId,\r\n 'layout': {},\r\n 'paint': {\r\n 'line-color': '#0899fe',\r\n 'line-width': 3\r\n }\r\n });\r\n\r\n var centrePoint;\r\n if (featureCollection.features[0].geometry.type == \"Polygon\") {\r\n centrePoint = (0,polylabel__WEBPACK_IMPORTED_MODULE_0__[\"default\"])(featureCollection.features[0].geometry.coordinates, 0.000001);\r\n }\r\n if (featureCollection.features[0].geometry.type == \"MultiPolygon\") {\r\n // TODO: Find the biggest polygon in the multipolygon and use that to find the centre point \r\n // instead of just picking the second polygon.\r\n //\r\n // The 2024 set of boundaries only uses 2 MultiPolygon objects (Cathedral in Murrindindi Shire Council and Island in Bass Coast Shire Council)\r\n // Luckily, the second polygon for both objects results in a good label placement.\r\n centrePoint = (0,polylabel__WEBPACK_IMPORTED_MODULE_0__[\"default\"])(featureCollection.features[0].geometry.coordinates[1], 0.000001);\r\n }\r\n \r\n\r\n if (wardData.electorateName.includes(' ')) {\r\n // Breaking long names into newlines looks better\r\n const parts = wardData.electorateName.split(' ');\r\n // Special case if a ward starts with \"St\" (like \"St Albans East\")\r\n // Join the first two parts\r\n if (parts[0] == \"St\") {\r\n parts[0] = parts[0] + ' ' + parts[1];\r\n parts.splice(1, 1);\r\n }\r\n const wardNameNewLines = parts.join('\\n');\r\n labelFeatures.push({\r\n 'type': 'Feature',\r\n 'properties': {\r\n 'description': wardNameNewLines\r\n },\r\n 'geometry': {\r\n 'type': 'Point',\r\n 'coordinates': centrePoint\r\n }\r\n });\r\n } else {\r\n labelFeatures.push({\r\n 'type': 'Feature',\r\n 'properties': {\r\n 'description': wardData.electorateName\r\n },\r\n 'geometry': {\r\n 'type': 'Point',\r\n 'coordinates': centrePoint\r\n }\r\n });\r\n }\r\n });\r\n\r\n map.addSource('labels', {\r\n 'type': 'geojson',\r\n 'data': {\r\n 'type': 'FeatureCollection',\r\n 'features': labelFeatures\r\n }\r\n });\r\n\r\n map.addLayer({\r\n 'id': 'labels',\r\n 'type': 'symbol',\r\n 'source': 'labels',\r\n 'layout': {\r\n 'text-field': ['get', 'description'],\r\n 'text-variable-anchor': ['top', 'left', 'bottom', 'right', 'top-left', 'top-right', 'bottom-left', 'bottom-right'],\r\n 'text-radial-offset': 1,\r\n 'text-padding': 0,\r\n 'text-justify': 'auto',\r\n 'text-allow-overlap': false,\r\n 'text-ignore-placement': false,\r\n 'icon-image': 'blue-circle'\r\n }\r\n });\r\n\r\n map.fitBounds([\r\n [bounds.west, bounds.south],\r\n [bounds.east, bounds.north]\r\n ], {\r\n padding: 25,\r\n animate: false\r\n });\r\n\r\n }).catch(err => {\r\n console.log(err);\r\n });\r\n });\r\n \r\n });\r\n});\n\n//# sourceURL=webpack:///./src.js?"); +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var polylabel__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! polylabel */ \"./node_modules/polylabel/polylabel.js\");\n\r\n\r\nfunction normaliseCouncilName(str) {\r\n const regex = /(.*?)(?:(?: Rural)?(?: City| Shire| Borough) Council)/g;\r\n const matches = str.matchAll(regex);\r\n\r\n // If we get a match, convert to slug format\r\n for (const match of matches) {\r\n return match[1].toLowerCase().replace(\" \", \"-\");\r\n }\r\n\r\n // If we didn't find any matches, try convert input to slug format\r\n return str.toLowerCase().replace(\" \", \"-\");\r\n};\r\n\r\nconst searchParams = new URLSearchParams(window.location.search);\r\nconst councilName = normaliseCouncilName(searchParams.get(\"council\"));\r\nconsole.log(councilName);\r\n\r\nmapboxgl.accessToken = 'pk.eyJ1IjoibWF0dHl3YXkiLCJhIjoiY2x6eG9vMzZyMHY2cDJqb3M1ODZnNjF4cyJ9.IX8CfYQZUaQhSjWgMXmsEA';\r\nconst map = new mapboxgl.Map({\r\n container: 'map',\r\n zoom: 10,\r\n style: 'mapbox://styles/mattyway/cm03vw57q00fd01pn93wf2j7p',\r\n center: [145.00724,-37.79011]\r\n});\r\n\r\nmap.on('load', () => {\r\n // Load an image from an external URL.\r\n map.loadImage('circle.png', (error, image) => {\r\n if (error) throw error;\r\n\r\n map.addImage('blue-circle', image);\r\n\r\n fetch(\"federal_2025_boundaries.json\")\r\n .then(response => {\r\n response.json()\r\n .then((wardData) => {\r\n const filteredWardData = wardData.filter((ward) => normaliseCouncilName(ward.parentElectorateName) == councilName);\r\n\r\n var bounds = {\r\n \"west\": undefined,\r\n \"south\": undefined,\r\n \"east\": undefined,\r\n \"north\": undefined\r\n }\r\n\r\n function addToBounds(coordinate) {\r\n if (bounds.west == undefined || coordinate[0] < bounds.west) {\r\n bounds.west = coordinate[0];\r\n }\r\n\r\n if (bounds.south == undefined || coordinate[1] < bounds.south) {\r\n bounds.south = coordinate[1];\r\n }\r\n\r\n if (bounds.east == undefined || coordinate[0] > bounds.east) {\r\n bounds.east = coordinate[0];\r\n }\r\n\r\n if (bounds.north == undefined || coordinate[1] > bounds.north) {\r\n bounds.north = coordinate[1];\r\n }\r\n }\r\n\r\n var labelFeatures = [];\r\n\r\n filteredWardData.forEach(wardData => {\r\n const featureCollection = {\r\n 'type': 'FeatureCollection',\r\n 'features': [\r\n {\r\n 'type': 'Feature',\r\n 'geometry': JSON.parse(wardData.boundaryJson)\r\n }\r\n ]\r\n };\r\n\r\n if (featureCollection.features[0].geometry.type == \"Polygon\") {\r\n featureCollection.features[0].geometry.coordinates[0].forEach(coordinate => {\r\n addToBounds(coordinate);\r\n });\r\n }\r\n if (featureCollection.features[0].geometry.type == \"MultiPolygon\") {\r\n featureCollection.features[0].geometry.coordinates.forEach(polygon => {\r\n polygon[0].forEach(coordinate => {\r\n addToBounds(coordinate);\r\n });\r\n });\r\n }\r\n\r\n // Add data\r\n map.addSource(\"data_\"+wardData.electorateId, {\r\n 'type': 'geojson',\r\n 'data': featureCollection\r\n });\r\n\r\n // Add a line along the data\r\n map.addLayer({\r\n 'id': \"outline_\"+wardData.electorateId,\r\n 'type': 'line',\r\n 'source': \"data_\"+wardData.electorateId,\r\n 'layout': {},\r\n 'paint': {\r\n 'line-color': '#0899fe',\r\n 'line-width': 3\r\n }\r\n });\r\n\r\n var centrePoint;\r\n if (featureCollection.features[0].geometry.type == \"Polygon\") {\r\n centrePoint = (0,polylabel__WEBPACK_IMPORTED_MODULE_0__[\"default\"])(featureCollection.features[0].geometry.coordinates, 0.000001);\r\n }\r\n if (featureCollection.features[0].geometry.type == \"MultiPolygon\") {\r\n // TODO: Find the biggest polygon in the multipolygon and use that to find the centre point \r\n // instead of just picking the second polygon.\r\n //\r\n // The 2024 set of boundaries only uses 2 MultiPolygon objects (Cathedral in Murrindindi Shire Council and Island in Bass Coast Shire Council)\r\n // Luckily, the second polygon for both objects results in a good label placement.\r\n centrePoint = (0,polylabel__WEBPACK_IMPORTED_MODULE_0__[\"default\"])(featureCollection.features[0].geometry.coordinates[0], 0.000001);\r\n }\r\n \r\n\r\n if (wardData.electorateName.includes(' ')) {\r\n // Breaking long names into newlines looks better\r\n const parts = wardData.electorateName.split(' ');\r\n // Special case if a ward starts with \"St\" (like \"St Albans East\")\r\n // Join the first two parts\r\n if (parts[0] == \"St\") {\r\n parts[0] = parts[0] + ' ' + parts[1];\r\n parts.splice(1, 1);\r\n }\r\n const wardNameNewLines = parts.join('\\n');\r\n labelFeatures.push({\r\n 'type': 'Feature',\r\n 'properties': {\r\n 'description': wardNameNewLines\r\n },\r\n 'geometry': {\r\n 'type': 'Point',\r\n 'coordinates': centrePoint\r\n }\r\n });\r\n } else {\r\n labelFeatures.push({\r\n 'type': 'Feature',\r\n 'properties': {\r\n 'description': wardData.electorateName\r\n },\r\n 'geometry': {\r\n 'type': 'Point',\r\n 'coordinates': centrePoint\r\n }\r\n });\r\n }\r\n });\r\n\r\n map.addSource('labels', {\r\n 'type': 'geojson',\r\n 'data': {\r\n 'type': 'FeatureCollection',\r\n 'features': labelFeatures\r\n }\r\n });\r\n\r\n map.addLayer({\r\n 'id': 'labels',\r\n 'type': 'symbol',\r\n 'source': 'labels',\r\n 'layout': {\r\n 'text-field': ['get', 'description'],\r\n 'text-variable-anchor': ['top', 'left', 'bottom', 'right', 'top-left', 'top-right', 'bottom-left', 'bottom-right'],\r\n 'text-radial-offset': 1,\r\n 'text-padding': 0,\r\n 'text-justify': 'auto',\r\n 'text-allow-overlap': false,\r\n 'text-ignore-placement': false,\r\n 'icon-image': 'blue-circle'\r\n }\r\n });\r\n\r\n map.fitBounds([\r\n [bounds.west, bounds.south],\r\n [bounds.east, bounds.north]\r\n ], {\r\n padding: 25,\r\n animate: false\r\n });\r\n\r\n }).catch(err => {\r\n console.log(err);\r\n });\r\n });\r\n \r\n });\r\n});\n\n//# sourceURL=webpack:///./src.js?"); /***/ }), diff --git a/map-generator/shptojson/Makefile b/map-generator/shptojson/Makefile new file mode 100644 index 0000000..b314a4a --- /dev/null +++ b/map-generator/shptojson/Makefile @@ -0,0 +1,11 @@ +SHPFILE := ../../../spl-data/federal_2025/aec_data/E_VIC24_region.shp +SHAPELIB := ../../../lib/shapelib-1.6.0 + +../federal_2025_boundaries.json: $(SHPFILE) shptojson + LD_LIBRARY_PATH=$(SHAPELIB)/.libs ./shptojson $(SHPFILE) > $@ + +CFLAGS := -I $(SHAPELIB) +LDFLAGS := -L $(SHAPELIB)/.libs + +shptojson: shptojson.c + gcc -Wall $(CFLAGS) $(LDFLAGS) -lshp $< -o $@ diff --git a/map-generator/shptojson/shptojson.c b/map-generator/shptojson/shptojson.c new file mode 100644 index 0000000..903a08d --- /dev/null +++ b/map-generator/shptojson/shptojson.c @@ -0,0 +1,142 @@ +/****************************************************************************** + * + * Project: Shapelib + * Purpose: Sample application for dumping contents of a shapefile to + * the terminal in human readable form. + * Author: Frank Warmerdam, warmerdam@pobox.com + * + ****************************************************************************** + * Copyright (c) 1999, Frank Warmerdam + * + * SPDX-License-Identifier: MIT OR LGPL-2.0-or-later + ****************************************************************************** + * + */ + +#include +#include +#include +#include +#include "shapefil.h" + +int main(int argc, char **argv) +{ + int nPrecision = 15; + int first; + const char *divison_name; + + /* -------------------------------------------------------------------- */ + /* Display a usage message. */ + /* -------------------------------------------------------------------- */ + if (argc != 2) + { + printf("Usage: shptojson shp_file\n"); + exit(1); + } + /* -------------------------------------------------------------------- */ + /* Open the passed shapefile. */ + /* -------------------------------------------------------------------- */ + SHPHandle hSHP = SHPOpen(argv[1], "rb"); + DBFHandle hDBF = DBFOpen(argv[1], "rb"); + if (hSHP == NULL || hDBF == NULL) + { + printf("Unable to open:%s\n", argv[1]); + exit(1); + } + + /* -------------------------------------------------------------------- */ + /* Print out the file bounds. */ + /* -------------------------------------------------------------------- */ + int nEntities; + int nShapeType; + double adfMinBound[4]; + double adfMaxBound[4]; + SHPGetInfo(hSHP, &nEntities, &nShapeType, adfMinBound, adfMaxBound); + + fprintf(stderr, "Shapefile Type: %s # of Shapes: %d\n\n", + SHPTypeName(nShapeType), nEntities); + + /* -------------------------------------------------------------------- */ + /* Skim over the list of shapes, printing all the vertices. */ + /* -------------------------------------------------------------------- */ + printf("[\n"); + for (int i = 0; i < nEntities; i++) + { + SHPObject *psShape = SHPReadObject(hSHP, i); + + if (psShape == NULL) + { + fprintf(stderr, + "Unable to read shape %d, terminating object reading.\n", + i); + break; + } + + divison_name = DBFReadStringAttribute(hDBF, i, 1); + fprintf(stderr, "shape %s has %i parts\n", divison_name, psShape->nParts); + + printf(" {\n"); + printf(" \"electorateId\": \"%08i\",\n", i); + printf(" \"electorateName\": \"%s\",\n", divison_name); + printf(" \"electorateType\": \"Division\",\n"); + printf(" \"parentElectorateName\": \"%s\",\n", divison_name); + printf(" \"boundaryJson\": \"{\\\"type\\\": "); + + if (psShape->nParts != 1) + { + printf("\\\"MultiPolygon\\\",\\\"coordinates\\\":[[["); + } + else + { + printf("\\\"Polygon\\\",\\\"coordinates\\\":[["); + } + + first = 1; + for (int j = 0, iPart = 1; j < psShape->nVertices; j++) + { + if (iPart < psShape->nParts && psShape->panPartStart[iPart] == j) + { + iPart++; + printf("]],[["); + } + else if (!first) + { + printf(","); + } + first = 0; + printf("[%.*g,%.*g]", nPrecision, psShape->padfX[j], + nPrecision, psShape->padfY[j]); + } + + if (i == (nEntities-1)) + { + if (psShape->nParts != 1) + { + printf("]]]}\"\n }\n"); + } + else + { + printf("]]}\"\n }\n"); + } + } + else + { + if (psShape->nParts != 1) + { + printf("]]]}\"\n },\n"); + } + else + { + printf("]]}\"\n },\n"); + } + } + + SHPDestroyObject(psShape); + } + printf("]\n"); + + SHPClose(hSHP); + DBFClose(hDBF); + + exit(0); +} diff --git a/map-generator/src.js b/map-generator/src.js index e8aa5fb..847dfc6 100644 --- a/map-generator/src.js +++ b/map-generator/src.js @@ -32,7 +32,7 @@ map.on('load', () => { map.addImage('blue-circle', image); - fetch("wards_withboundaries.json") + fetch("federal_2025_boundaries.json") .then(response => { response.json() .then((wardData) => { @@ -117,7 +117,7 @@ map.on('load', () => { // // 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); + centrePoint = polylabel(featureCollection.features[0].geometry.coordinates[0], 0.000001); }