Reputation: 49
I'm developing a mapping software that renders text from database to a specific coordinate on the canvas. The objective is for the rendered text not to step on each other (not to overlap) but still follow the coordinate where it should display. The idea is, if the rendered texts overlaps, the program may opt to display it at an angle. Currently I'm rendering text via the code below:
create_point:function(x,y,stitle){
var canvas = document.getElementById('text-layer');
var context = canvas.getContext('2d');
context.fillText(stitle,x,y); // text and position
context.save();
}
Any ideas on this?
Thanks in advance :-)
Upvotes: 2
Views: 1714
Reputation: 105035
Interesting mind puzzle!
Problem
You have mapped coordinates (with text labels) from your database and occasionally 2 or more coordinates are so close together that their text labels intersect (causing their text labels to be unreadable).
One solution
For each new text label to be drawn on the map:
Assume each new text label is to be drawn on the top-side of the map coordinate. Test if the new label would overwrite any existing label.
Repeat step#1 assuming the new label would be on right-side of the map coordinate.
Repeat step#1 assuming the new label would be on bottom-side of the map coordinate.
Repeat step#1 assuming the new label would be on left-side of the map coordinate.
If all 4 steps above fail then this text label cannot be draw without overwriting existing labels.
Given failure, you have to decide on an alternate way to give the user your text label information.
These options come to mind:
Draw a small marker on the map that the user can hover over and view a popup tooltip with the text label information. This is a very common way of dealing with information that doesn't fit on the page.
Draw a small marker on the map that refers the user to a separate legend containing the text label information.
Draw a small marker on the map with an arrow-line that leads the user to a text-label that is drawn on the map but is further away from the map coordinate.
Don't include this new label at all! This new label might not be as important as existing map labels and therefore might be omitted. You can easily achieve this by sorting your map database in order of importance to the user.
Here is demo that illustrates this solution
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var cw=canvas.width;
var ch=canvas.height;
function reOffset(){
var BB=canvas.getBoundingClientRect();
offsetX=BB.left;
offsetY=BB.top;
}
var offsetX,offsetY;
reOffset();
window.onscroll=function(e){ reOffset(); }
var fontSize=12;
var fontFace='verdana';
var dotRadius=3;
var legendX=350;
var legendY=0;
var legendYincrement=10;
var labels=[];
var nextId=0;
ctx.textAlign='left';
ctx.textBaseline='top';
ctx.font='10px arial';
ctx.strokeRect(legendX-5,0,cw-legendX+5,ch);
ctx.fillText('Other labels',legendX-3,legendY+2);
legendY+=legendYincrement;
ctx.fillText('(Color Coded)',legendX-3,legendY+2);
legendY+=legendYincrement;
var label=addLabel('Label #0',cw/2,ch/2,fontSize,fontFace,dotRadius);
drawLabel(label);
$("#canvas").mousedown(function(e){handleMouseDown(e);});
//
function addLabel(text,dotX,dotY,fontsize,fontface,dotRadius){
var font=fontsize+'px '+fontface;
ctx.font=font;
var w=ctx.measureText(text).width;
var h=fontsize*1.286;
var label={
id:nextId++,
text:text,
x:dotX-w/2,
y:dotY-dotRadius-h,
w:w,
h:h,
offsetY:0,
font:font,
isColliding:false,
dotRadius:dotRadius,
dotX:dotX,
dotY:dotY,
};
labels.push(label);
// try to position this new label in a non-colliding position
var positions=[
{ x:dotX-w/2, y:dotY-dotRadius-h }, // N
{ x:dotX+dotRadius, y:dotY-h/2 }, // E
{ x:dotX-w/2, y:dotY+dotRadius }, // S
{ x:dotX-dotRadius-w, y:dotY-h/2 }, // W
];
for(var i=0;i<positions.length;i++){
var p=positions[i];
label.x=p.x;
label.y=p.y;
label.isColliding=thisLabelCollides(label);
if(!label.isColliding){ break; }
}
//
return(label);
}
function handleMouseDown(e){
// tell the browser we're handling this event
e.preventDefault();
e.stopPropagation();
var x=parseInt(e.clientX-offsetX);
var y=parseInt(e.clientY-offsetY);
var label=addLabel('Label #'+nextId,x,y,fontSize,fontFace,dotRadius)
drawLabel(label);
}
//
function drawLabel(label){
ctx.textAlign='left';
ctx.textBaseline='top';
if(label.isColliding){
legendY+=legendYincrement;
ctx.beginPath();
ctx.arc(legendX,legendY,3,0,Math.PI*2);
ctx.fillStyle=randomColor();
ctx.fill();
ctx.font='10px arial';
ctx.fillText(label.text,legendX+5,legendY-5);
}else{
ctx.font=label.font;
ctx.fillStyle='black';
ctx.fillText(label.text,label.x,label.y)
ctx.strokeRect(label.x,label.y,label.w,label.h);
}
ctx.beginPath();
ctx.arc(label.dotX,label.dotY,label.dotRadius,0,Math.PI*2);
ctx.fill();
}
//
function thisLabelCollides(r1){
for(var i=0;i<labels.length;i++){
var r2=labels[i];
if(r1.id==r2.id || r2.isColliding){continue;}
var collides=(!(
r1.x > r2.x+r2.w ||
r1.x+r1.w < r2.x ||
r1.y > r2.y+r2.h ||
r1.y+r1.h < r2.y
));
if(collides){return(true);}
}
return(false);
}
//
function randomColor(){
return('#'+Math.floor(Math.random()*16777215).toString(16));
}
body{ background-color: ivory; }
#canvas{border:1px solid red; margin:0 auto; }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<h4>Click on the canvas to add more map labels.</h4>
<canvas id="canvas" width=450 height=300></canvas>
Upvotes: 4