最近一直在写canvas绘制室内地图和路径这个功能,大致聊一下这个功能讲了什么。
具体是需要用canvas将室内地图绘制出来(这个不难,canvas教程有),并且地图需要能进行放大缩小,同时将地图上的路径进行展示,同样需要能放大缩小。
其实不能看出,就是将地图和路径进行同时的放大缩小。
我之前的思路是(之前没有要求缩放),创建canvas,然后设置背景(背景就是地图),然后将后端的路径数据展示到前端背景上,具体的实现,之前的博客有说明,可以翻翻看。
之后添加一个功能就是要求能将地图和路径能进行缩放,那么再将地图作为背景就不能实现缩放,所以我就使用canvas先绘制一个图片,先实现图片缩放,再在图片缩放的同时将路径数据放入到图片中,实现在绘制图片的同时,顺带加上路径数据。这是我的思路,因为没系统的接触过canvas,不知道还有没有别的思路和方法,总感觉我的还是有问题。
具体流程:(html + vue + django)
HTML:
<canvas id="scaleDragCanvas" width="1276" height="800"></canvas>
vue、js:
var img_path = res.data['img_path']; // 不用太在意,图片路径
var piece = res.data['loc_data']; // 不用太在意,路径数据
_this.rountData = res.data['rount_data']; // 不用太在意,路线数据
var canvas, context;
var showTips = null, originalPos = null;
var initWidth = 0, initHeight = 0;
var img, imgX = 0, imgY = 0, imgScale = 1, scaleRate = 1, scaleRatey = 1; // imgScale 缩放比例 scaleRate 图片比例尺 imgX 图片左上角坐标
var MINIMUM_SCALE = 1 ,pos = {}, posl = {}, dragging = false;
var rount_len = res.data['loc_data'].length;
(function int() { // 缩放主函数
canvas = document.getElementById('scaleDragCanvas'); // 画布对象
context = canvas.getContext('2d'); // 画布显示二维图片
loadImg();
canvasEventsInit(); // 转换坐标
})();
function stroke() { // 缩放路径
var color_list = ['rgb(255,0,0)', 'rgb(255,255,0)', 'rgb(0,255,0)', 'rgb(0,0,255)', 'rgb(255,0,255)', 'rgb(0,0,0)'];
for (var j = 0; j < rount_len; j++){
piece = res.data['loc_data'][j];
context.strokeStyle = color_list[j];
for (var i = 0; i < piece.length; i++) {
context.beginPath();
context.moveTo((piece[i].startX)*(imgScale),(piece[i].startY)*(imgScale));
context.lineTo((piece[i].endX)*(imgScale), (piece[i].endY)*(imgScale));
context.stroke();
}
}
}
function strokemove() { // 移动路径
var color_list = ['rgb(255,0,0)', 'rgb(255,255,0)', 'rgb(0,255,0)', 'rgb(0,0,255)', 'rgb(255,0,255)', 'rgb(0,0,0)'];
for (var j = 0; j < rount_len; j++){
piece = res.data['loc_data'][j];
context.strokeStyle = color_list[j];
for (var i = 0; i < piece.length; i++) {
context.beginPath();
context.moveTo(((piece[i].startX)*(imgScale))+imgX,((piece[i].startY)*(imgScale))+imgY);
context.lineTo(((piece[i].endX)*(imgScale))+imgX, ((piece[i].endY)*(imgScale))+imgY);
context.stroke();
}
}
}
function loadImg() { // 画图主函数
img = new Image();
img.onload = function () {
scaleRate = img.width / canvas.width; // 让长宽保持相同缩放比例
scaleRatey = img.height / canvas.height;
initWidth = img.width * scaleRate;
initHeight = img.height * scaleRatey;
console.log("initWidth=" + initWidth + ",initHeight=" + initHeight + ", scaleRate=" + scaleRate + ", scaleRatey=" + scaleRatey);
drawImage();
};
img.src ='/media/maps/' + img_path;
}
function drawImage() { // 画图副函数
context.clearRect(0, 0, canvas.width, canvas.height); // 清空给定矩形内的指定像素
// 保证 imgX 在 [img.width*(1-imgScale),0] 区间内
///**
if(imgX<img.width*(1-imgScale) /scaleRate) {
imgX = img.width*(1-imgScale)/scaleRate ;
}else if(imgX>0) {
imgX=0
}
// 保证 imgY 在 [img.height*(1-imgScale),0] 区间内
if(imgY<img.height*(1-imgScale)/scaleRatey) {
imgY = img.height*(1-imgScale)/scaleRatey;
}else if(imgY>0) {
imgY=0
}
//*/
context.drawImage(
img, //规定要使用的图像、画布或视频。
0, 0, //开始剪切的 xy 坐标位置。
initWidth, initHeight, //被剪切图像的高度。
imgX, imgY,//在画布上放置图像的 x 、y坐标位置。
img.width * imgScale, img.height * imgScale //要使用的图像的宽度、高度
);
stroke();
}
/*事件注册*/
function canvasEventsInit() {
canvas.onmousedown = function (event) { // 鼠标按下
if(showTips == null){
dragging = true;
pos = windowToCanvas(event.clientX, event.clientY); // 坐标转换,将窗口坐标转换成canvas的坐标
}
};
canvas.onmousemove = function (evt) { // 鼠标移动
if(showTips != null){
// 测距功能
if(showTips){
showPosTips(originalPos.x, originalPos.y, evt);
}
}else{
if(dragging){
posl = windowToCanvas(evt.clientX, evt.clientY);
let x = posl.x - pos.x, y = posl.y - pos.y;
imgX += x;
imgY += y;
pos = JSON.parse(JSON.stringify(posl));
context.clearRect(0, 0, canvas.width, canvas.height); // 清空给定矩形内的指定像素
// 保证 imgX 在 [img.width*(1-imgScale),0] 区间内
///**
if(imgX<img.width*(1-imgScale) /scaleRate) {
imgX = img.width*(1-imgScale)/scaleRate ;
}else if(imgX>0) {
imgX=0
}
// 保证 imgY 在 [img.height*(1-imgScale),0] 区间内
if(imgY<img.height*(1-imgScale)/scaleRatey) {
imgY = img.height*(1-imgScale)/scaleRatey;
}else if(imgY>0) {
imgY=0
}
//*/
context.drawImage(
img, //规定要使用的图像、画布或视频。
0, 0, //开始剪切的 xy 坐标位置。
initWidth, initHeight, //被剪切图像的高度。
imgX, imgY,//在画布上放置图像的 x 、y坐标位置。
img.width * imgScale, img.height * imgScale //要使用的图像的宽度、高度
);
strokemove();
}else{
let pos3 = windowToCanvas(evt.clientX, evt.clientY);
if(context.isPointInPath(pos3.x, pos3.y)){
console.log("进入区域");
}
}
}
};
canvas.onmouseup = function (evt) { // 鼠标抬起
if(showTips != null){
if(!showTips){
showTips = true;
}else{
showTips = null;
}
}else{
dragging = false;
}
};
canvas.onmousewheel = canvas.onwheel = function (event) { //滚轮放大缩小
let pos = windowToCanvas (event.clientX, event.clientY);
event.wheelDelta = event.wheelDelta ? event.wheelDelta : (event.deltalY * (-40)); //获取当前鼠标的滚动情况
let newPos = {x:((pos.x-imgX)/imgScale).toFixed(2) , y:((pos.y-imgY)/imgScale).toFixed(2)};
if (event.wheelDelta > 0) {// 放大
imgScale +=0.1;
imgX = 0;
imgY = 0;
} else {// 缩小
imgScale -=0.1;
if(imgScale<MINIMUM_SCALE) {//最小缩放1
imgScale = MINIMUM_SCALE;
}
imgX = 0;
imgY = 0;
}
drawImage(); //重新绘制图片
};
canvas.addEventListener('dblclick', function (event) { // 双击
let pos = windowToCanvas(event.clientX,event.clientY);
//实际点的坐标
let isX = 0;
let isY = 0;
//实际选中坐标 肯定 = 点击的坐标 + canvas的坐标(必须为-数)
let temp_canvas_x = imgX-imgX*2;
let temp_canvas_y = imgY-imgY*2;
scaleRate = img.width / canvas.width; // 让长宽保持相同缩放比例
scaleRatey = img.height / canvas.height;
if(imgScale === 1){ //原始尺寸
isX = (pos.x+temp_canvas_x)*scaleRate;
isY = (pos.y+temp_canvas_y)*scaleRatey;
axios.get('/api/loc/', {params: {'loc_x': isX, 'loc_y': isY}})
} else {
if (imgScale > 1) { //被放大了,实际坐标需要/
isX = ((pos.x + temp_canvas_x) / imgScale)*scaleRate;
isY = ((pos.y + temp_canvas_y) / imgScale)*scaleRatey;
console.log(isX, isY);
axios.get('/api/loc/', {params: {'loc_x': isX, 'loc_y': isY}})
} else { //被缩小了,坐标点放大
isX = ((pos.x + temp_canvas_x) * (1 / imgScale))*scaleRate;
isY = ((pos.y + temp_canvas_y) * (1 / imgScale)*scaleRatey);
console.log(isX, isY);
axios.get('/api/loc/', {params: {'loc_x': isX, 'loc_y': isY}})
}
}
});
}
function showPosTips(x, y, evt){
drawImage();
context.beginPath();
context.stokeStyle="#f00";
context.moveTo(x, y);
let temp = windowToCanvas(evt.clientX, evt.clientY);
context.lineTo(temp.x, temp.y);
context.stroke();
document.getElementById("mouseTip").innerHTML = temp.x + "," + temp.y;
document.getElementById("mouseTip").style.left = evt.clientX + 'px';
document.getElementById("mouseTip").style.top = evt.clientY + 'px';
}
/*坐标转换*/
function windowToCanvas(x,y) {
var box = canvas.getBoundingClientRect();
//console.log( "x=" + (x - box.left - (box.width - canvas.width) / 2) + ",y=" + (y - box.top - (box.height - canvas.height) / 2));
//这个方法返回一个矩形对象,包含四个属性:left、top、right和bottom。分别表示元素各边与页面上边和左边的距离
return {
x: x - box.left - (box.width - canvas.width) / 2,
y: y - box.top - (box.height - canvas.height) / 2
};
}
django:
# api
@csrf_exempt
def get_loc(request):
"""保存地图路径到数据库"""
json_str = request.body.decode()
req_data = json.loads(json_str)
req_data = req_data['loc_data'] # 一个大的列表
req_len = len(req_data)
for i in range(req_len):
map_name = ((req_data[i]['map_name']).split('.')[0]) + '.jpg'
rount_index = req_data[i]['rount_index']
loc = req_data[i]['loc'] # 一个列表
loc_len = len(loc)
for i in range(loc_len):
loc_seq = loc[i]['loc_seq']
loc_x = loc[i]['loc_x']
loc_y = loc[i]['loc_y']
try:
map = Map.objects.get(name=map_name)
Location.objects.create(loc2_index=rount_index, loc2_x=loc_x, loc2_y=loc_y, loc2_seq=loc_seq, mid_id=map.id)
except Exception as e:
print(e)
return HttpResponse('not ok')
return HttpResponse('is ok!!!')
# api
@csrf_exempt
def get_map(request):
"""保存地图"""
car_index = request.POST.get('car_index')
img = request.FILES.get('img')
map_name = request.POST.get('map_name')
if img is None:
return HttpResponse('img is not ok')
try:
robot = Robot.objects.get(car_index=car_index)
Map.objects.create(name=map_name, rid_id=robot.id, img=img)
except Exception as e:
print(e)
return HttpResponse('mysql not ok')
new_img_path, new_img_name = pgm_tojpg(map_name)
Map.objects.filter(name=map_name).update(name=new_img_name, img=new_img_path)
return HttpResponse('is ok!!!')
def pgm_tojpg(img_name):
"""pgm转换成jpg"""
img = Image.open(MEDIA_ROOT + '/maps/' + img_name)
img_name = img_name.split('.')[0]
new_img_name = img_name + '.jpg'
new_img_path = 'maps/' + img_name + '.jpg'
img.save(MEDIA_ROOT + '/maps/' + img_name + '.jpg')
return new_img_path, new_img_name
def get_mapdetail(request):
"""获取地图详情: 坐标,路线"""
map_name = request.GET.get('map_name')
m = Map.objects.get(name=map_name)
img = Image.open(MEDIA_ROOT + '/maps/' + map_name)
wimg = img.width
himg = img.height
rount = m.location_set.values('loc2_index').distinct()
list3 = []
color_list = [{"rgb(255,0,0)": "红"}, {"rgb(255,255,0)": "黄"}, {"rgb(0,255,0)": "青"}, {"rgb(0,0,255)": "蓝"}, {"rgb(255,0,255)": "紫"}, {"rgb(0,0,0)": "黑"}]
for r in rount:
list3.append({
'rount_index': (r['loc2_index']).split('rount_')[1],
'rount_name': r['loc2_index'],
'rount_color': list(color_list[int((r['loc2_index']).split('rount_')[1])-1].values())[0]
})
print(list3)
rount_count = rount.count() # 多少条路线
context = {}
list1 = []
for i in range(rount_count):
list2 = []
rount_index = 'rount_' + str(i+1)
L = m.location_set.filter(loc2_index=rount_index)
l_len = len(L)
if l_len <= 1:
print('只有一个点')
if wimg >= 1276 and himg >= 800:
for i in range(l_len - 1):
list2.append({
'startX': float(L[i].loc2_x) / (wimg / 1276), # 5083 6597
'startY': float(L[i].loc2_y) / (himg / 800),
'endX': float(L[i + 1].loc2_x) / (wimg / 1276),
'endY': float(L[i + 1].loc2_y) / (himg / 800)
})
list1.append(list2)
else:
for i in range(l_len - 1):
list2.append({
'startX': float(L[i].loc2_x) * (wimg / 1276),
'startY': float(L[i].loc2_y) * (himg / 800),
'endX': float(L[i + 1].loc2_x) * (wimg / 1276),
'endY': float(L[i + 1].loc2_y) * (himg / 800)
})
list1.append(list2)
context['loc_data'] = list1
context['img_path'] = map_name
context['rount_data'] = list3
return JsonResponse(data=context)
# api
@csrf_exempt
def set_loc(request):
"""传递坐标给小车"""
loc_x = request.GET.get('loc_x')
loc_y = request.GET.get('loc_y')
context = {}
context['loc_x'] = loc_x
context['loc_y'] = loc_y
print(context)
res = upload_rount(context)
return JsonResponse(res, safe=False)
# api
@csrf_exempt
def set_rount(request):
"""传递路线"""
rount_index = request.GET.get('rount_index')
res = upload_rount(rount_index)
return JsonResponse(res, safe=False)