openlayers + FabricJs 实现在地图上编辑图片(拉伸、旋转、缩放、平移)

        博主是一名webgis小白。学习webgis到发布这边文章开始只有一周时间,实现这个效果走了很多的弯路。博主研究了官网文档后,官方文档并没有对于图片有这种效果。下方是效果展示:

cf0423e6ac544a61ba22016a9c32effa.gif

作者微信: BH_JJ89757  觉得不错可以添加我好有。可以一起沟通!

<style scoped>

.maps-container{
  width: 100vw;
  height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  position: relative;
  #maps{
    width: 100vw;
    height: 100vh;
    position: absolute;
  }
  .fabric-container{
    width: 100vw;
    height: 100vh;
    position: absolute;

  }
}


.input-container{
    position: absolute;
    top: 0px;
    left: 0px;
  width: 150px;
  height: 30px;
  color: #ffffff;
  padding-left: 5px;
  .input{
    border-radius: 10px;

    outline: none;
    width: 100%;
    height: 100%;
    border: 2px solid #535bf2;
    background-color: #999ff3;
    color: #ffffff;
    letter-spacing: 3px;
    text-align: center;
  }
}
</style>

<template>
  <div class="maps-container">
    <!-- 地图视图 -->
    <div id="maps" ref="maps">

    </div>
    <!-- 编辑层 -->
    <div class="fabric-container" :style="{zIndex: state.fabric_z_index}">
        <canvas id="fabric"></canvas>
    </div>

    <!-- 输入框 -->
    <div class="input-container" :style="{
      zIndex: state.fabric_z_index,
       left: (state._now_feature.start_left - state._now_feature.width / 2) - 10 + 'px',
       top: (state._now_feature.start_top + state._now_feature.height / 2) + 10 + 'px',
       width: state._now_feature.width + 'px',
    }">
      <input class="input" v-model="state.inputValue" type="text" placeholder=""/>
    </div>
  </div>


</template>

<script setup>


/*******************************************/
/********** openLayers + FabricJs **********/
/*******************************************/
import html2canvas from "html2canvas";
import {onMounted, reactive, ref} from "vue";

import { fabric } from 'fabric';
import View from "ol/View";
import Map from "ol/Map";
import TileLayer from "ol/layer/Tile";
import {XYZ} from "ol/source";
import VectorSource from "ol/source/Vector";
import {Feature} from "ol";
import {Fill, Icon, Stroke, Style, Text} from "ol/style";
import VectorLayer from "ol/layer/Vector";
import {Point} from "ol/geom";
import {toLonLat} from "ol/proj";

const maps = ref(null) // 获取元素Dom
const state = reactive({
  MapsViewOption: { // 地图视图参数
    center: [116.39702518856394, 39.918590567855425], // 默认视图中心点经纬度
    projection: "EPSG:4326", // EPSG码(默认情况下是:EPSG:3857)
    zoom: 16, // 默认视图层级
    maxZoom: 17, // 最大地图层级
    minZoom: 1, // 最晓地图层级
  },
  mapsXYZ: "https://map.geoq.cn/ArcGIS/rest/services/ChinaOnlineStreetPurplishBlue/MapServer/tile/{z}/{y}/{x}", // 地图XYZ 免费的XYZ瓦片地图地址
  fabric_z_index: -1, // 默认编辑层 的定位层级为 -1 在地图之下
  _now_feature: {}, // 当前编辑的是哪一个feature
})

// 准备的数据
let layers = [ // 编辑层上元素 数据
      {
        url: 'http://pic.soutu123.com/element_origin_min_pic/16/10/04/0157f29624c2501.jpg%21/fw/700/quality/90/unsharp/true/compress/true',
        start_left: 100,  // 起始坐标 left
        start_top: 50, // 起始坐标 top
        width: 83, // 图片宽度
        height: 83, // 图片高度
        originWidth: 650, // 图片原始高度
        originHeight: 425, // 图片原始宽度
        angle: 0, // 图片旋转角度
        img_feature: {}, // 图片特征对象(fabric的feature对象)
        img_vector_source: {}, // 图片保存的矢量图层对象(vector)
        inputValue: '' // 图片 // 输入框距离(这个值目前只是绑定内容 后续开发自己发掘)
      },
      {
        url: 'https://img2.baidu.com/it/u=2946301796,104274799&fm=253&fmt=auto&app=138&f=PNG?w=500&h=500',
        start_left: 500,
        start_top: 500,
        width: 83,
        height: 83,
        originWidth: 500,
        originHeight: 500,
        angle: 0,
        img_feature: {},
        img_vector_source: {},
        inputValue: ''
      }
    ]

let mapsObj = null // 实例化 maps 对象
let fabricObj = null // 实例化 Fabric 对象
let coordinate = [] // 鼠标在地图中的经纬度
onMounted( () => {
  // 初始化地图
  mapsObj = init_maps()

  // 创建FabricJs用于地图编辑层
  fabricObj = init_fabric()

  // 设置编辑层的宽高和地图视图大小一致
  set_Fabric_width_height()

  // 添加地图瓦片至Layer
  mapsObj.addLayer(create_maps_layer_XYZ())

  // 通过监听地图加载第一次加载 使用once 使用on事件会多次触发 然后添加对应 layer 至地图上
  mapsObj.once('rendercomplete', ()=>{
    layers.forEach((layersItem, layersIndex) => {
      add_image_static_to_map(layersItem, layersIndex)
    })
  })

  // 给地图添加点击事件,
  mapsObj.on('click', function (evt) {

    // 在点击位置检查特征,返回第一个命中的特征
    let _now_feature = mapsObj.forEachFeatureAtPixel(evt.pixel, function(feature, layer) {
      return feature;
    });

    if(_now_feature){
      for (let i = 0; i < layers.length; i++) {
        let layerItem = layers[i]

        if(layers[i].img_feature.ol_uid === _now_feature.ol_uid){
          state._now_feature = layerItem
          let imgPoint = layers[i].img_feature.getGeometry().getCoordinates();

          // 动态获取图片 距离地图视图的left 和 top 值
          let _imgPoint_position = mapsObj.getPixelFromCoordinate([imgPoint[0],imgPoint[1]])
          // let _img_feature_position = [_imgPoint_position[0]- layerItem.width / 2,_imgPoint_position[1]- layerItem.height / 2]

          layerItem.start_left = _imgPoint_position[0]
          layerItem.start_top = _imgPoint_position[1]

          // 将图片添加到 fabric 编辑层
          add_image_static_to_fabric(layerItem)
        }
      }
    }

  });
  mapsObj.on('pointermove', function (event) {
    coordinate = toLonLat(event.coordinate); // 将地图坐标转换为经纬度坐标
    const hit = mapsObj.hasFeatureAtPixel(event.pixel);
    mapsObj.getTargetElement().style.cursor = hit ? 'pointer' : '';
  });
  // 监听地图层级变化
  // mapsObj.getView().on('change:resolution', function(){
  //   for (let i = 0; i < layers.length; i++) {
  //     //获取图标的样式对象
  //     let style = layers[i].img_feature.getStyle();
  //     console.log(this.getZoom())
  //     // 重新设置图标的缩放率,基于层级10来做缩放
  //     style.getImage().setScale(this.getZoom() / 17);
  //
  //     layers[i].img_feature.setStyle(style);
  //   }
  // })
})



/*
* 初始化 openLayers 地图 视图层
* */
const init_maps = () => {
  return new Map({
    layers: [],
    target: maps.value,
    view: new View(state.MapsViewOption)
  });
}

/*
* 初始化 Fabric 编辑层
* */
const init_fabric = ()=>{
  return new fabric.Canvas('fabric', {
    isDrawingMode: false,   //设置是否可以绘制
  })

}

/*
* 创建地图瓦片Layer
* */
const create_maps_layer_XYZ = ()=>{
  return new TileLayer({
    title: "地图瓦片",
    source: new XYZ({
      url: state.mapsXYZ,
    }),
  })
}

/*
* 设置编辑层操作大小和地图一致
* */
const set_Fabric_width_height = ()=>{
  const {width, height} = maps.value.getBoundingClientRect()
  fabricObj.setDimensions({width, height})
}

/*
* 添加图片到地图上
* */
const add_image_static_to_map = (layerItem) => {
  // console.log('添加到地图层', layerItem)
  // 创建一个空的矢量源
  let vectorSource = new VectorSource();

  // 创建一个特征并从图片创建一个样式
  let feature = new Feature({
    // population: 4000,
    // rainfall: 500,
  });

  feature.setStyle(
      new Style({
        image: new Icon(({
          src: layerItem.url,
          width: layerItem.width,
          height: layerItem.height,
          rotation: Math.PI / 180 * layerItem.angle,  // 设置图片的旋转角度,用弧度表示
        })),
        text: new Text({
          text: '',
          font: '20px Calibri,sans-serif',
          fill: new Fill({
            color: 'white',
          }),
          stroke: new Stroke({
            color: 'white',
            width: 2,
          }),
          offsetY: (70 < layerItem.angle < 90) || (240 < layerItem.angle < 280) ? (layerItem.height / 2) + 40 : (layerItem.height / 2) + 40,
          offsetX: layerItem.width - layerItem.width
        }),
  }));
  console.log(layerItem.angle)
  // 转换为地图坐标
  let mapCoordinate = mapsObj.getCoordinateFromPixel([layerItem.start_left,layerItem.start_top])

  // 添加地理位置
  feature.setGeometry(new Point(mapCoordinate));
  vectorSource.addFeature(feature);

  // 创建矢量图层
  let vectorLayer = new VectorLayer({
    source: vectorSource,
  });

  // 添加图层到地图
  mapsObj.addLayer(vectorLayer);

  console.log('图片被保存中心点位置是', layerItem.start_left,layerItem.start_top)

  // 保存这个特征
  layerItem.img_feature = feature
  layerItem.img_vector_layer = vectorLayer

  if(state.fabric_z_index !== -1){
    state.fabric_z_index = -1
  }
}

/*
* 添加到编辑层
* */
const add_image_static_to_fabric = (layerItem)=>{

  const { img_feature, img_vector_source, start_left, start_top, width, height, url, originWidth, originHeight, angle } = layerItem
  // console.log('添加到编辑层', layerItem)
  console.log('到编辑层获取的中心点位置是', layerItem.start_left,layerItem.start_top)
  fabric.Image.fromURL(url, (fabricImg) => {

    // 初始化图片的宽度、高度、角度
    fabricImg.set({
      scaleX: width / originWidth,
      scaleY: height / originHeight,
      left: start_left,
      top: start_top,
      originX: 'center',
      originY: 'center',
      angle: angle
    })

    // 初始化输入框参数
    state._now_feature.start_top = fabricImg.top + Math.abs(Math.sin(angle * Math.PI / 90) * (fabricImg.height * fabricImg.scaleY / 2) + 50)

    // 初始化编辑参数
    let _left_point = 0
    let _top_point = 0
    let _angle_point = angle


    // 监听图片在fabric(编辑层)的【添加】
    fabricImg.on('added',(e)=>{
      console.log('监听图片在fabric(编辑层)的【添加】')
      // 在地图上移除掉这个图片
      mapsObj.removeLayer(layerItem.img_vector_layer)
      // 打开编辑层
      state.fabric_z_index = 1
    })

    // 监听图片在fabric(编辑层)的【选中】
    fabricImg.on('selected',(e)=>{
      console.log('监听图片在fabric(编辑层)的【选中】')

    })

    // 监听图片在fabric(编辑层)的【取消选中】
    fabricImg.on('deselected',(e)=>{
      console.log('监听图片在fabric(编辑层)的【取消选中】')

      // 更新对应layers
      layerItem.width = fabricImg.width * fabricImg.scaleX
      layerItem.height = fabricImg.height * fabricImg.scaleY
      layerItem.start_left = _left_point || fabricImg.left
      layerItem.start_top = _top_point || fabricImg.top
      layerItem.angle = _angle_point
      state._now_feature = layerItem

      // 编辑层移除元素
      fabricObj.remove(fabricImg)

      // 重新渲染画布
      fabricObj.renderAll()

      // 将编辑后图片传给 地图 (添加至地图)
      add_image_static_to_map(layerItem)
    })

    // 监听图片在fabric(编辑层)的【移动】
    fabricImg.on('moving',(e)=>{
      console.log('监听图片在fabric(编辑层)的【移动】')
      // 计算点要素移动的位置
      _left_point = fabricImg.left
      _top_point = fabricImg.top
      _angle_point = fabricImg.angle

      state._now_feature.width = fabricImg.width * fabricImg.scaleX
      state._now_feature.height = fabricImg.height * fabricImg.scaleY
      state._now_feature.start_left = _left_point || fabricImg.left
      state._now_feature.start_top = _top_point || fabricImg.top
      state._now_feature.angle = _angle_point
      state._now_feature.start_top = fabricImg.top + Math.abs(Math.sin(_angle_point * Math.PI / 90) * (fabricImg.height * fabricImg.scaleY / 2) + 50)

    })

    // 监听图片在fabric(编辑层)的【旋转】
    fabricImg.on('rotating',(e)=>{
      console.log('监听图片在fabric(编辑层)的【旋转】')
      // 记录点要素旋转的角度
      _angle_point = fabricImg.angle

      state._now_feature.angle = _angle_point

      state._now_feature.start_top = fabricImg.top + Math.abs(Math.sin(_angle_point * Math.PI / 90) * (fabricImg.height * fabricImg.scaleY / 2) + 50)

    })

    // 监听图片在fabric(编辑层)的【缩放、放大】
    fabricImg.on('scaling',(e)=>{
      console.log('监听图片在fabric(编辑层)的【缩放、放大】')
      _left_point = fabricImg.left
      _top_point = fabricImg.top
      _angle_point = fabricImg.angle

      state._now_feature.width = fabricImg.width * fabricImg.scaleX
      state._now_feature.height = fabricImg.height * fabricImg.scaleY
      state._now_feature.start_left = _left_point || fabricImg.left
      state._now_feature.start_top = _top_point || fabricImg.top
      state._now_feature.angle = _angle_point
      state._now_feature.start_top = fabricImg.top + Math.abs(Math.sin(_angle_point * Math.PI / 90) * (fabricImg.height * fabricImg.scaleY / 2) + 50)
    })
    fabricObj.add(fabricImg)

  },{ left: start_left,top: start_top })

}

</script>

 

 

  • 9
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值