This commit is contained in:
2025-11-19 02:43:04 -06:00
parent ab8323e5d6
commit 2a81ab8b45
2 changed files with 124 additions and 54 deletions

View File

@@ -114,14 +114,13 @@ class MapViewLabelController {
.attr("rx", 4)
.attr("ry", 4);
graphElem.attr("data-bbox-width", bbox.width);
graphElem.attr("data-bbox-height", bbox.height);
}
// Update d3 shapes' positions to the map's current state
updateLabelsPosition() {
if (this.mapLabels !== null && this.mapLabels !== undefined) {
const lblVerticalOffset = 4;
// The original cloned svg elements are removed and new ones are created
d3.selectAll("g.labelclone").remove();
this.mapLabels.each((_, i, nodes) => {
@@ -135,33 +134,41 @@ class MapViewLabelController {
});
d3.selectAll("g.label").style("display", "none");
d3.selectAll("g.label, g.labelclone")
.attr("transform", (_, i, nodes) => {
const lon = Number(nodes[i].getAttribute("data-lon"));
const lat = Number(nodes[i].getAttribute("data-lat"));
const p = this.projectCoordinatesToPosition([lon, lat]);
const translateVal = `translate(${p.x}, ${p.y + lblVerticalOffset})`;
console.log(translateVal);
return translateVal;
});
// Set the map projection transformation and remove not visible nodes
d3.selectAll("g.labelclone").each((_, i, nodes) => {
const lon = Number(nodes[i].getAttribute("data-lon"));
const lat = Number(nodes[i].getAttribute("data-lat"));
const p = this.projectCoordinatesToPosition([lon, lat]);
// Check if the label overlaps and if it does create a grouup with the overlaping labels.
const labelNodes = d3.selectAll("g.labelclone").nodes();
if(p.x > 0 && p.y >0){
const translateVal = `translate(${p.x}, ${p.y})`;
nodes[i].setAttribute("transform", translateVal);
}
else {
nodes[i].remove();
}
});
// Check if the marker overlaps and if it does create a group merging labels
const labelGroups = [];
for (let i = 0; i < labelNodes.length; i++) {
const lNode = labelNodes[i];
d3.selectAll("g.labelclone").each((_,i,nodes) => {
const node = nodes[i];
let groupFound = false;
for (let group of labelGroups) {
for (let j = 0; j < group.length; j++) {
if (this.isOverlapping(lNode, group[j])) {
group.push(lNode);
const recta = this.getOverlapZone(node);
const rectb = this.getOverlapZone(group[j]);
if (this.isOverlapping(recta, rectb)) {
group.push(node);
groupFound = true;
break;
}
}
if (groupFound) break;
if (!groupFound){
labelGroups.push([node]);
}
}
// If any label group was found then create a new one with the label node
@@ -175,50 +182,32 @@ class MapViewLabelController {
if (group.length === 1) {
continue;
}
// Gather merged text and coordinates
const names = group.map(el => el.querySelector("text")!.getAttribute("feature-name"));
const coords = group.map(el => {
const transform = el.getAttribute("transform")!;
const match = /translate\(([^,]+),\s*([^)]+)\)/.exec(transform);
return { x: +match![1], y: +match![2] };
});
// Calculate the new position
const avgX = coords.reduce((sum, c) => sum + c.x, 0) / coords.length;
const avgY = Math.min(...coords.map(c => c.y));
// Create a new label with the combined text and append it to the svg elements.
const main = d3.select(group[0]);
const mergedLabel : GeoJSON.Feature<GeoJSON.Point> = {
type: "Feature",
properties: {
name: names.join('\n')
},
geometry: {
type: "Point",
coordinates: [avgX, avgY]
}
};
this.appendLabels(mergedLabel, main);
// Hide others
for (let i = 1; i < group.length; i++) {
group[i].style.display = "none";
}
}
}
}
// Gets the lat-long attributes from a svg group element
getElementLatLon(el){
const lat = +el.getAttribute("data-lat");
const lon = +el.getAttribute("data-lon");
return { lat: lat, lon: lon };
}
// Project GeoJSON coordinate to the map's current state
projectCoordinatesToPosition(c : number[]) {
return map.project(new mapboxgl.LngLat(+c[0], +c[1]));
}
getElementTransform(el : Element){
const transform = el.getAttribute("transform");
const match = /translate\(([^,]+),\s*([^)]+)\)/.exec(transform);
return { x: +match[1], y: +match[2] };
}
// Detect if two labels overlap
isOverlapping(a : SVGElement, b : SVGElement) {
const recta = a.getBoundingClientRect();
const rectb = b.getBoundingClientRect();
isOverlapping(recta, rectb) {
const aright_lt_bleft = recta.right < rectb.left;
const abottom_lt_btop = recta.bottom < rectb.top;
@@ -228,4 +217,87 @@ class MapViewLabelController {
return overlaping;
}
createClusteredLabel(group){
// Gather merged text and coordinates
const names = group.map(el => el.querySelector("text")!.getAttribute("feature-name"));
const coords = group.map(el => {
const latlon = this.getElementLatLon(el);
return { lat: latlon.lat, lon: latlon.lon };
});
// Calculate the new position
const avgLat = coords.reduce((sum, c) => sum + c.lat, 0) / coords.length;
const avgLon = Math.min(...coords.map(c => c.lon));
// Create a new label with the combined text and append it to the svg elements.
const main = d3.select(group[0]);
const mergedLabel : GeoJSON.Feature<GeoJSON.Point> = {
type: "Feature",
properties: {
name: names.join('\n')
},
geometry: {
type: "Point",
coordinates: [avgLat, avgLon]
}
};
this.appendLabels(mergedLabel, main);
// Hide others
for (let i = 1; i < group.length; i++) {
group[i].remove();
}
}
getOverlapZone(el){
const overlappingOffset = 0;
const transEl = this.getElementTransform(el);
return { top: transEl.y - overlappingOffset, bottom: transEl.y + overlappingOffset, left: transEl.x - overlappingOffset, right: transEl.x + overlappingOffset };
}
// Repositions the final groups
setFinalPositioning(){
// Sort by latitude
let sortedLabels = d3.selectAll("g.labelclone").nodes()
.sort((a,b) => {
let lonA = +a.getAttribute("data-lon");
let lonB = +a.getAttribute("data-lon");
let result = d3.ascending(lonA, lonB);
return result;
});
d3.selectAll("g.labelclone").each( (_, i, nodes) => {
let translateVal = this.getOffsetPosition(i, node);
nodes[i].setAttribute("transform", translateVal);
});
}
getOffsetPosition(index, nodes){
const el = nodes[index];
const pos = this.getElementTransform(el);
let markerHeight = 22;
let markerWidht = 14;
const boxHeight = +el.getAttribute("data-box-height");
const boxWidth = +el.getAttribute("data-box-width");
// Up
return `translate(${pos.x}, ${pos.y - boxHeight})`;
/*
// Down
return `translate(${pos.x}, ${pos.y - markerHeight})`;
// Left
return `translate(${pos.x - (boxWidth/2 + markerWidth)}, ${pos.y})`;
// Right
return `translate(${pos.x + (boxWidth/2 + markerWidth)}, ${pos.y})`;
*/
}
}

View File

@@ -17,9 +17,7 @@
"strict": true,
"jsx": "react-jsx",
"noUncheckedSideEffectImports": true,
"skipLibCheck": true
},
"include": [