wip
This commit is contained in:
@@ -9,7 +9,7 @@ mapboxgl.accessToken = 'pk.eyJ1Ijoiam9yZGl0b3N0IiwiYSI6ImQtcVkyclEifQ.vwKrOGZoZS
|
||||
let map = new mapboxgl.Map({
|
||||
container: 'map',
|
||||
style: 'mapbox://styles/mapbox/outdoors-v9',
|
||||
zoom: 12
|
||||
zoom: 14
|
||||
});
|
||||
let mapViewLabelController = new MapViewLabelController(map);
|
||||
// Get Mapbox map canvas container
|
||||
@@ -30,7 +30,6 @@ map.on('load', function () {
|
||||
}
|
||||
]
|
||||
};
|
||||
console.log(trackData);
|
||||
map.addSource('flight-track', {
|
||||
type: 'geojson',
|
||||
data: trackData
|
||||
@@ -76,7 +75,6 @@ function drawData(data) {
|
||||
map.on("moveend", update);
|
||||
}
|
||||
function update() {
|
||||
console.log("update");
|
||||
circles.attr("cx", function (d) {
|
||||
return mapViewLabelController.projectCoordinatesToPosition(d.geometry.coordinates).x;
|
||||
})
|
||||
|
||||
@@ -11,7 +11,7 @@ mapboxgl.accessToken = 'pk.eyJ1Ijoiam9yZGl0b3N0IiwiYSI6ImQtcVkyclEifQ.vwKrOGZoZS
|
||||
let map = new mapboxgl.Map({
|
||||
container: 'map',
|
||||
style: 'mapbox://styles/mapbox/outdoors-v9',
|
||||
zoom: 12
|
||||
zoom: 14
|
||||
});
|
||||
|
||||
let mapViewLabelController = new MapViewLabelController(map);
|
||||
@@ -37,8 +37,6 @@ map.on('load', function () {
|
||||
]
|
||||
};
|
||||
|
||||
console.log(trackData);
|
||||
|
||||
map.addSource('flight-track', {
|
||||
type: 'geojson',
|
||||
data: trackData
|
||||
@@ -100,9 +98,6 @@ function drawData(data : GeoJSON.FeatureCollection) {
|
||||
}
|
||||
|
||||
function update(){
|
||||
|
||||
console.log("update");
|
||||
|
||||
circles.attr("cx", function(d){
|
||||
return mapViewLabelController.projectCoordinatesToPosition((d.geometry as GeoJSON.Point).coordinates).x;
|
||||
})
|
||||
|
||||
@@ -94,12 +94,13 @@ class MapViewLabelController {
|
||||
.attr("stroke-width", 0.5)
|
||||
.attr("rx", 4)
|
||||
.attr("ry", 4);
|
||||
graphElem.attr("data-bbox-width", bbox.width);
|
||||
graphElem.attr("data-bbox-height", bbox.height);
|
||||
graphElem.attr("label-anchor", "top");
|
||||
}
|
||||
// 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) => {
|
||||
@@ -110,86 +111,180 @@ class MapViewLabelController {
|
||||
d3.select(nodes[i].parentNode).append(() => clone);
|
||||
});
|
||||
d3.selectAll("g.label").style("display", "none");
|
||||
d3.selectAll("g.label, g.labelclone")
|
||||
.attr("transform", (_, i, nodes) => {
|
||||
// Set the map projection transformation and remove not visible nodes
|
||||
const canvas = d3.select(".mapboxgl-canvas");
|
||||
const canvas_width = +canvas.attr("width");
|
||||
const canvas_height = +canvas.attr("height");
|
||||
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]);
|
||||
const translateVal = `translate(${p.x}, ${p.y + lblVerticalOffset})`;
|
||||
console.log(translateVal);
|
||||
return translateVal;
|
||||
if (p.x > 0 && p.y > 0 && p.x <= canvas_width && p.y <= canvas_height) {
|
||||
const translateVal = `translate(${p.x}, ${p.y})`;
|
||||
nodes[i].setAttribute("transform", translateVal);
|
||||
}
|
||||
else {
|
||||
nodes[i].remove();
|
||||
}
|
||||
});
|
||||
// Check if the label overlaps and if it does create a grouup with the overlaping labels.
|
||||
const labelNodes = d3.selectAll("g.labelclone").nodes();
|
||||
// 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]);
|
||||
const ol_result = this.isOverlapping(recta, rectb);
|
||||
if (ol_result.overlapping) {
|
||||
if (ol_result.bottom || ol_result.right) {
|
||||
group.unshift(node);
|
||||
}
|
||||
else {
|
||||
group.push(node);
|
||||
}
|
||||
groupFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (groupFound)
|
||||
if (groupFound) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// If any label group was found then create a new one with the label node
|
||||
if (!groupFound) {
|
||||
labelGroups.push([lNode]);
|
||||
labelGroups.push([node]);
|
||||
}
|
||||
}
|
||||
// Step 2: Merge labels in overlapping groups
|
||||
});
|
||||
// Merge labels in overlapping groups
|
||||
for (const group of labelGroups) {
|
||||
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 = {
|
||||
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";
|
||||
}
|
||||
this.createClusteredLabel(group);
|
||||
}
|
||||
this.setFinalPositioning();
|
||||
}
|
||||
}
|
||||
// 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) {
|
||||
return map.project(new mapboxgl.LngLat(+c[0], +c[1]));
|
||||
}
|
||||
// Get the x-y properties in a svg group element
|
||||
getElementTransform(el) {
|
||||
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, b) {
|
||||
const recta = a.getBoundingClientRect();
|
||||
const rectb = b.getBoundingClientRect();
|
||||
const aright_lt_bleft = recta.right < rectb.left;
|
||||
const abottom_lt_btop = recta.bottom < rectb.top;
|
||||
const aleft_gt_bright = recta.left > rectb.right;
|
||||
const atop_gt_bbottom = recta.top > rectb.bottom;
|
||||
const overlaping = !(aright_lt_bleft || aleft_gt_bright || abottom_lt_btop || atop_gt_bbottom);
|
||||
return overlaping;
|
||||
isOverlapping(recta, rectb) {
|
||||
const ol_y = (recta.bottom >= rectb.top) && (recta.top <= rectb.bottom);
|
||||
const ol_top = (recta.bottom > rectb.bottom) && ol_y;
|
||||
const ol_bottom = (recta.top < rectb.top) && ol_y;
|
||||
const ol_x = (recta.right >= rectb.left) && (recta.left <= rectb.right);
|
||||
const ol_left = (recta.right > rectb.right) && ol_x;
|
||||
const ol_right = (recta.left < rectb.left) && ol_x;
|
||||
const overlapping = ol_y && ol_x;
|
||||
const result = { overlapping: overlapping, top: ol_top, bottom: ol_bottom, left: ol_left, right: ol_right };
|
||||
return result;
|
||||
}
|
||||
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 = {
|
||||
type: "Feature",
|
||||
properties: {
|
||||
name: names.join('\n')
|
||||
},
|
||||
geometry: {
|
||||
type: "Point",
|
||||
coordinates: [avgLon, avgLat]
|
||||
}
|
||||
};
|
||||
this.appendLabels(mergedLabel, main);
|
||||
// Removes the labels in the group
|
||||
for (let i = 1; i < group.length; i++) {
|
||||
group[i].remove();
|
||||
}
|
||||
}
|
||||
getOverlapZone(el) {
|
||||
const overlappingOffset = 8;
|
||||
const transEl = this.getElementTransform(el);
|
||||
return { top: transEl.y - overlappingOffset, bottom: transEl.y + overlappingOffset, left: transEl.x - overlappingOffset, right: transEl.x + overlappingOffset };
|
||||
}
|
||||
getOverlappingZoneLabels(el) {
|
||||
let pos = this.getElementTransform(el);
|
||||
const anchor = el.getAttribute("label-anchor");
|
||||
const rect = d3.select(el).select("rect");
|
||||
const boxHeight = +rect.attr("height");
|
||||
let offsetWidth = +rect.attr("width");
|
||||
return { top: pos.y, bottom: pos.y + boxHeight, left: pos.x, right: pos.x + offsetWidth };
|
||||
}
|
||||
// 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, nodes);
|
||||
nodes[i].setAttribute("transform", translateVal);
|
||||
});
|
||||
}
|
||||
getOffsetPosition(index, nodes) {
|
||||
const el = nodes[index];
|
||||
const pos = this.getElementTransform(el);
|
||||
let markerHeight = 22;
|
||||
let markerWidth = 14;
|
||||
const boxHeight = +el.getAttribute("data-bbox-height");
|
||||
const boxWidth = +el.getAttribute("data-bbox-width");
|
||||
let anchor = el.getAttribute("label-anchor");
|
||||
// Check if there is an overlapping with the marker and the label.
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
if (i !== index) {
|
||||
let recta = this.getOverlappingZoneLabels(nodes[index]);
|
||||
let rectb = this.getOverlappingZoneLabels(nodes[i]);
|
||||
const ol_result = this.isOverlapping(recta, rectb);
|
||||
if (ol_result.overlapping) {
|
||||
console.log("index", index, "recta", recta, nodes[i]);
|
||||
console.log("i", i, "rectb", rectb, nodes[index]);
|
||||
console.log("overlapping", ol_result);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Top
|
||||
if (anchor === "top") {
|
||||
return `translate(${pos.x}, ${pos.y - boxHeight})`;
|
||||
}
|
||||
else if (anchor === "bottom") {
|
||||
return `translate(${pos.x}, ${pos.y + markerHeight})`;
|
||||
}
|
||||
else if (anchor === "left") {
|
||||
return `translate(${pos.x - (boxWidth / 2 + markerWidth)}, ${pos.y})`;
|
||||
}
|
||||
else { // right
|
||||
return `translate(${pos.x + (boxWidth / 2 + markerWidth)}, ${pos.y})`;
|
||||
}
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=MapViewLabelController.js.map
|
||||
@@ -116,6 +116,7 @@ class MapViewLabelController {
|
||||
|
||||
graphElem.attr("data-bbox-width", bbox.width);
|
||||
graphElem.attr("data-bbox-height", bbox.height);
|
||||
graphElem.attr("label-anchor", "top");
|
||||
}
|
||||
|
||||
// Update d3 shapes' positions to the map's current state
|
||||
@@ -135,12 +136,15 @@ class MapViewLabelController {
|
||||
d3.selectAll("g.label").style("display", "none");
|
||||
|
||||
// Set the map projection transformation and remove not visible nodes
|
||||
const canvas = d3.select(".mapboxgl-canvas");
|
||||
const canvas_width = +canvas.attr("width");
|
||||
const canvas_height = +canvas.attr("height");
|
||||
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]);
|
||||
|
||||
if(p.x > 0 && p.y >0){
|
||||
if(p.x > 0 && p.y >0 && p.x <= canvas_width && p.y <= canvas_height) {
|
||||
const translateVal = `translate(${p.x}, ${p.y})`;
|
||||
nodes[i].setAttribute("transform", translateVal);
|
||||
}
|
||||
@@ -151,7 +155,6 @@ class MapViewLabelController {
|
||||
|
||||
// Check if the marker overlaps and if it does create a group merging labels
|
||||
const labelGroups = [];
|
||||
|
||||
d3.selectAll("g.labelclone").each((_,i,nodes) => {
|
||||
const node = nodes[i];
|
||||
let groupFound = false;
|
||||
@@ -160,29 +163,39 @@ class MapViewLabelController {
|
||||
for (let j = 0; j < group.length; j++) {
|
||||
const recta = this.getOverlapZone(node);
|
||||
const rectb = this.getOverlapZone(group[j]);
|
||||
if (this.isOverlapping(recta, rectb)) {
|
||||
group.push(node);
|
||||
const ol_result = this.isOverlapping(recta, rectb);
|
||||
if (ol_result.overlapping) {
|
||||
if(ol_result.bottom || ol_result.right) {
|
||||
group.unshift(node);
|
||||
}
|
||||
else {
|
||||
group.push(node);
|
||||
}
|
||||
groupFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!groupFound){
|
||||
labelGroups.push([node]);
|
||||
if (groupFound){
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If any label group was found then create a new one with the label node
|
||||
if (!groupFound) {
|
||||
labelGroups.push([lNode]);
|
||||
labelGroups.push([node]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Step 2: Merge labels in overlapping groups
|
||||
// Merge labels in overlapping groups
|
||||
for (const group of labelGroups) {
|
||||
if (group.length === 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.createClusteredLabel(group)
|
||||
}
|
||||
|
||||
this.setFinalPositioning();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,6 +212,7 @@ class MapViewLabelController {
|
||||
return map.project(new mapboxgl.LngLat(+c[0], +c[1]));
|
||||
}
|
||||
|
||||
// Get the x-y properties in a svg group element
|
||||
getElementTransform(el : Element){
|
||||
const transform = el.getAttribute("transform");
|
||||
const match = /translate\(([^,]+),\s*([^)]+)\)/.exec(transform);
|
||||
@@ -208,17 +222,23 @@ class MapViewLabelController {
|
||||
|
||||
// Detect if two labels overlap
|
||||
isOverlapping(recta, rectb) {
|
||||
const ol_y = (recta.bottom >= rectb.top) && (recta.top <= rectb.bottom);
|
||||
const ol_top = (recta.bottom > rectb.bottom) && ol_y;
|
||||
const ol_bottom = (recta.top < rectb.top) && ol_y;
|
||||
|
||||
const aright_lt_bleft = recta.right < rectb.left;
|
||||
const abottom_lt_btop = recta.bottom < rectb.top;
|
||||
const aleft_gt_bright = recta.left > rectb.right;
|
||||
const atop_gt_bbottom = recta.top > rectb.bottom;
|
||||
const overlaping = !(aright_lt_bleft || aleft_gt_bright || abottom_lt_btop || atop_gt_bbottom);
|
||||
const ol_x = (recta.right >= rectb.left) && (recta.left <= rectb.right);
|
||||
const ol_left = (recta.right > rectb.right) && ol_x;
|
||||
const ol_right = (recta.left < rectb.left) && ol_x;
|
||||
|
||||
return overlaping;
|
||||
const overlapping = ol_y && ol_x;
|
||||
|
||||
const result = { overlapping: overlapping, top: ol_top, bottom: ol_bottom, left: ol_left, right: ol_right };
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
createClusteredLabel(group){
|
||||
|
||||
// Gather merged text and coordinates
|
||||
const names = group.map(el => el.querySelector("text")!.getAttribute("feature-name"));
|
||||
const coords = group.map(el => {
|
||||
@@ -240,26 +260,39 @@ class MapViewLabelController {
|
||||
},
|
||||
geometry: {
|
||||
type: "Point",
|
||||
coordinates: [avgLat, avgLon]
|
||||
coordinates: [avgLon, avgLat]
|
||||
}
|
||||
};
|
||||
|
||||
this.appendLabels(mergedLabel, main);
|
||||
|
||||
// Hide others
|
||||
// Removes the labels in the group
|
||||
for (let i = 1; i < group.length; i++) {
|
||||
group[i].remove();
|
||||
}
|
||||
}
|
||||
|
||||
getOverlapZone(el){
|
||||
const overlappingOffset = 0;
|
||||
const overlappingOffset = 8;
|
||||
const transEl = this.getElementTransform(el);
|
||||
|
||||
return { top: transEl.y - overlappingOffset, bottom: transEl.y + overlappingOffset, left: transEl.x - overlappingOffset, right: transEl.x + overlappingOffset };
|
||||
}
|
||||
|
||||
getOverlappingZoneLabels(el){
|
||||
let pos = this.getElementTransform(el);
|
||||
const anchor = el.getAttribute("label-anchor");
|
||||
|
||||
const rect = d3.select(el).select("rect");
|
||||
const boxHeight = +rect.attr("height");
|
||||
let offsetWidth = +rect.attr("width");
|
||||
|
||||
return { top: pos.y, bottom: pos.y + boxHeight, left: pos.x, right: pos.x + offsetWidth };
|
||||
}
|
||||
|
||||
// Repositions the final groups
|
||||
setFinalPositioning(){
|
||||
|
||||
// Sort by latitude
|
||||
let sortedLabels = d3.selectAll("g.labelclone").nodes()
|
||||
.sort((a,b) => {
|
||||
@@ -271,7 +304,7 @@ class MapViewLabelController {
|
||||
});
|
||||
|
||||
d3.selectAll("g.labelclone").each( (_, i, nodes) => {
|
||||
let translateVal = this.getOffsetPosition(i, node);
|
||||
let translateVal = this.getOffsetPosition(i, nodes);
|
||||
|
||||
nodes[i].setAttribute("transform", translateVal);
|
||||
});
|
||||
@@ -282,22 +315,39 @@ class MapViewLabelController {
|
||||
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");
|
||||
let markerWidth = 14;
|
||||
const boxHeight = +el.getAttribute("data-bbox-height");
|
||||
const boxWidth = +el.getAttribute("data-bbox-width");
|
||||
let anchor = el.getAttribute("label-anchor");
|
||||
|
||||
// Up
|
||||
return `translate(${pos.x}, ${pos.y - boxHeight})`;
|
||||
// Check if there is an overlapping with the marker and the label.
|
||||
for(let i = 0; i < nodes.length; i++ ){
|
||||
if(i !== index) {
|
||||
let recta = this.getOverlappingZoneLabels(nodes[index]);
|
||||
let rectb = this.getOverlappingZoneLabels(nodes[i]);
|
||||
|
||||
/*
|
||||
// Down
|
||||
return `translate(${pos.x}, ${pos.y - markerHeight})`;
|
||||
const ol_result = this.isOverlapping(recta, rectb);
|
||||
|
||||
// Left
|
||||
return `translate(${pos.x - (boxWidth/2 + markerWidth)}, ${pos.y})`;
|
||||
if(ol_result.overlapping) {
|
||||
console.log("index", index, "recta", recta, nodes[i]);
|
||||
console.log("i", i, "rectb", rectb, nodes[index]);
|
||||
console.log("overlapping", ol_result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Right
|
||||
return `translate(${pos.x + (boxWidth/2 + markerWidth)}, ${pos.y})`;
|
||||
*/
|
||||
// Top
|
||||
if(anchor === "top") {
|
||||
return `translate(${pos.x}, ${pos.y - boxHeight})`;
|
||||
}
|
||||
else if(anchor === "bottom") {
|
||||
return `translate(${pos.x}, ${pos.y + markerHeight})`;
|
||||
}
|
||||
else if(anchor === "left") {
|
||||
return `translate(${pos.x - (boxWidth/2 + markerWidth)}, ${pos.y})`;
|
||||
}
|
||||
else{ // right
|
||||
return `translate(${pos.x + (boxWidth/2 + markerWidth)}, ${pos.y})`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user