Almost there. Adds typescript
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />
|
||||
<link href="https://api.tiles.mapbox.com/mapbox-gl-js/v3.0.1/mapbox-gl.css" rel="stylesheet" />
|
||||
<script src="https://api.tiles.mapbox.com/mapbox-gl-js/v3.0.1/mapbox-gl.js"></script>
|
||||
<script src="https://d3js.org/d3.v4.min.js"></script>
|
||||
<script src="https://d3js.org/d3.v7.min.js"></script>
|
||||
|
||||
<style media="screen">
|
||||
body { margin:0; padding:0; }
|
||||
@@ -26,285 +26,6 @@
|
||||
<body>
|
||||
<div id="map">
|
||||
</div>
|
||||
<script>
|
||||
|
||||
//////////////////
|
||||
// Mapbox stuff
|
||||
//////////////////
|
||||
|
||||
// Set-up map
|
||||
mapboxgl.accessToken = 'pk.eyJ1Ijoiam9yZGl0b3N0IiwiYSI6ImQtcVkyclEifQ.vwKrOGZoZSj3N-9MB6FF_A';
|
||||
var map = new mapboxgl.Map({
|
||||
container: 'map',
|
||||
style: 'mapbox://styles/mapbox/outdoors-v9',
|
||||
zoom: 11.5,
|
||||
center: [13.4426, 52.5100],
|
||||
});
|
||||
|
||||
|
||||
//////////////////////////
|
||||
// Mapbox+D3 Connection
|
||||
//////////////////////////
|
||||
|
||||
// Get Mapbox map canvas container
|
||||
var canvas = map.getCanvasContainer();
|
||||
|
||||
// Overlay d3 on the map
|
||||
var svg = d3.select(canvas).append("svg");
|
||||
|
||||
// Load map and dataset
|
||||
map.on('load', function () {
|
||||
d3.json("data/berlin-parks-new.json", function(err, data) {
|
||||
drawData(data);
|
||||
});
|
||||
});
|
||||
|
||||
// Project GeoJSON coordinate to the map's current state
|
||||
function project(d) {
|
||||
return map.project(new mapboxgl.LngLat(+d[0], +d[1]));
|
||||
}
|
||||
|
||||
|
||||
//////////////
|
||||
// D3 stuff
|
||||
//////////////
|
||||
|
||||
// Draw GeoJSON data with d3
|
||||
var circles;
|
||||
var labels;
|
||||
function drawData(data) {
|
||||
// Add circles
|
||||
circles = svg.selectAll("circle")
|
||||
.data(data.features)
|
||||
.enter()
|
||||
.append("circle")
|
||||
.attr("r", 8);
|
||||
/*.append("title") // append a title for tooltip
|
||||
.text(function(d) { return d.properties.name; });*/
|
||||
|
||||
// Add labels
|
||||
/*labels = svg.selectAll("text")
|
||||
.data(data.features)
|
||||
.enter()
|
||||
.append("text")
|
||||
.text(function(d) { return d.properties.name; }) // or any other property
|
||||
.attr("font-size", "12px")
|
||||
.attr("fill", "black")
|
||||
.attr("color", "white");*/
|
||||
|
||||
lablels = getMarkerLabels(data);
|
||||
|
||||
// Call the update function
|
||||
update();
|
||||
|
||||
// Update on map interaction
|
||||
map.on("viewreset", update);
|
||||
map.on("move", update);
|
||||
map.on("moveend", update);
|
||||
}
|
||||
|
||||
function getMarkerLabels(data){
|
||||
// Add labels with background
|
||||
const maxWidth = 100;
|
||||
|
||||
console.log("In markerlabels");
|
||||
const markerLabels = svg.selectAll("g.label")
|
||||
.data(data.features)
|
||||
.enter()
|
||||
.append("g")
|
||||
.attr("class", "label")
|
||||
.each(function(d) {
|
||||
const g = d3.select(this);
|
||||
const name = d.properties.name;
|
||||
|
||||
// Append the text
|
||||
const text = g.append("text")
|
||||
.attr("feature-name", d.properties.name)
|
||||
.attr("font-size", "12px")
|
||||
.attr("fill", "black")
|
||||
.attr("text-anchor", "middle")
|
||||
.attr("x", 0);
|
||||
|
||||
// Manually wrap text
|
||||
const words = name.split(/\s+/);
|
||||
let line = [];
|
||||
let lineNumber = 0;
|
||||
const lineHeight = 1.1; // ems
|
||||
const x = 0;
|
||||
const y = 0;
|
||||
|
||||
let tspan = text.append("tspan")
|
||||
.attr("x", x)
|
||||
.attr("y", y)
|
||||
.attr("dy", `${lineNumber * lineHeight}em`);
|
||||
|
||||
for (let i = 0; i < words.length; i++) {
|
||||
line.push(words[i]);
|
||||
tspan.text(line.join(" "));
|
||||
|
||||
// If text exceeds maxWidth, remove last word and start new line
|
||||
if (tspan.node().getComputedTextLength() > maxWidth) {
|
||||
line.pop();
|
||||
tspan.text(line.join(" "));
|
||||
line = [words[i]];
|
||||
|
||||
tspan = text.append("tspan")
|
||||
.attr("x", x)
|
||||
.attr("y", y)
|
||||
.attr("dy", `${++lineNumber * lineHeight}em`)
|
||||
.text(words[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// Measure the text
|
||||
const bbox = text.node().getBBox();
|
||||
|
||||
// Move both text and rect left by half the text width to center
|
||||
text.attr("x", -bbox.width / 2);
|
||||
|
||||
// Append rect behind text
|
||||
g.insert("rect", "text")
|
||||
.attr("x", bbox.x - 4)
|
||||
.attr("y", bbox.y - 2)
|
||||
.attr("width", bbox.width + 8)
|
||||
.attr("height", bbox.height + 4)
|
||||
.attr("fill", "white")
|
||||
.attr("stroke", "black")
|
||||
.attr("stroke-width", 0.5)
|
||||
.attr("rx", 4)
|
||||
.attr("ry", 4);
|
||||
|
||||
// Save bbox dimensions for use in `update()`
|
||||
//g.attr("data-bbox-width", bbox.width);
|
||||
g.attr("data-bbox-height", bbox.height);
|
||||
});
|
||||
|
||||
console.log("EIT");
|
||||
console.log(markerLabels);
|
||||
return markerLabels;
|
||||
}
|
||||
|
||||
// Update d3 shapes' positions to the map's current state
|
||||
function update() {
|
||||
const lblVerticalOffset = 4;
|
||||
//console.log("update");
|
||||
|
||||
circles.attr("cx", function(d) { return project(d.geometry.coordinates).x })
|
||||
.attr("cy", function(d) { return project(d.geometry.coordinates).y });
|
||||
/*labels.attr("x", function(d) { return project(d.geometry.coordinates).x - 8}) // offset to right of circle
|
||||
.attr("y", function(d) { return project(d.geometry.coordinates).y - 20 }); // vertically center text*/
|
||||
|
||||
labels
|
||||
.attr("transform", function(d) {
|
||||
const p = project(d.geometry.coordinates);
|
||||
const dy = -(+this.getAttribute("data-bbox-height") + lblVerticalOffset)
|
||||
return `translate(${p.x}, ${p.y + dy})`;
|
||||
});
|
||||
|
||||
//console.log(map.getZoom());
|
||||
|
||||
const labelNodes = labels.nodes();
|
||||
const mergedGroups = [];
|
||||
|
||||
for(let i = 0; i < labelNodes.length; i++){
|
||||
|
||||
const a = labelNodes[i];
|
||||
let groupFound = false;
|
||||
|
||||
for (let group of mergedGroups) {
|
||||
for (let j = 0; j < group.length; j++) {
|
||||
if (isOverlapping(a, group[j])) {
|
||||
console.log(a);
|
||||
group.push(a);
|
||||
groupFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (groupFound) break;
|
||||
}
|
||||
|
||||
if (!groupFound) {
|
||||
mergedGroups.push([a]);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Merge labels in overlapping groups
|
||||
for (const group of mergedGroups) {
|
||||
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] };
|
||||
});
|
||||
|
||||
// Average position
|
||||
const avgX = coords.reduce((sum, c) => sum + c.x, 0) / coords.length;
|
||||
const avgY = coords.reduce((sum, c) => sum + c.y, 0) / coords.length;
|
||||
|
||||
// Keep only the first label, update its text
|
||||
const main = group[0];
|
||||
const textEl = main.querySelector("text");
|
||||
|
||||
// Clear previous tspans or text
|
||||
textEl.innerHTML = "";
|
||||
|
||||
// Create a new <tspan> per name, each on a new line
|
||||
names.forEach((name, i) => {
|
||||
const tspan = document.createElementNS("http://www.w3.org/2000/svg", "tspan");
|
||||
tspan.setAttribute("x", 0);
|
||||
tspan.setAttribute("dy", i === 0 ? "0" : "1.2em"); // first line dy=0, next lines shift down
|
||||
tspan.textContent = name;
|
||||
textEl.appendChild(tspan);
|
||||
});
|
||||
main.setAttribute("transform", `translate(${avgX}, ${avgY})`);
|
||||
|
||||
// 3. Resize and reposition the <rect> background
|
||||
const textBBox = textEl.getBBox();
|
||||
const g = main; // the parent <g> element
|
||||
const rect = g.querySelector("rect");
|
||||
|
||||
if (rect) {
|
||||
rect.setAttribute("x", textBBox.x - 4);
|
||||
rect.setAttribute("y", textBBox.y - 2);
|
||||
rect.setAttribute("width", textBBox.width + 8);
|
||||
rect.setAttribute("height", textBBox.height + 4);
|
||||
}
|
||||
|
||||
// Optional: Update bbox height for offsetting in update()
|
||||
g.setAttribute("data-bbox-height", textBBox.height);
|
||||
|
||||
// Hide others
|
||||
for (let i = 1; i < group.length; i++) {
|
||||
group[i].style.display = "none";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function 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);
|
||||
|
||||
if(overlaping){
|
||||
const aName = a.querySelector("text").getAttribute("feature-name");
|
||||
const bName = b.querySelector("text").getAttribute("feature-name");
|
||||
console.log(`A: ${aName} (${(+recta.right).toFixed(4)}, ${recta.left}), B: ${bName}`);
|
||||
console.log(aright_lt_bleft, abottom_lt_btop, aleft_gt_bright, atop_gt_bbottom);
|
||||
}
|
||||
|
||||
|
||||
return overlaping;
|
||||
}
|
||||
|
||||
</script>
|
||||
<script src="./02-script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
224
02-script.js
Normal file
224
02-script.js
Normal file
@@ -0,0 +1,224 @@
|
||||
// #region Map Section
|
||||
mapboxgl.accessToken = 'pk.eyJ1Ijoiam9yZGl0b3N0IiwiYSI6ImQtcVkyclEifQ.vwKrOGZoZSj3N-9MB6FF_A';
|
||||
var map = new mapboxgl.Map({
|
||||
container: 'map',
|
||||
style: 'mapbox://styles/mapbox/outdoors-v9',
|
||||
zoom: 11.5,
|
||||
center: [13.4426, 52.5100],
|
||||
});
|
||||
// Get Mapbox map canvas container
|
||||
var canvas = map.getCanvasContainer();
|
||||
// Overlay d3 on the map
|
||||
var svg = d3.select(canvas).append("svg");
|
||||
// Load map and dataset
|
||||
map.on('load', function () {
|
||||
d3.json("data/berlin-parks-new.json")
|
||||
.then(function (data) {
|
||||
drawData(data);
|
||||
})
|
||||
.catch(function (error) {
|
||||
console.error("Error loading data", error);
|
||||
});
|
||||
});
|
||||
// Project GeoJSON coordinate to the map's current state
|
||||
function project(d) {
|
||||
return map.project(new mapboxgl.LngLat(+d[0], +d[1]));
|
||||
}
|
||||
// #endregion
|
||||
// Draw GeoJSON data with d3
|
||||
var circles;
|
||||
var labels;
|
||||
function drawData(data) {
|
||||
// Add circles
|
||||
circles = svg.selectAll("circle")
|
||||
.data(data.features)
|
||||
.enter()
|
||||
.append("circle")
|
||||
.attr("r", 8);
|
||||
labels = svg.selectAll("g.label")
|
||||
.data(data.features)
|
||||
.enter()
|
||||
.append("g")
|
||||
.attr("class", "label")
|
||||
.each(function (d) {
|
||||
var g = d3.select(this);
|
||||
appendLabel(d, g);
|
||||
});
|
||||
// Call the update function
|
||||
update();
|
||||
// Update on map interaction
|
||||
map.on("viewreset", update);
|
||||
map.on("move", update);
|
||||
map.on("moveend", update);
|
||||
}
|
||||
// Add labels with background
|
||||
function appendLabel(d, g) {
|
||||
var _a, _b;
|
||||
var maxWidth = 100;
|
||||
var name = (_b = (_a = d.properties) === null || _a === void 0 ? void 0 : _a.name) !== null && _b !== void 0 ? _b : "";
|
||||
var _c = d.geometry.coordinates, lon = _c[0], lat = _c[1];
|
||||
g.attr("data-lon", lon)
|
||||
.attr("data-lat", lat);
|
||||
g.select("text").remove();
|
||||
g.select("rect").remove();
|
||||
// Append the text
|
||||
var text = g.append("text")
|
||||
.attr("feature-name", name)
|
||||
.attr("font-size", "12px")
|
||||
.attr("fill", "black")
|
||||
.attr("text-anchor", "middle")
|
||||
.attr("x", 0);
|
||||
// Manually wrap text
|
||||
var words = name.split(/\s+/);
|
||||
var line = [];
|
||||
var lineNumber = 0;
|
||||
var lineHeight = 1.1; // ems
|
||||
var x = 0;
|
||||
var y = 0;
|
||||
var tspan = text.append("tspan")
|
||||
.attr("x", x)
|
||||
.attr("y", y)
|
||||
.attr("dy", "".concat(lineNumber * lineHeight, "em"));
|
||||
for (var i = 0; i < words.length; i++) {
|
||||
line.push(words[i]);
|
||||
tspan.text(line.join(" "));
|
||||
// If text exceeds maxWidth, remove last word and start new line
|
||||
if (tspan.node().getComputedTextLength() > maxWidth) {
|
||||
line.pop();
|
||||
tspan.text(line.join(" "));
|
||||
line = [words[i]];
|
||||
tspan = text.append("tspan")
|
||||
.attr("x", x)
|
||||
.attr("y", y)
|
||||
.attr("dy", "".concat(++lineNumber * lineHeight, "em"))
|
||||
.text(words[i]);
|
||||
}
|
||||
}
|
||||
// Measure the text
|
||||
var bbox = text.node().getBBox();
|
||||
// Move both text and rect left by half the text width to center
|
||||
text.attr("x", -bbox.width / 2);
|
||||
// Append rect behind text
|
||||
g.insert("rect", "text")
|
||||
.attr("x", bbox.x - 4)
|
||||
.attr("y", bbox.y - 2)
|
||||
.attr("width", bbox.width + 8)
|
||||
.attr("height", bbox.height + 4)
|
||||
.attr("fill", "white")
|
||||
.attr("stroke", "black")
|
||||
.attr("stroke-width", 0.5)
|
||||
.attr("rx", 4)
|
||||
.attr("ry", 4);
|
||||
g.attr("data-bbox-height", bbox.height);
|
||||
}
|
||||
// Update d3 shapes' positions to the map's current state
|
||||
function update() {
|
||||
if (labels !== null) {
|
||||
// #region
|
||||
var lblVerticalOffset_1 = 4;
|
||||
circles.attr("cx", function (d) {
|
||||
return project(d.geometry.coordinates).x;
|
||||
})
|
||||
.attr("cy", function (d) {
|
||||
return project(d.geometry.coordinates).y;
|
||||
});
|
||||
// Make clones of original labels.
|
||||
d3.selectAll("g.labelclone").remove();
|
||||
var labelNodes = labels.each(function (_, i) {
|
||||
var clone = this.cloneNode(true);
|
||||
clone.setAttribute("class", "labelclone");
|
||||
clone.setAttribute("data-elementid", i.toString());
|
||||
clone.removeAttribute("style");
|
||||
d3.select(this.parentNode).append(function () { return clone; });
|
||||
}).nodes();
|
||||
d3.selectAll("g.label").style("display", "none");
|
||||
d3.selectAll("g.label, g.labelclone")
|
||||
.attr("transform", function (d) {
|
||||
var lon = Number(this.getAttribute("data-lon"));
|
||||
var lat = Number(this.getAttribute("data-lat"));
|
||||
var p = project([lon, lat]);
|
||||
var dy = -(Number(this.getAttribute("data-bbox-height")) + lblVerticalOffset_1);
|
||||
return "translate(".concat(p.x, ", ").concat(p.y + dy, ")");
|
||||
});
|
||||
console.log(labelNodes);
|
||||
//let labelNodes = d3.select("g.labelclones").nodes;
|
||||
var graph = new Map();
|
||||
for (var i = 0; i < labelNodes.length; i++) {
|
||||
for (var j = i + 1; j < labelNodes.length; j++) {
|
||||
if (isOverlapping(labelNodes[i], labelNodes[j])) {
|
||||
console.log("A:", labelNodes[i], labelNodes[i].getClientRects(), labelNodes[i].getBoundingClientRect());
|
||||
console.log("B: ", labelNodes[j], labelNodes[j].getClientRects(), labelNodes[j].getBoundingClientRect());
|
||||
console.log("Overlapping!");
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
var labelGroups = [];
|
||||
// #endregion
|
||||
console.log("Nodes Length: ", labelNodes.length);
|
||||
for (var i = 0; i < labelNodes.length; i++) {
|
||||
var lNode = labelNodes[i];
|
||||
var groupFound = false;
|
||||
console.log("FeatureName: ", lNode.getAttribute("feature-name"), labelGroups);
|
||||
for (var _i = 0, labelGroups_1 = labelGroups; _i < labelGroups_1.length; _i++) {
|
||||
var group = labelGroups_1[_i];
|
||||
console.log("A");
|
||||
for (var j = 0; j < group.length; j++) {
|
||||
console.log("B");
|
||||
if (isOverlapping(lNode, group[j])) {
|
||||
console.log(lNode);
|
||||
group.push(lNode);
|
||||
groupFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (groupFound)
|
||||
break;
|
||||
}
|
||||
// If any label group was found then create a new one with the label node
|
||||
if (!groupFound) {
|
||||
labelGroups.push([lNode]);
|
||||
}
|
||||
}
|
||||
// Step 2: Merge labels in overlapping groups
|
||||
for (var _a = 0, labelGroups_2 = labelGroups; _a < labelGroups_2.length; _a++) {
|
||||
var group = labelGroups_2[_a];
|
||||
if (group.length === 1) {
|
||||
continue;
|
||||
}
|
||||
// Gather merged text and coordinates
|
||||
var names = group.map(function (el) { return el.querySelector("text").getAttribute("feature-name"); });
|
||||
var coords = group.map(function (el) {
|
||||
var transform = el.getAttribute("transform");
|
||||
var match = /translate\(([^,]+),\s*([^)]+)\)/.exec(transform);
|
||||
return { x: +match[1], y: +match[2] };
|
||||
});
|
||||
// Average position
|
||||
var avgX = coords.reduce(function (sum, c) { return sum + c.x; }, 0) / coords.length;
|
||||
var avgY = coords.reduce(function (sum, c) { return sum + c.y; }, 0) / coords.length;
|
||||
// Keep only the first label, update its text
|
||||
var main = d3.select(group[0]);
|
||||
appendLabel(names.join('\n'), main);
|
||||
// Hide others
|
||||
for (var i = 1; i < group.length; i++) {
|
||||
group[i].style.display = "none";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
function isOverlapping(a, b) {
|
||||
var recta = a.getBoundingClientRect();
|
||||
var rectb = b.getBoundingClientRect();
|
||||
var aright_lt_bleft = recta.right < rectb.left;
|
||||
var abottom_lt_btop = recta.bottom < rectb.top;
|
||||
var aleft_gt_bright = recta.left > rectb.right;
|
||||
var atop_gt_bbottom = recta.top > rectb.bottom;
|
||||
var overlaping = !(aright_lt_bleft || aleft_gt_bright || abottom_lt_btop || atop_gt_bbottom);
|
||||
// if (overlaping) {
|
||||
// const aName = a.querySelector("text")!.getAttribute("feature-name");
|
||||
// const bName = b.querySelector("text")!.getAttribute("feature-name");
|
||||
// console.log(`A: ${aName} (${(+recta.right).toFixed(4)}, ${recta.left}), B: ${bName}`);
|
||||
// console.log(aright_lt_bleft, abottom_lt_btop, aleft_gt_bright, atop_gt_bbottom);
|
||||
// }
|
||||
return overlaping;
|
||||
}
|
||||
280
02-script.ts
Normal file
280
02-script.ts
Normal file
@@ -0,0 +1,280 @@
|
||||
// #region Map Section
|
||||
mapboxgl.accessToken = 'pk.eyJ1Ijoiam9yZGl0b3N0IiwiYSI6ImQtcVkyclEifQ.vwKrOGZoZSj3N-9MB6FF_A';
|
||||
var map = new mapboxgl.Map({
|
||||
container: 'map',
|
||||
style: 'mapbox://styles/mapbox/outdoors-v9',
|
||||
zoom: 11.5,
|
||||
center: [13.4426, 52.5100],
|
||||
});
|
||||
|
||||
// Get Mapbox map canvas container
|
||||
var canvas = map.getCanvasContainer();
|
||||
|
||||
// Overlay d3 on the map
|
||||
var svg = d3.select(canvas).append("svg");
|
||||
|
||||
// Load map and dataset
|
||||
map.on('load', function () {
|
||||
d3.json("data/berlin-parks-new.json")
|
||||
.then(function (data) {
|
||||
drawData(data as GeoJSON.FeatureCollection);
|
||||
})
|
||||
.catch(function(error){
|
||||
console.error("Error loading data", error);
|
||||
});
|
||||
});
|
||||
|
||||
// Project GeoJSON coordinate to the map's current state
|
||||
function project(d : number[]) {
|
||||
return map.project(new mapboxgl.LngLat(+d[0], +d[1]));
|
||||
}
|
||||
|
||||
// #endregion
|
||||
|
||||
|
||||
|
||||
// Draw GeoJSON data with d3
|
||||
var circles : d3.Selection<
|
||||
SVGCircleElement,
|
||||
GeoJSON.Feature<GeoJSON.Geometry, GeoJSON.GeoJsonProperties>,
|
||||
SVGSVGElement,
|
||||
unknown
|
||||
>;
|
||||
let labels : d3.Selection<
|
||||
SVGGElement,
|
||||
GeoJSON.Feature<GeoJSON.Geometry, GeoJSON.GeoJsonProperties>,
|
||||
SVGSVGElement,
|
||||
unknown
|
||||
>;
|
||||
function drawData(data : GeoJSON.FeatureCollection) {
|
||||
// Add circles
|
||||
circles = svg.selectAll("circle")
|
||||
.data(data.features)
|
||||
.enter()
|
||||
.append("circle")
|
||||
.attr("r", 8);
|
||||
|
||||
labels = svg.selectAll("g.label")
|
||||
.data(data.features)
|
||||
.enter()
|
||||
.append("g")
|
||||
.attr("class", "label")
|
||||
.each(function (d) {
|
||||
const g = d3.select(this);
|
||||
|
||||
appendLabel(d, g);
|
||||
});
|
||||
|
||||
// Call the update function
|
||||
update();
|
||||
|
||||
// Update on map interaction
|
||||
map.on("viewreset", update);
|
||||
map.on("move", update);
|
||||
map.on("moveend", update);
|
||||
}
|
||||
|
||||
// Add labels with background
|
||||
function appendLabel(d : GeoJSON.Feature, g : d3.Selection<SVGGElement, unknown, null, undefined>){
|
||||
const maxWidth = 100;
|
||||
const name = d.properties?.name ?? "";
|
||||
|
||||
const [lon, lat] = (d.geometry as GeoJSON.Point).coordinates;
|
||||
|
||||
g.attr("data-lon", lon)
|
||||
.attr("data-lat", lat);
|
||||
|
||||
g.select("text").remove();
|
||||
g.select("rect").remove();
|
||||
|
||||
// Append the text
|
||||
const text = g.append("text")
|
||||
.attr("feature-name", name)
|
||||
.attr("font-size", "12px")
|
||||
.attr("fill", "black")
|
||||
.attr("text-anchor", "middle")
|
||||
.attr("x", 0);
|
||||
|
||||
// Manually wrap text
|
||||
const words = name.split(/\s+/);
|
||||
let line = [];
|
||||
let lineNumber = 0;
|
||||
const lineHeight = 1.1; // ems
|
||||
const x = 0;
|
||||
const y = 0;
|
||||
|
||||
let tspan = text.append("tspan")
|
||||
.attr("x", x)
|
||||
.attr("y", y)
|
||||
.attr("dy", `${lineNumber * lineHeight}em`);
|
||||
|
||||
for (let i = 0; i < words.length; i++) {
|
||||
line.push(words[i]);
|
||||
tspan.text(line.join(" "));
|
||||
|
||||
// If text exceeds maxWidth, remove last word and start new line
|
||||
if (tspan.node()!.getComputedTextLength() > maxWidth) {
|
||||
line.pop();
|
||||
tspan.text(line.join(" "));
|
||||
line = [words[i]];
|
||||
|
||||
tspan = text.append("tspan")
|
||||
.attr("x", x)
|
||||
.attr("y", y)
|
||||
.attr("dy", `${++lineNumber * lineHeight}em`)
|
||||
.text(words[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// Measure the text
|
||||
const bbox = text.node()!.getBBox();
|
||||
|
||||
// Move both text and rect left by half the text width to center
|
||||
text.attr("x", -bbox.width / 2);
|
||||
|
||||
// Append rect behind text
|
||||
g.insert("rect", "text")
|
||||
.attr("x", bbox.x - 4)
|
||||
.attr("y", bbox.y - 2)
|
||||
.attr("width", bbox.width + 8)
|
||||
.attr("height", bbox.height + 4)
|
||||
.attr("fill", "white")
|
||||
.attr("stroke", "black")
|
||||
.attr("stroke-width", 0.5)
|
||||
.attr("rx", 4)
|
||||
.attr("ry", 4);
|
||||
|
||||
g.attr("data-bbox-height", bbox.height);
|
||||
}
|
||||
|
||||
// Update d3 shapes' positions to the map's current state
|
||||
function update() {
|
||||
if (labels !== null) {
|
||||
|
||||
// #region
|
||||
const lblVerticalOffset = 4;
|
||||
|
||||
circles.attr("cx", function (d) {
|
||||
return project((d.geometry as GeoJSON.Point).coordinates).x
|
||||
})
|
||||
.attr("cy", function (d) {
|
||||
return project((d.geometry as GeoJSON.Point).coordinates).y
|
||||
});
|
||||
|
||||
// Make clones of original labels.
|
||||
d3.selectAll("g.labelclone").remove();
|
||||
const labelNodes = labels.each(function(_,i){
|
||||
const clone = this.cloneNode(true) as Element;
|
||||
|
||||
clone.setAttribute("class", "labelclone");
|
||||
clone.setAttribute("data-elementid", i.toString());
|
||||
clone.removeAttribute("style");
|
||||
|
||||
d3.select(this.parentNode).append(() => clone);
|
||||
}).nodes();
|
||||
d3.selectAll("g.label").style("display", "none");
|
||||
|
||||
d3.selectAll("g.label, g.labelclone")
|
||||
.attr("transform", function (d) {
|
||||
const lon = Number(this.getAttribute("data-lon"));
|
||||
const lat = Number(this.getAttribute("data-lat"));
|
||||
const p = project([lon, lat]);
|
||||
const dy = -(Number(this.getAttribute("data-bbox-height"))! + lblVerticalOffset)
|
||||
return `translate(${p.x}, ${p.y + dy})`;
|
||||
});
|
||||
|
||||
console.log(labelNodes);
|
||||
|
||||
//let labelNodes = d3.select("g.labelclones").nodes;
|
||||
const graph = new Map<string, Set<string>>();
|
||||
for(let i = 0; i < labelNodes.length; i++){
|
||||
for(let j = i + 1; j < labelNodes.length; j++){
|
||||
if(isOverlapping(labelNodes[i], labelNodes[j])){
|
||||
console.log("A:", labelNodes[i], labelNodes[i].getClientRects(), labelNodes[i].getBoundingClientRect());
|
||||
console.log("B: ", labelNodes[j], labelNodes[j].getClientRects(), labelNodes[j].getBoundingClientRect());
|
||||
console.log("Overlapping!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
const labelGroups = [];
|
||||
// #endregion
|
||||
|
||||
console.log("Nodes Length: ", labelNodes.length);
|
||||
|
||||
for (let i = 0; i < labelNodes.length; i++) {
|
||||
|
||||
const lNode = labelNodes[i];
|
||||
let groupFound = false;
|
||||
|
||||
console.log("FeatureName: ", lNode.getAttribute("feature-name"), labelGroups);
|
||||
for (let group of labelGroups) {
|
||||
console.log("A");
|
||||
for (let j = 0; j < group.length; j++) {
|
||||
console.log("B");
|
||||
if (isOverlapping(lNode, group[j])) {
|
||||
console.log(lNode);
|
||||
group.push(lNode);
|
||||
groupFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (groupFound) break;
|
||||
}
|
||||
|
||||
// If any label group was found then create a new one with the label node
|
||||
if (!groupFound) {
|
||||
labelGroups.push([lNode]);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: 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] };
|
||||
});
|
||||
|
||||
// Average position
|
||||
const avgX = coords.reduce((sum, c) => sum + c.x, 0) / coords.length;
|
||||
const avgY = coords.reduce((sum, c) => sum + c.y, 0) / coords.length;
|
||||
|
||||
// Keep only the first label, update its text
|
||||
const main = d3.select(group[0]);
|
||||
appendLabel(names.join('\n'), main);
|
||||
|
||||
// Hide others
|
||||
for (let i = 1; i < group.length; i++) {
|
||||
group[i].style.display = "none";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isOverlapping(a : SVGElement, b : SVGElement) {
|
||||
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);
|
||||
|
||||
// if (overlaping) {
|
||||
// const aName = a.querySelector("text")!.getAttribute("feature-name");
|
||||
// const bName = b.querySelector("text")!.getAttribute("feature-name");
|
||||
// console.log(`A: ${aName} (${(+recta.right).toFixed(4)}, ${recta.left}), B: ${bName}`);
|
||||
// console.log(aright_lt_bleft, abottom_lt_btop, aleft_gt_bright, atop_gt_bbottom);
|
||||
// }
|
||||
|
||||
return overlaping;
|
||||
}
|
||||
7
package.json
Normal file
7
package.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"devDependencies": {
|
||||
"@types/d3": "^7.4.3",
|
||||
"@types/geojson": "^7946.0.16",
|
||||
"@types/mapbox-gl": "^3.4.1"
|
||||
}
|
||||
}
|
||||
13
tsconfig.json
Normal file
13
tsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"target": "es6",
|
||||
"module": "commonjs",
|
||||
"strict": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": [
|
||||
"02-script.ts"
|
||||
],
|
||||
"exclude": ["../node_modules", "node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user