Improve label rendering on maps

This commit is contained in:
2024-08-25 11:31:03 +10:00
parent 6d4a49208a
commit c7d9465ba0
4 changed files with 157 additions and 143 deletions

BIN
map-generator/circle.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 354 B

4
map-generator/circle.svg Normal file
View 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

File diff suppressed because one or more lines are too long

View File

@@ -21,166 +21,176 @@ mapboxgl.accessToken = 'pk.eyJ1IjoibWF0dHl3YXkiLCJhIjoiY2x6eG9vMzZyMHY2cDJqb3M1O
const map = new mapboxgl.Map({ const map = new mapboxgl.Map({
container: 'map', container: 'map',
zoom: 10, zoom: 10,
style: 'mapbox://styles/mattyway/clzy2ozzf004k01pn840h9xdb', style: 'mapbox://styles/mattyway/cm03vw57q00fd01pn93wf2j7p',
center: [145.00724,-37.79011] center: [145.00724,-37.79011]
}); });
fetch("wards_withboundaries.json") map.on('load', () => {
.then(response => { // Load an image from an external URL.
response.json() map.loadImage('circle.png', (error, image) => {
.then((wardData) => { if (error) throw error;
const filteredWardData = wardData.filter((ward) => normaliseCouncilName(ward.parentElectorateName) == councilName);
var bounds = { map.addImage('blue-circle', image);
"west": undefined,
"south": undefined,
"east": undefined,
"north": undefined
}
function addToBounds(coordinate) { fetch("wards_withboundaries.json")
if (bounds.west == undefined || coordinate[0] < bounds.west) { .then(response => {
bounds.west = coordinate[0]; response.json()
} .then((wardData) => {
const filteredWardData = wardData.filter((ward) => normaliseCouncilName(ward.parentElectorateName) == councilName);
if (bounds.south == undefined || coordinate[1] < bounds.south) { var bounds = {
bounds.south = coordinate[1]; "west": undefined,
} "south": undefined,
"east": undefined,
"north": undefined
}
if (bounds.east == undefined || coordinate[0] > bounds.east) { function addToBounds(coordinate) {
bounds.east = coordinate[0]; if (bounds.west == undefined || coordinate[0] < bounds.west) {
} bounds.west = 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") { if (bounds.south == undefined || coordinate[1] < bounds.south) {
featureCollection.features[0].geometry.coordinates[0].forEach(coordinate => { bounds.south = coordinate[1];
addToBounds(coordinate); }
});
} if (bounds.east == undefined || coordinate[0] > bounds.east) {
if (featureCollection.features[0].geometry.type == "MultiPolygon") { bounds.east = coordinate[0];
featureCollection.features[0].geometry.coordinates.forEach(polygon => { }
polygon[0].forEach(coordinate => {
addToBounds(coordinate); 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 data // Add a line along the data
map.addSource("data_"+wardData.electorateId, { map.addLayer({
'type': 'geojson', 'id': "outline_"+wardData.electorateId,
'data': featureCollection 'type': 'line',
}); 'source': "data_"+wardData.electorateId,
'layout': {},
'paint': {
'line-color': '#0899fe',
'line-width': 3
}
});
// Add a line along the data var centrePoint;
map.addLayer({ if (featureCollection.features[0].geometry.type == "Polygon") {
'id': "outline_"+wardData.electorateId, centrePoint = polylabel(featureCollection.features[0].geometry.coordinates, 0.000001);
'type': 'line', }
'source': "data_"+wardData.electorateId, if (featureCollection.features[0].geometry.type == "MultiPolygon") {
'layout': {}, // TODO: Find the biggest polygon in the multipolygon and use that to find the centre point
'paint': { // instead of just picking the second polygon.
'line-color': '#0899fe', //
'line-width': 3 // 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);
}
var centrePoint; if (wardData.electorateName.includes(' ')) {
if (featureCollection.features[0].geometry.type == "Polygon") { // Breaking long names into newlines looks better
centrePoint = polylabel(featureCollection.features[0].geometry.coordinates, 0.000001); const parts = wardData.electorateName.split(' ');
} // Special case if a ward starts with "St" (like "St Albans East")
if (featureCollection.features[0].geometry.type == "MultiPolygon") { // Join the first two parts
// TODO: Find the biggest polygon in the multipolygon and use that to find the centre point if (parts[0] == "St") {
// instead of just picking the second polygon. parts[0] = parts[0] + ' ' + parts[1];
// parts.splice(1, 1);
// 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. const wardNameNewLines = parts.join('\n');
centrePoint = polylabel(featureCollection.features[0].geometry.coordinates[1], 0.000001); labelFeatures.push({
} 'type': 'Feature',
'properties': {
'description': wardNameNewLines
if (wardData.electorateName.includes(' ')) { },
// Breaking long names into newlines looks better 'geometry': {
const parts = wardData.electorateName.split(' '); 'type': 'Point',
// Special case if a ward starts with "St" (like "St Albans East") 'coordinates': centrePoint
// Join the first two parts }
if (parts[0] == "St") { });
parts[0] = parts[0] + ' ' + parts[1]; } else {
parts.splice(1, 1); labelFeatures.push({
} 'type': 'Feature',
const wardNameNewLines = parts.join('\n'); 'properties': {
labelFeatures.push({ 'description': wardData.electorateName
'type': 'Feature', },
'properties': { 'geometry': {
'description': wardNameNewLines 'type': 'Point',
}, 'coordinates': centrePoint
'geometry': { }
'type': 'Point', });
'coordinates': centrePoint
} }
}); });
} else {
labelFeatures.push({ map.addSource('labels', {
'type': 'Feature', 'type': 'geojson',
'properties': { 'data': {
'description': wardData.electorateName 'type': 'FeatureCollection',
}, 'features': labelFeatures
'geometry': {
'type': 'Point',
'coordinates': centrePoint
} }
}); });
}
});
map.addSource('labels', { map.addLayer({
'type': 'geojson', 'id': 'labels',
'data': { 'type': 'symbol',
'type': 'FeatureCollection', 'source': 'labels',
'features': labelFeatures '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.addLayer({ map.fitBounds([
'id': 'labels', [bounds.west, bounds.south],
'type': 'symbol', [bounds.east, bounds.north]
'source': 'labels', ], {
'layout': { padding: 25,
'text-field': ['get', 'description'], animate: false
'text-variable-anchor': ['center', 'top', 'bottom'], });
'text-radial-offset': 0.5,
'text-padding': 0,
'text-justify': 'auto',
'text-allow-overlap': false,
'text-ignore-placement': false,
}
});
map.fitBounds([ }).catch(err => {
[bounds.west, bounds.south], console.log(err);
[bounds.east, bounds.north] });
], {
padding: 25,
animate: false
});
}).catch(err => {
console.log(err);
}); });
});
});
});