Przeglądaj źródła

feat: 制作高德底图

bill 4 miesięcy temu
rodzic
commit
e2e3deade2

+ 1 - 1
src/example/basemap/dialog.vue

@@ -7,7 +7,7 @@
     @closed="showContent = false"
   >
     <div v-if="showContent">
-      <component :is="props.content" ref="contentRef" />
+      <component :is="props.content" ref="contentRef" v-bind="props.args" />
     </div>
     <template #footer>
       <div class="dialog-footer">

+ 184 - 115
src/example/basemap/gd-map/selectAMapImage.vue

@@ -1,27 +1,36 @@
 <template>
   <div class="search-layout">
-    <el-input v-model="keyword" placeholder="输入名称搜索" style="width: 350px" clearable>
+    <el-input
+      v-model="keyword"
+      placeholder="输入名称或经纬度(如113.281272,23.117661)搜索"
+      style="width: 400px"
+      @focus="showSearch = true"
+      clearable
+    >
       <template #append>
         <el-button :icon="Search" />
       </template>
     </el-input>
     <div class="rrr">
-      <div class="search-result" v-show="keyword && showSearch" ref="resultEl"></div>
-      <div class="search-sh" v-show="keyword">
-        <el-button style="width: 100%" @click="showSearch = !showSearch">
-          {{ showSearch ? "收起" : "展开" }}搜索结果
-        </el-button>
-      </div>
+      <div
+        class="search-result"
+        v-show="isKeySearch && keyword && showSearch"
+        ref="resultEl"
+      ></div>
     </div>
   </div>
   <div class="def-select-map-layout">
     <div class="def-select-map" ref="mapEl"></div>
+    <div
+      class="split-map"
+      :style="{ width: size.width + 'px', height: size.height + 'px' }"
+    ></div>
   </div>
 
-  <div class="def-map-info" v-if="info">
-    <p><span>纬度</span>{{ info.lat }}</p>
-    <p><span>经度</span>{{ info.lng }}</p>
-    <p><span>缩放级别</span>{{ info.zoom }}</p>
+  <div class="def-map-info" v-if="mapInfo">
+    <p><span>纬度</span>{{ mapInfo.lat }}</p>
+    <p><span>经度</span>{{ mapInfo.lng }}</p>
+    <p><span>缩放级别</span>{{ mapInfo.zoom }}</p>
   </div>
 </template>
 
@@ -29,35 +38,41 @@
 import AMapLoader from "@amap/amap-jsapi-loader";
 import { ElInput, ElButton } from "element-plus";
 import { Search } from "@element-plus/icons-vue";
-import { ref, watchEffect } from "vue";
-import { debounce } from "@/utils/shared";
+import { ref, watch } from "vue";
+import { analysisGPS, debounce } from "@/utils/shared";
+import { Size } from "@/utils/math";
+import { BasemapInfo, MarkInfo } from "..";
 
-export type MapImage = { blob: Blob | null; search: MapInfo | null; ratio: number };
-type MapInfo = { lat: number; lng: number; zoom: number; text: string };
+const props = withDefaults(
+  defineProps<{
+    info?: MarkInfo;
+    size?: Size;
+    pixelRatio?: number;
+  }>(),
+  { size: () => ({ width: 800, height: 600 }), pixelRatio: 1 }
+);
 
-const keyword = ref("");
-const showSearch = ref(true);
-const info = ref<MapInfo>();
-const searchInfo = ref<MapInfo>();
+let AMap: any;
+let map: any;
+let placeSearch: any;
 
-watchEffect(() => {
-  if (keyword.value) {
-    showSearch.value = true;
-  }
-});
+let __showMarker: any;
+const onlyMarker = (pos?: { lat: number; lng: number; text: string }) => {
+  __showMarker && map.remove(__showMarker);
+  __showMarker = null;
+  if (!pos) return;
 
-const mapEl = ref<HTMLDivElement>();
-const resultEl = ref<HTMLDivElement>();
-const searchAMap = ref<any>();
+  __showMarker = new AMap.Marker({
+    position: [pos.lng, pos.lat],
+    title: pos.text,
+  });
+  map.add(__showMarker);
+  map.panTo(__showMarker.getPosition());
+};
 
-let AMap: any;
-let map: any;
-watchEffect(async (onCleanup) => {
-  if (!mapEl.value || !resultEl.value) {
-    return;
-  }
+const initMap = async () => {
   AMap = await AMapLoader.load({
-    plugins: ["AMap.PlaceSearch", "AMap.Event"],
+    plugins: ["AMap.PlaceSearch", "AMap.Event", "AMap.Scale"],
     key: "e661b00bdf2c44cccf71ef6070ef41b8",
     version: "2.0",
   });
@@ -67,127 +82,170 @@ watchEffect(async (onCleanup) => {
       preserveDrawingBuffer: true,
     },
     resizeEnable: true,
+    zoom: props.info?.zoom || 17.22,
+  });
+  const scale = new AMap.Scale();
+  map.addControl(scale);
+
+  map.on("click", function (e: any) {
+    onlyMarker({
+      lat: e.lnglat.lat,
+      lng: e.lnglat.lng,
+      text: "点击位置",
+    });
   });
-  const placeSearch = new AMap.PlaceSearch({
+};
+
+const initSearch = () => {
+  placeSearch = new AMap.PlaceSearch({
     pageSize: 5,
     showCover: false,
     pageIndex: 1,
     map: map,
     panel: resultEl.value,
-    autoFitView: true,
   });
-  const setSearch = (data: any) => {
-    searchInfo.value = {
+  placeSearch.on("listElementClick", (e: any) => {
+    showSearch.value = false;
+    const data = e.data;
+    onlyMarker({
       text: data.pname + data.cityname + data.adname + data.address,
       lat: data.location.lat,
       lng: data.location.lng,
-      zoom: 0,
-    };
-  };
-
-  placeSearch.on("listElementClick", (e: any) => {
-    setSearch(e.data);
+    });
     showSearch.value = false;
   });
-  let clickMarker: any;
-
-  map.on("click", function (e: any) {
-    // 获取点击位置的经纬度坐标
-    var latitude = e.lnglat.lat;
-    var longitude = e.lnglat.lng;
-
-    searchInfo.value = {
-      text: "",
-      lat: latitude,
-      lng: longitude,
-      zoom: 0,
-    };
-    clickMarker && map.remove(clickMarker);
-    clickMarker = null;
-    // 在地图上添加标记
-    clickMarker = new AMap.Marker({
-      position: [longitude, latitude],
-      title: "点击位置",
-    });
 
-    map.add(clickMarker);
-  });
-  placeSearch.on("complete", function (result: any) {
+  placeSearch.on("complete", function () {
     setTimeout(() => {
       const markers = map.getAllOverlays("marker");
       for (const marker of markers) {
-        marker.on("click", () => {
-          clickMarker && map.remove(clickMarker);
-          clickMarker = null;
-          setSearch(marker._data);
-        });
+        if (marker !== __showMarker) {
+          map.remove(marker);
+        }
       }
     }, 500);
   });
+};
 
-  const getMapInfo = (): MapInfo => {
-    var zoom = map.getZoom(); //获取当前地图级别
-    var center = map.getCenter();
-    return {
-      text: "",
-      zoom,
-      lat: center.lat,
-      lng: center.lng,
-    };
+const mapInfo = ref<MarkInfo>();
+const updateMapInfo = () => {
+  var zoom = map.getZoom(); //获取当前地图级别
+  var center = map.getCenter();
+  mapInfo.value = {
+    text: "",
+    zoom,
+    lat: center.lat,
+    lng: center.lng,
   };
-  //绑定地图移动与缩放事件
-  map.on("moveend", () => {
-    info.value = getMapInfo();
-  });
-  map.on("zoomend", () => {
-    info.value = getMapInfo();
-  });
-  searchAMap.value = placeSearch;
+};
+
+const isKeySearch = ref(false);
+const keyword = ref("");
+const showSearch = ref(true);
+const init = async () => {
+  await initMap();
+  await initSearch();
+
+  map.on("moveend", updateMapInfo);
+  map.on("zoomend", updateMapInfo);
 
-  onCleanup(() => {
-    searchAMap.value = null;
+  const search = debounce(() => placeSearch.search(keyword.value), 100);
+  const stopWatch = watch(
+    keyword,
+    () => {
+      const gps = analysisGPS(keyword.value);
+      if (gps) {
+        isKeySearch.value = false;
+        onlyMarker({ lat: gps.y, lng: gps.x, text: "直接输入经纬度" });
+        console.log(gps);
+      } else {
+        isKeySearch.value = true;
+        search();
+      }
+    },
+    { immediate: true }
+  );
+  if (props.info) {
+    onlyMarker(props.info);
+  }
+  return () => {
     map.destroy();
-  });
-});
+    stopWatch();
+    map = null;
+    placeSearch = null;
+  };
+};
 
-const search = debounce((keyword: string) => {
-  searchAMap.value.search(keyword);
-}, 1000);
-watchEffect(() => {
-  searchAMap.value && search(keyword.value);
+const mapEl = ref<HTMLDivElement>();
+const resultEl = ref<HTMLDivElement>();
+watch([mapEl, resultEl], async (_a, _b, onCleanup) => {
+  if (!mapEl.value || !resultEl.value) return;
+  onCleanup(await init());
 });
 
 const getPixelAspectRatio = () => {
   // 获取地图视口的经纬度范围
   const bounds = map.getBounds();
-
   // 计算视口的宽度和高度(单位为米)
   const southWest = bounds.getSouthWest(); // 西南角
   const northEast = bounds.getNorthEast(); // 东北角
   const width = AMap.GeometryUtil.distance(
     [southWest.lng, northEast.lat],
     [northEast.lng, northEast.lat]
-  ); // 经度变化
-  // const height = AMap.GeometryUtil.distance(
-  //   [southWest.lng, southWest.lat],
-  //   [southWest.lng, northEast.lat]
-  // ); // 纬度变化
-
-  return width / mapEl.value!.offsetWidth;
-  // console.log(width / height);
-  // console.log(mapEl.value?.offsetWidth / mapEl.value?.offsetHeight);
+  );
+  console.log(width / (mapEl.value!.offsetWidth * props.pixelRatio));
+  return width / (mapEl.value!.offsetWidth * props.pixelRatio);
 };
 
 const submit = () => {
-  return new Promise<MapImage>((resolve) => {
+  return new Promise<BasemapInfo>((resolve, reject) => {
     if (mapEl.value) {
-      getPixelAspectRatio();
+      console.log("ha?");
+      let info = undefined;
+      if (__showMarker) {
+        const pos = __showMarker.getPosition();
+        info = {
+          lat: pos.lat,
+          lng: pos.lng,
+          zoom: map.zoom,
+          text: __showMarker._opts.title,
+        };
+      }
+
       const canvas = mapEl.value.querySelector("canvas") as HTMLCanvasElement;
-      canvas.toBlob((blob) =>
-        resolve({ blob, search: searchInfo.value!, ratio: getPixelAspectRatio() })
+      const mapPixelRatio = canvas.width / canvas.offsetWidth;
+      const size = {
+        width: canvas.width / mapPixelRatio,
+        height: canvas.height / mapPixelRatio,
+      };
+      const box = {
+        x: (size.width - props.size.width) / 2,
+        y: (size.height - props.size.height) / 2,
+        width: props.size.width,
+        height: props.size.height,
+      };
+      const tempCanvas = document.createElement("canvas")!;
+      tempCanvas.width = box.width * props.pixelRatio;
+      tempCanvas.height = box.height * props.pixelRatio;
+      const tempCtx = tempCanvas.getContext("2d")!;
+      tempCtx.drawImage(
+        canvas,
+        box.x * mapPixelRatio,
+        box.y * mapPixelRatio,
+        box.width * mapPixelRatio,
+        box.height * mapPixelRatio,
+        0,
+        0,
+        tempCanvas.width,
+        tempCanvas.height
+      );
+      tempCanvas.toBlob(
+        (blob) => resolve({ blob: blob!, info, ratio: getPixelAspectRatio() }),
+        "jpeg",
+        1
       );
     } else {
-      resolve({ blob: null, search: null, ratio: 1 });
+      reject("地图未初始化");
     }
   });
 };
@@ -222,10 +280,12 @@ defineExpose({ submit });
 
 .def-map-info {
   margin-top: 10px;
+
   p {
     font-size: 14px;
     color: rgba(0, 0, 0, 0.85);
     display: inline;
+
     &:not(:last-child)::after {
       content: ",";
       margin-right: 6px;
@@ -238,9 +298,9 @@ defineExpose({ submit });
 }
 
 .def-select-map-layout {
-  --scale: 1.5;
+  // --scale: 1.5;
   width: 100%;
-  padding-top: calc((390 / 540) * 100%);
+  padding-top: calc((800 / 1326) * 100%);
   position: relative;
   z-index: 1;
 }
@@ -252,4 +312,13 @@ defineExpose({ submit });
   width: 100%;
   height: 100%;
 }
+
+.split-map {
+  position: absolute;
+  left: 50%;
+  top: 50%;
+  border: 1px dashed red;
+  transform: translate(-50%, -50%);
+  pointer-events: none;
+}
 </style>

+ 14 - 5
src/example/basemap/index.ts

@@ -1,7 +1,8 @@
-import { reactive } from "vue";
+import { markRaw, reactive } from "vue";
 import selectAMapImage from "./gd-map/selectAMapImage.vue";
+import { Size } from "@/utils/math";
 
-export type BasemapInfo = { blob: Blob; scale: number, ratio: number };
+export type BasemapInfo = { blob: Blob; info?: MarkInfo, ratio: number };
 
 type Props = {
   title: string;
@@ -9,18 +10,26 @@ type Props = {
   visiable: boolean;
   height?: string;
   content: any;
+  args?: {
+    info?: MarkInfo;
+    size?: Size;
+    pixelRatio?: number;
+  },
   submit: (info: BasemapInfo) => void;
   cancel: () => void;
 };
 export const props = reactive({
   title: "底图设置",
-  width: "800px",
+  width: "1200px",
 }) as Props;
 
-export const getAMapInfo = () =>
+export type MarkInfo = { lat: number; lng: number; text: string; zoom?: number };
+
+export const getAMapInfo = (args?: Props['args']) =>
   new Promise<BasemapInfo>((resolve, reject) => {
-    props.content = selectAMapImage;
+    props.content = markRaw(selectAMapImage);
     props.title = "选择高德地图底图";
+    props.args = args
     props.visiable = true;
     props.submit = (info: BasemapInfo) => {
       resolve(info);

+ 46 - 7
src/utils/shared.ts

@@ -136,17 +136,15 @@ export const debounce = <T extends (...args: any) => any>(
 };
 
 // 防抖
-export const frameEebounce = <T extends (...args: any) => any>(
-  fn: T,
-) => {
-  let count = 0
+export const frameEebounce = <T extends (...args: any) => any>(fn: T) => {
+  let count = 0;
   return function (...args: Parameters<T>) {
-    let current = ++count
+    let current = ++count;
     requestAnimationFrame(() => {
       if (current === count) {
         fn.apply(null, args);
       }
-    })
+    });
   };
 };
 
@@ -264,7 +262,6 @@ export const getResizeCorsur = (level = true, r = 0) => {
   }
 };
 
-
 export const diffArrayChange = <T extends Array<any>>(
   newItems: T,
   oldItems: T
@@ -289,3 +286,45 @@ export const diffArrayChange = <T extends Array<any>>(
     deleted: deletedItems,
   };
 };
+
+const DMSRG =
+  /(\d+(?:\.\d+)?)°(?:(\d+(?:\.\d+)?)['|′])?(?:(\d+(?:\.\d+)?)["|″])?$/;
+export const dmsCheck = (dms: string) => {
+  const r = DMSRG.exec(dms);
+  return r && Number(r[2] || 0) < 60 && (Number(r[3]) || 0) < 60;
+};
+// 度分秒转经纬度
+export const toDigital = (dms: string) => {
+  const r = DMSRG.exec(dms);
+  if (r) {
+    return round(
+      Number(r[1]) + Number(r[2] || 0) / 60 + (Number(r[3]) || 0) / 3600,
+      12
+    );
+  }
+};
+
+export const analysisGPS = (temp: string) => {
+  const getGPS = (t: any[]) => {
+    t = t.map((d) => (dmsCheck(d) ? toDigital(d) : Number(d)));
+    if (t.length > 1 && t.every((v) => !Number.isNaN(v))) {
+      return {
+        x: t[0],
+        y: t[1],
+      } as Pos;
+    }
+  };
+  function isValidWGS84(lon: number, lat: number) {
+    const isValidLon = lon >= -180 && lon <= 180;
+    const isValidLat = lat >= -90 && lat <= 90;
+    return isValidLon && isValidLat;
+  }
+
+  const splitChars = [",", " ", ","];
+  for (const splitChar of splitChars) {
+    const gps = getGPS(temp.split(splitChar));
+    if (gps && isValidWGS84(gps.x, gps.y)) {
+      return gps;
+    }
+  }
+};