<template>
<div class="label_main">
<div class="labek_operation">
<div class="button-wrap">
<el-button type="primary" class="label_btn" @click="restoration()">图像居中</el-button>
<el-button type="primary" class="label_btn" @click="zoomIn()">图像放大</el-button>
<el-button type="primary" class="label_btn" @click="zoomOut()">图像缩小</el-button>
<el-button type="primary" class="label_btn" @click="setMode('PAN')">移动</el-button>
<!-- <el-button type="primary" class="label_btn" @click="setMode('MARKER')" >注记</el-button> -->
<el-button type="primary" class="label_btn" @click="setMode('POINT')">点</el-button>
<el-button type="primary" class="label_btn" @click="setMode('LINE')">线段</el-button>
<!-- <el-button type="primary" class="label_btn" @click="setMode('POLYLINE')" >多段线</el-button> -->
<el-button type="primary" class="label_btn" @click="setMode('CIRCLE')">圆</el-button>
<el-button type="primary" class="label_btn" @click="setMode('RECT')">矩形</el-button>
<el-button type="primary" class="label_btn" @click="setMode('POLYGON')">多边形</el-button>
<!-- <el-button type="primary" class="label_btn" @click="Fill()" >填充</el-button> -->
<!-- <el-button type="primary" class="label_btn" @click="Revoke()">撤销</el-button> -->
<el-button type="primary" class="label_btn" @click="getFeatures()">获取标注数据</el-button>
<!-- <el-button type="primary" class="label_btn" @click="setMode('DRAWMASK')" >涂抹</el-button> -->
<!-- <el-button type="primary" class="label_btn" @click="setMode('CLEARMASK')" >擦除</el-button> -->
<!-- <el-button type="primary" class="label_btn" @click="getRle()">获取rle数据</el-button> -->
<!-- <el-button type="primary" class="label_btn" @click="setGraph()" >手动设置坐标</el-button> -->
</div>
</div>
<div class="main_map" ref="main_map">
</div>
</div>
</template>
<script>
import AILabel from "ailabel";
import { reactive, toRefs, onBeforeMount, onMounted, watch, onBeforeUnmount, ref } from 'vue'
// 导入toRaw函数
import { toRaw } from '@vue/reactivity';
import { ElMessageBox, ElMessage } from 'element-plus';
export default {
name: "LabelTemplate",
props: ['imgUrl', 'divId'],
setup (props, context) {
//动态添加视图
const main_map = ref(null)
const mountEle = document.createElement("div");
mountEle.id = props.divId
mountEle.style.overflow = 'hidden';
mountEle.style.position = 'relative';
mountEle.style.height = '450px';
mountEle.style.width = '650px';
mountEle.style.border = '1px dashed #ccc';
const state = reactive({
imgUrl: props.imgUrl,
divId: props.divId,
drawingStyle: {},
mode: "",
itemName: "",
editId: "", //待填充图形id
deleteIconId: "delete01",
gMap: null, //AILabel实例
gFirstFeatureLayer: null, //矢量图层实例(矩形,多边形等矢量)
allFeatures: null, //所有features
gFirstImageLayer: null, //图层
nHeight: '',
nWidth: '',
centerObj: {},
gFirstTextLayer: null, //文本
gFirstMaskLayer : null, //涂抹层
})
onBeforeMount(() => {
})
onMounted(() => {
main_map.value.appendChild(mountEle)
//获取图片原宽高
getImageWH()
//初始化实例
initMap()
//添加事件
addEvent();
//图片层添加
setGFirstImageLayer()
//添加矢量图层
setGFirstFeatureLayer()
// 添加 text 文本图层,用于展示文本
setGFirstTextLayer()
//初始化涂抹层
setGFirstMaskLayer()
// AILabel.Map设置绘制过程中十字丝关闭
// gMap.disableDrawingCrosshair();
// 禁用绘制时鼠标达到边界外自动平移
// gMap.disablePanWhenDrawing();
//禁用绘制时可鼠标滑轮缩放
// gMap.disableZoomWhenDrawing()
window.onresize = function () {
state.gMap && state.gMap.resize();
};
})
onBeforeUnmount(() => {
state.gMap.destroy();
})
watch(() => state.mode, (newVal, oldVal) => {
console.log('新的值mode:', newVal, '旧的mode:', oldVal,);
state.gMap.setMode(newVal);
setDrawingStyle(newVal);
})
watch(() => props.imgUrl, (newVal, oldVal) => {
console.log('新的值imgUrl:', newVal, '旧的imgUrl:', oldVal,);
state.imgUrl = newVal
//获取图片原宽高
getImageWH()
//重新设置图片
setGFirstImageLayer()
//添加矢量图层
setGFirstFeatureLayer()
// 添加 text 文本图层,用于展示文本
setGFirstTextLayer()
//刷新map
state.gMap.refresh();
})
// 初始化实例
const initMap = () => {
const gMap = new AILabel.Map(state.divId, {
center: state.centerObj, // 为了让图片居中
zoom: 500, //初始缩放级别
mode: "PAN", // 绘制线段
refreshDelayWhenZooming: true, // 缩放时是否允许刷新延时,性能更优
zoomWhenDrawing: false, //绘制时可滑轮缩放
panWhenDrawing: true, //绘制时可到边界外自动平移
zoomWheelRatio: 5, // 控制滑轮缩放缩率[0, 10), 值越小,则缩放越快,反之越慢
withHotKeys: true, // 关闭快捷键
});
state.gMap = gMap;
}
// 初始样式
const setDrawingStyle = (mode) => {
let drawingStyle = {};
switch (mode) {
//平移
case "PAN": {
break;
}
//注记
case "MARKER": {
// 忽略
break;
}
//点
case "POINT": {
state.drawingStyle = { fillStyle: "#FF8C00" };
state.gMap.setDrawingStyle(drawingStyle);
break;
}
//圆
case "CIRCLE": {
state.drawingStyle = {
fillStyle: "#87CEFF",
fill: true, //是否填充
strokeStyle: "#87CEFF",
globalAlpha: 0.3,
lineWidth: 2,
};
state.gMap.setDrawingStyle(drawingStyle);
break;
}
//线段
case "LINE": {
state.drawingStyle = {
strokeStyle: "#BA55D3",
lineJoin: "round",
lineCap: "round",
lineWidth: 10,
arrow: false,
};
state.gMap.setDrawingStyle(drawingStyle);
break;
}
//多线段
case "POLYLINE": {
state.drawingStyle = {
strokeStyle: "#FF1493",
lineJoin: "round",
lineCap: "round",
lineWidth: 10,
};
state.gMap.setDrawingStyle(drawingStyle);
break;
}
//矩形
case "RECT": {
state.drawingStyle = {
strokeStyle: "#0f0",
lineWidth: 1,
fillStyle: '#00f',
fill: true, //是否填充
globalAlpha: 0.3,
stroke: true,
};
state.gMap.setDrawingStyle(drawingStyle);
break;
}
//多边形
case "POLYGON": {
state.drawingStyle = {
strokeStyle: "#0099CC", //边框颜色
fill: true, //是否填充
fillStyle: "#FF6666", //填充色
globalAlpha: 0.3,
lineWidth: 3,
stroke: true,
};
state.gMap.setDrawingStyle(drawingStyle);
break;
}
//涂抹
case "DRAWMASK": {
state.drawingStyle = {
strokeStyle: "rgba(255, 0, 0, .5)",
fillStyle: "#00f",
lineWidth: 50,
};
state.gMap.setDrawingStyle(drawingStyle);
break;
}
//擦除
case "CLEARMASK": {
state.drawingStyle = { fillStyle: "#00f", lineWidth: 30 };
state.gMap.setDrawingStyle(drawingStyle);
break;
}
default:
break;
}
}
//添加 text 文本图层,用于展示文本
const setGFirstTextLayer = () => {
const gFirstTextLayer = new AILabel.Layer.Text(
'first-layer-text', // id
{ name: '第一个文本图层' }, // props
{ zIndex: 12, opacity: 1 } // style
)
state.gMap.addLayer(gFirstTextLayer)
state.gFirstTextLayer = gFirstTextLayer
}
//添加 涂抹层
const setGFirstMaskLayer = () => {
const gFirstMaskLayer = new AILabel.Layer.Mask(
'first-layer-mask', // id
{name: '第一个涂抹图层'}, // props
{zIndex: 11, opacity: .5} // style
);
state.gMap.addLayer(gFirstMaskLayer);
state.gFirstMaskLayer = gFirstMaskLayer
}
//添加矢量图层
const setGFirstFeatureLayer = () => {
// 添加矢量图层
const gFirstFeatureLayer = new AILabel.Layer.Feature(
"first-layer-feature", // id
{ name: "第一个矢量图层" }, // props
{ zIndex: 10 } // style
);
state.gFirstFeatureLayer = gFirstFeatureLayer;
state.gMap.addLayer(gFirstFeatureLayer);
}
// 图片层添加
const setGFirstImageLayer = () => {
// 图片层添加
const gFirstImageLayer = new AILabel.Layer.Image(
"first-layer-image", // id
{
src: state.imgUrl,
width: state.nWidth,
height: state.nHeight,
crossOrigin: false, // 如果跨域图片,需要设置为true
position: {
// 左上角相对中心点偏移量
x: 0,
y: 0,
},
// 网格
grid: {
// 3 * 3
// columns: [{ color: "#9370DB" }, { color: "#FF6347" }],
// rows: [{ color: "#9370DB" }, { color: "#FF6347" }],
},
}, // imageInfo
{ name: "第一个图片图层" }, // props
{ zIndex: 5 } // style
);
// 添加到gMap对象
state.gMap.addLayer(gFirstImageLayer);
state.gFirstImageLayer = gFirstImageLayer
}
//获取原图宽度高度
const getImageWH = () => {
let nWidth = 500
let nHeight = 300
let centerObj = { x: 250, y: 150 }
// 创建对象
var img = new Image()
// 改变图片的src
img.src = state.imgUrl
//根据原图宽高展示全部内容
if (img.width / nWidth == img.height / nHeight) {
nWidth = 500
nHeight = 300
centerObj = { x: 250, y: 150 }
} else if (img.width >= nWidth) {
nWidth = (nWidth / img.width) * img.width
nHeight = (nWidth / img.width) * img.height
centerObj.y = nHeight / 2
} else if (img.height >= nHeight) {
nWidth = (nHeight / img.height) * img.width
nHeight = (nHeight / img.height) * img.height
centerObj.x = nWidth / 2
} else if (img.height < nHeight || img.width < nWidth) {
nHeight = 300
nWidth = img.width * (300 / img.height)
centerObj.x = nWidth / 2
centerObj.y = nHeight / 2
}
state.centerObj = centerObj
state.nWidth = nWidth
state.nHeight = nHeight
return { nWidth, nHeight, centerObj }
}
// 获取所有features
const getFeatures = () => {
state.allFeatures = state.gFirstFeatureLayer.getAllFeatures();
console.log("--allFeatures--", toRaw(state.allFeatures));
toRaw(state.allFeatures).forEach((e, i) => {
console.log(`第${i}条:`, e);
})
}
// 添加图形
const addFeature = (data, type, id) => {
let drawingStyle = state.drawingStyle;
let name = id
id = `${id}${new Date().valueOf()}`
console.log(data, '===', type);
//线
if (type === "LINE") {
const scale = state.gMap.getScale();
const width = drawingStyle.lineWidth / scale;
const lineFeature = new AILabel.Feature.Line(
id, // id
{ ...data, width }, // shape
{ name, textId: id }, // props
drawingStyle // style
);
state.gFirstFeatureLayer.addFeature(lineFeature);
// 多边形 添加 文本
const textPosition = setText(data, type)
addLayerText(id, name, textPosition)
}
//线段
else if (type === "POLYLINE") {
const scale = state.gMap.getScale();
const width = drawingStyle.lineWidth / scale;
const polylineFeature = new AILabel.Feature.Polyline(
id, // id
{ points: data, width }, // shape
{ name, textId: id }, // props
drawingStyle // style
);
state.gFirstFeatureLayer.addFeature(polylineFeature);
// 多边形 添加 文本
const textPosition = setText(data, type)
addLayerText(id, name, textPosition)
}
//矩形
else if (type === "RECT") {
const rectFeature = new AILabel.Feature.Rect(
id, // id
data, // shape
{ name, textId: id }, // props
drawingStyle // style
);
state.gFirstFeatureLayer.addFeature(rectFeature);
// 多边形 添加 文本
const textPosition = setText(data, type)
addLayerText(id, name, textPosition)
}
//多边形
else if (type === "POLYGON") {
const polygonFeature = new AILabel.Feature.Polygon(
id, // id
{ points: data }, // shape
{ name, textId: id }, // props
drawingStyle // style
);
state.gFirstFeatureLayer.addFeature(polygonFeature);
// 多边形 添加 文本
const textPosition = setText(data, type)
addLayerText(id, name, textPosition)
}
//点
else if (type == "POINT") {
const gFirstFeaturePoint = new AILabel.Feature.Point(
id, // id
{ x: data.x, y: data.y, r: 5 }, // shape
{ name, textId: id }, // props
{ fillStyle: "#FF8C00", zIndex: 5, lineWidth: 2 } // style
);
state.gFirstFeatureLayer.addFeature(gFirstFeaturePoint);
// 多边形 添加 文本
const textPosition = setText(data, type)
addLayerText(id, name, textPosition)
}
//注记
else if (type == "MARKER") {
const gFirstMarker = new AILabel.Marker(
id, // id
{
src: "http://ailabel.com.cn/public/ailabel/demo/marker.png",
position: {
// marker坐标位置
x: data.x,
y: data.y,
},
offset: {
x: -16,
y: 32,
},
}, // markerInfo
{ name: "第一个marker注记", textId: id } // props
);
state.gFirstFeatureLayer.addFeature(gFirstMarker);
// 多边形 添加 文本
const textPosition = setText(data, type)
addLayerText(id, name, textPosition)
}
//圆
else if (type == "CIRCLE") {
const gFirstFeatureCircle = new AILabel.Feature.Circle(
id, // id
{ cx: data.cx, cy: data.cy, r: data.r }, // shape
{ name, textId: id }, // props
{
fillStyle: "#87CEFF",
strokeStyle: "#3CB371",
fill: true, //是否填充
globalAlpha: 0.3,
lineWidth: 2,
} // style
);
state.gFirstFeatureLayer.addFeature(gFirstFeatureCircle);
// 多边形 添加 文本
const textPosition = setText(data, type)
addLayerText(id, name, textPosition)
}
//涂抹
else if (type == "DRAWMASK") {
const drawMaskAction = new AILabel.Mask.Draw(
`${+new Date().valueOf()}`, // id
"铅笔",
{ points: data, width: 5 }, // shape
{ name: "港币", price: "1元", textId: id }, // props
{ strokeStyle: "#FF0000" } // style
);
state.gFirstFeatureLayer.addAction(drawMaskAction);
}
//擦除
else if (type == "CLEARMASK") {
const clearMaskAction = new AILabel.Mask.Clear(
"first-action-clear", // id
{ points: data, width: 5 } // shape
);
state.gFirstMaskLayer.addAction(clearMaskAction);
}
getFeatures();
}
// 画完取名
const getName = async (mode) => {
console.log(mode);
try {
const { value } = await ElMessageBox.prompt("请输入填写名字", {
confirmButtonText: "确定",
showCancelButton: false,
inputValidator: (v) => {
if (v) {
return true
} else {
return '请填写名字'
}
}
});
if (value) {
state.itemName = value;
return value;
} else {
return ''
}
} catch {
return null;
}
}
// 增加删除图标
const addDeleteIcon = (feature, shape) => {
// 添加delete-icon
console.log(shape, "shape");
let x = (shape?.x || shape?.cx || shape?.start?.x || shape?.points[0].x) + (shape?.width || shape?.r || 0)
let y = (shape?.y || shape?.cy || shape?.start?.y || shape?.points[0].y) - 15
const gFirstMarker = new AILabel.Marker(
state.deleteIconId, // id
{
src: "https://s1.ax1x.com/2022/06/20/XvFRbT.png",
position: { x, y }, // 矩形右上角 根据图形动态调整
offset: {
x: -20,
y: -4,
},
}, // markerInfo
{ name: "delete" } // props
);
gFirstMarker.events.on("click", (marker) => {
// 首先删除当前marker
state.gMap.markerLayer.removeMarkerById(marker.id);
// 删除对应text
state.gFirstTextLayer.removeTextById(feature.props.textId);
// 删除对应feature
state.gFirstFeatureLayer.removeFeatureById(feature.id);
});
state.gMap.markerLayer.addMarker(gFirstMarker);
}
// 添加文本
const addLayerText = (textId, textName, textPosition) => {
// 添加一个文本
const gFirstText = new AILabel.Text(
textId, // id
{ text: textName, position: textPosition, offset: { x: 0, y: 0 } },
{ name: textName }, // props
// 文本显示 style
{ fillStyle: '#F4A460', strokeStyle: '#D2691E', background: true, globalAlpha: 1, fontColor: 'red' }
)
state.gFirstTextLayer.addText(gFirstText)
}
// 删除 删除按钮
const deIcon = () => {
state.gMap.markerLayer.removeAllMarkers();
}
//设置文本坐标
const setText = (data, type) => {
let textPosition = {}
switch (type) {
case 'LINE':
textPosition = { x: data?.start['x'], y: data?.start['y'] }
break;
case 'POLYLINE':
textPosition = { x: data?.x, y: data?.y }
break;
case 'RECT':
textPosition = { x: data['x'], y: data['y'] }
break;
case 'POLYGON':
textPosition = { x: data[0]['x'], y: data[0]['y'] }
break;
case 'POINT':
textPosition = { x: data.x, y: data.y }
break;
case 'MARKER':
textPosition = { x: data.x, y: data.y }
break;
case 'CIRCLE':
textPosition = { x: data.cx, y: data.cy - data.r }
break;
default:
break;
}
return textPosition
}
// 增加事件
const addEvent = () => {
let gMap = state.gMap;
gMap.events.on("drawDone", (type, data) => {
console.log("--type, data--", type, data);
if (type == "CLEARMASK" || type == "DRAWMASK") { //除了擦除 抹除外
addFeature(data, type);
} else {
getName(type).then((id) => {
if (id) {
addFeature(data, type, id);
}
});
}
});
gMap.events.on("boundsChanged", (data) => {
console.log("--map boundsChanged--", data);
return "";
});
// 双击编辑 在绘制模式下双击feature触发选中
gMap.events.on("featureSelected", (feature) => {
state.editId = feature.id;
console.log("--map featureSelected--", feature, "双击编辑");
//设置编辑feature
gMap.setActiveFeature(feature);
if (feature.type != "POINT") {
// 增加删除按钮
addDeleteIcon(feature, feature.shape);
}
});
//右键 目前只针对"点"双击选中右键触发
gMap.events.on("featureDeleted", (feature) => {
if (feature.type == "POINT") {
// 根据id删除对应feature
state.gFirstFeatureLayer.removeFeatureById(feature.id);
// 删除对应text
state.gFirstTextLayer.removeTextById(feature.props.textId);
}
});
// 单机空白取消编辑
gMap.events.on("featureUnselected", () => {
// 取消featureSelected
state.editId = "";
deIcon();
gMap.setActiveFeature(null);
});
// 更新完
gMap.events.on("featureUpdated", (feature, shape) => {
console.log(feature, 'feature', shape);
// 更新或者移动需要重新设置删除图标
deIcon();
feature.updateShape(shape);
// 删除对应文本
state.gFirstTextLayer.removeTextById(feature.props.textId);
// 设置文本
const textPosition = setText(shape, feature.type)
addLayerText(feature.props.textId, feature.props.name, textPosition)
if (feature.type != "POINT") {
addDeleteIcon(feature, shape);
}
getFeatures()
});
// 删除
gMap.events.on("FeatureDeleted", () => {
console.log(2222222);
// that.gFirstFeatureLayer.removeFeatureById(that.editId);
});
}
// 获取坐标 需要自行添加
const getPoints = (feature) => {
switch (feature.type) {
case "RECT":
return feature.getPoints();
case "LINE":
return [feature.shape.start, feature.shape.end];
case "POLYLINE":
return feature.shape.points;
case "POLYGON":
return feature.shape.points;
default:
return [];
}
}
//填充事件
const Fill = () => {
console.log("填充事件");
if (!state.editId) {
ElMessage({
type: "info",
message: "请选择填充标注",
});
return
}
let fill = state.gFirstFeatureLayer.getFeatureById(state.editId);
console.log("--填充对象--", fill);
fill.style.fillStyle = "#FFDAB9";
fill.style.fill = true;
//刷新map
state.gMap.refresh();
}
//撤销
const Revoke = () => {
console.log("撤销");
getFeatures();
state.allFeatures.pop();
//刷新map
state.gMap.refresh();
console.log(state.allFeatures, "--所有操作--");
}
const zoomIn = () => {
state.gMap.zoomIn();
}
const zoomOut = () => {
state.gMap.zoomOut();
}
const setMode = (mode) => {
state.mode = mode;
}
//图片复位
const restoration = () => {
state.gMap.centerAndZoom({
center: state.centerObj,
zoom: 500
})
}
//手动设置坐标
function setGraph (data) {
setDrawingStyle('POLYGON')
addFeature([
{
"x": 163.125,
"y": 63.125
},
{
"x": 165.625,
"y": 89.375
},
{
"x": 226.25,
"y": 106.875
},
{
"x": 251.875,
"y": 72.5
},
{
"x": 268.75,
"y": 37.5
},
{
"x": 226.25,
"y": 23.125
}
], 'POLYGON');
}
function getRle () {
}
return {
...toRefs(state),
initMap,
getFeatures,
setDrawingStyle,
addFeature,
getName,
addDeleteIcon,
addEvent,
zoomIn,
zoomOut,
setMode,
deIcon,
getPoints,
Fill,
Revoke,
setGraph,
restoration,
addLayerText,
getRle,
main_map
}
},
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style lang="scss" scoped>
.label_main {
display: flex;
margin-left: 20px;
flex-direction: column;
height: 100%;
}
.button-wrap {
display: flex;
/* flex-direction: column; */
position: relative;
z-index: 99;
}
.label_btn {
padding: 0 10px;
border: 1px solid #ccc;
border-right: 0;
border-radius: 0;
}
.label_btn:first-child {
border-radius: 4px 0 0 4px;
}
.label_btn:last-child {
border: 1px solid #ccc;
border-radius: 0 4px 4px 0;
}
#map {
/* margin: 0 auto; */
overflow: hidden;
position: relative;
height: 450px;
width: 650px;
border: 1px dashed #ccc;
// margin-left: 36px;
}
.main_map {
position: relative;
}
.zoom-icon-wrapper {
position: absolute;
/* left: -36px; */
top: 0px;
z-index: 1000;
}
.zoom-icon-plus {
width: 30px;
height: 30px;
line-height: 20px;
text-align: center;
border: 3px solid #6495ed;
font-size: 20px;
border-top-left-radius: 6px;
border-top-right-radius: 6px;
color: #ff8c00;
cursor: pointer;
}
.zoom-icon-plus:hover {
border-color: #4169e1;
}
.zoom-icon-minus {
margin-top: 6px;
width: 30px;
height: 30px;
line-height: 20px;
text-align: center;
border: 3px solid #6495ed;
font-size: 25px;
border-bottom-left-radius: 6px;
border-bottom-right-radius: 6px;
color: #ff8c00;
cursor: pointer;
}
.zoom-icon-minus:hover {
border-color: #4169e1;
}
/* 删除图标 */
#delete01 {
width: 10px;
height: 10px;
}
.el-button+.el-button {
margin-left: 0px;
}
</style>
使用
//引入
import { dLabelTemplate } from '@/components'
<dLabelTemplate :imgUrl="imgUrl" divId="map3" />
源代码(star点起来):https://github.com/dingyang9642/AILabel (已经关了)
这个是源代码地址:http://ailabel.com.cn/public/ailabel/demo/index.js
API文档:http://ailabel.com.cn/public/ailabel/api/index.html
Demo文档:http://ailabel.com.cn/public/ailabel/demo/index.html
Demo1文档:http://ailabel.com.cn/public/ailabel/demo/label/index.html
npm地址:https://www.npmjs.com/package/ailabel
老版API文档(小于v5.0.0):https://dingyang9642.github.io/AILabel/old_version_docs/#/