mapCutter.vue 8.72 KB
<!--
 * @Date: 2025-01-22 11:40:12
 * @LastEditors: hookehuyr hookehuyr@gmail.com
 * @LastEditTime: 2025-01-24 17:33:54
 * @FilePath: /map-demo/src/views/mapCutter.vue
 * @Description: 文件描述
-->
<template>
  <div style="display: flex; padding: 1rem 0 0 1rem; gap: 1rem;  align-items: center;">
    <div>目标地图经纬度</div>
    <el-input
      v-model="map_center"
      style="width: 240px"
      placeholder="输入经纬度"
      @blur="onCenterBlur"
    />
  </div>
  <div style="display: flex; padding: 1rem; gap: 1rem;">
    <div style="display: flex; align-items: center;">
      <div>地图层级:&nbsp;</div>
      <el-select v-model="map_zoom" placeholder="地图层级" style="width: 240px" @change="onZoomChange">
          <el-option
            v-for="item in zoom_options"
            :key="item.value"
            :label="item.label"
            :value="item.value"
          />
        </el-select>
    </div>

    <div style="display: flex; align-items: center;">
      <div>上传图片范围:&nbsp;</div>
      <el-input
        v-model="map_left_bottom_range"
        style="width: 240px"
        placeholder="输入左下角经纬度"
        @blur="onLBRangeBlur"
      />
      &nbsp;
      <el-input
        v-model="map_right_top_range"
        style="width: 240px"
        placeholder="输入右上角的经纬度"
        @blur="onRTRangeBlur"
      />
    </div>
    <div v-if="showUpload">
      <input type="file" @change="handleImageUpload" />
      <el-button type="primary" @click="cutTiles">切割瓦片</el-button>
    </div>
  </div>
  <div id="map-container"></div>
  <div>

  </div>
  <div v-if="showUpload" class="controls">
    <el-button type="primary" :icon="Top" @click="moveImage('up')">图片上移</el-button>
    <el-button type="primary" :icon="Bottom" @click="moveImage('down')">图片下移</el-button>
    <el-button type="primary" :icon="Back" @click="moveImage('left')">图片左移</el-button>
    <el-button type="primary" :icon="Right" @click="moveImage('right')">图片右移</el-button>
    <el-button type="primary" :icon="Plus" @click="scaleImage(1.01)">图片放大</el-button>
    <el-button type="primary" :icon="Minus" @click="scaleImage(0.99)">图片缩小</el-button>
    <el-button type="primary" @click="rotateMap(10)">地图顺时针旋转</el-button>
    <el-button type="primary" @click="rotateMap(-10)">地图逆时针旋转</el-button>
  </div>

  <div v-if="log_lnglat" style="position: fixed; top: 8rem; left: 1rem; color: black; background-color: white; padding: 1rem;">
    <div style=" display: flex; align-items: center; justify-content: center;">经纬度:{{ log_lnglat }}&nbsp;&nbsp;&nbsp;<el-button @click="copyText(log_lnglat)" type="primary" :icon="Brush"  size="small">复制</el-button></div>
  </div>
</template>

<script setup>
import { onMounted, ref, computed } from "vue";
// import AMapLoader from "@amap/amap-jsapi-loader";
import { TileCutter } from "@/utils/TileCutter";
import { Top, Bottom, Back, Right, Plus, Minus, Brush } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'

const map = ref(null);
const imageLayer = ref(null);
const imageURL = ref(""); // 存储上传的图片
const bounds = ref(null); // 图片覆盖的边界
const mapRotation = ref(0); // 存储地图旋转角度
const zooms = ref([17, 19]);

const map_zoom = ref(17)

const zoom_options = [
  {
    value: 17,
    label: 17,
  },
  {
    value: 18,
    label: 18,
  },
]

const map_left_bottom_range = ref(null); // 120.583625,31.311858
const map_right_top_range = ref(null); // 120.591047,31.318265

const map_center = ref(null);

const log_lnglat = ref('') // 获取当前地址经纬度

onMounted(async () => {
  loadMap();
});

function loadMap() {
  // 初始化地图
  map.value = new AMap.Map('map-container', {
    zoom: 17,
    zooms: zooms.value,
    center: [120.587648, 31.314616],
    rotation: 0, // 初始地图角度
    layers: [
      new AMap.TileLayer.RoadNet(),
      new AMap.TileLayer.Satellite(),
    ],
  });


  // Canvas作为切片
  var layer1 = new AMap.TileLayer.Flexible({
    tileSize: 256,
    // cacheSize: 300,
    zIndex: 200,
    createTile: function (x, y, z, success, fail) {
      var c = document.createElement('canvas');
      c.width = c.height = 256;

      var cxt = c.getContext("2d");
      cxt.font = "15px Verdana";
      cxt.fillStyle = "#ff976a";
      cxt.strokeStyle = "#ff976a";
      cxt.strokeRect(0, 0, 256, 256);
      cxt.fillText('(' + [x, y, z].join(',') + ')', 10, 30);

      // 通知API切片创建完成
      success(c);
    }
  });

  layer1.setMap(map.value);

  // 监听 zoomchange 事件
  // map.value.on('zoomchange', () => {
  //   const currentZoom = map.value.getZoom();
  //   if (currentZoom === 18) {
  //     map_zoom.value = 18;
  //   } else {
  //     map_zoom.value = 17;
  //   }
  // });

  // 添加地图点击事件
  map.value.on("click", showInfoClick);
}

function handleImageUpload(event) {
  const file = event.target.files[0];
  if (!file) return;

  const reader = new FileReader();
  reader.onload = (e) => {
    imageURL.value = e.target.result;
    addImageToMap(imageURL.value);
  };
  reader.readAsDataURL(file);
}

function addImageToMap(url) {
  if (imageLayer.value) {
    map.value.remove(imageLayer.value);
  }

  // TAG: 设置图片范围
  bounds.value = new AMap.Bounds(map_left_bottom_range.value, map_right_top_range.value); // 设置图片范围 左下角 (西南) -> 右上角 (东北)
  imageLayer.value = new AMap.ImageLayer({
    url: url,
    bounds: bounds.value,
    zooms: [17, 19],
    opacity: 0.6 // 透明度 (0 完全透明, 1 完全不透明)
  });

  map.value.add(imageLayer.value);
}


function cutTiles() {
  if (!imageURL.value) {
    alert("请先上传图片");
    return;
  }
  // TAG: 切割瓦片等级
  TileCutter(imageURL.value, bounds.value, map_zoom.value); // 传入图片、地图范围、缩放级别
}

// ✅ 1. 位置移动
const moveImage = (direction) => {
const offset = 0.0001; // 移动步长(经纬度差值)
const sw = bounds.value.getSouthWest();
const ne = bounds.value.getNorthEast();

let newBounds;
switch (direction) {
  case "up":
    newBounds = new AMap.Bounds([sw.lng, sw.lat + offset], [ne.lng, ne.lat + offset]);
    break;
  case "down":
    newBounds = new AMap.Bounds([sw.lng, sw.lat - offset], [ne.lng, ne.lat - offset]);
    break;
  case "left":
    newBounds = new AMap.Bounds([sw.lng - offset, sw.lat], [ne.lng - offset, ne.lat]);
    break;
  case "right":
    newBounds = new AMap.Bounds([sw.lng + offset, sw.lat], [ne.lng + offset, ne.lat]);
    break;
}

bounds.value = newBounds;
imageLayer.value.setBounds(bounds.value);
};

// ✅ 2. 缩放图片
const scaleImage = (factor) => {
const sw = bounds.value.getSouthWest();
const ne = bounds.value.getNorthEast();

const centerX = (sw.lng + ne.lng) / 2;
const centerY = (sw.lat + ne.lat) / 2;
const newWidth = (ne.lng - sw.lng) * factor;
const newHeight = (ne.lat - sw.lat) * factor;

bounds.value = new AMap.Bounds(
  [centerX - newWidth / 2, centerY - newHeight / 2],
  [centerX + newWidth / 2, centerY + newHeight / 2]
);

imageLayer.value.setBounds(bounds.value);
};

// 旋转地图
const rotateMap = (deltaAngle) => {
  if (!map.value) return;

  mapRotation.value += deltaAngle;

  console.log(`地图旋转: ${mapRotation.value}°`);

  map.value.setRotation(mapRotation.value);
};


const onZoomChange = (value) => { // 调整地图图层
  // map.value.setZoom(value);
}

const onLBRangeBlur = () => {
  const str = map_left_bottom_range.value;
  const formattedArray = str?.split(',').map(Number);
  map_left_bottom_range.value = formattedArray;
}
const onRTRangeBlur = () => {
  const str = map_right_top_range.value;
  const formattedArray = str?.split(',').map(Number);
  map_right_top_range.value = formattedArray;
}

const showUpload = computed(() => {
  return map_left_bottom_range.value && map_right_top_range.value ? true : false;
});

const onCenterBlur = () => {
  const str = map_center.value;
  const formattedArray = str.split(',').map(Number);
  map.value.setCenter(formattedArray);
}

const showInfoClick = (e) => {
  var text =
    e.lnglat.getLng() +
    "," +
    e.lnglat.getLat()
  console.log(text);
  log_lnglat.value = text;
}

const copyText = (text) => {
  navigator.clipboard.writeText(text)
  .then(() => {
    ElMessage({
      message: '复制成功',
      type: 'success',
      plain: true,
    })
  })
  .catch(err => {
    ElMessage({
      message: '复制失败',
      type: 'warning',
      plain: true,
    })
    console.error('复制失败:', err);
  });
}
</script>

<style>
#map-container {
  width: 100%;
  height: 100vh;
}

.controls {
  position: absolute;
  top: 8rem;
  right: 10px;
  background: rgba(255, 255, 255, 0.8);
  padding: 10px;
  border-radius: 5px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
</style>