Переглянути джерело

feat: 添加内置地图底图绘画

bill 1 тиждень тому
батько
коміт
aca96f564d

+ 2 - 2
package.json

@@ -7,10 +7,8 @@
     "dev:fuse": "vite --mode=criminaldev",
     "build:fusetest": "vite build --mode=firetest && vite build --mode=criminaltest && vite build --mode=cjzfiretest && vite build --mode=xmfiretest",
     "build:fuse": "vite build --mode=fire && vite build --mode=criminal && vite build --mode=cjzfire && vite build --mode=xmfire",
-
     "dev:hx": "vite --mode=hxdev",
     "build:hx": "vite build --mode=hx",
-
     "dev:jm": "vite --mode=jmdev",
     "build:jm": "vite build --mode=jm"
   },
@@ -18,6 +16,7 @@
     "@amap/amap-jsapi-loader": "^1.0.1",
     "@tarikjabiri/dxf": "^2.8.9",
     "@tweenjs/tween.js": "^25.0.0",
+    "@types/leaflet": "^1.9.20",
     "@types/node": "^22.9.0",
     "@types/svg-path-parser": "^1.1.6",
     "@types/three": "^0.169.0",
@@ -28,6 +27,7 @@
     "jspdf": "^3.0.1",
     "jszip": "^3.10.1",
     "konva": "^9.3.18",
+    "leaflet": "^1.9.4",
     "localforage": "^1.10.0",
     "martinez-polygon-clipping": "^0.7.4",
     "mitt": "^3.0.1",

+ 23 - 0
pnpm-lock.yaml

@@ -17,6 +17,9 @@ importers:
       '@tweenjs/tween.js':
         specifier: ^25.0.0
         version: 25.0.0
+      '@types/leaflet':
+        specifier: ^1.9.20
+        version: 1.9.20
       '@types/node':
         specifier: ^22.9.0
         version: 22.15.18
@@ -47,6 +50,9 @@ importers:
       konva:
         specifier: ^9.3.18
         version: 9.3.20
+      leaflet:
+        specifier: ^1.9.4
+        version: 1.9.4
       localforage:
         specifier: ^1.10.0
         version: 1.10.0
@@ -527,6 +533,12 @@ packages:
   '@types/estree@1.0.7':
     resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==}
 
+  '@types/geojson@7946.0.16':
+    resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==}
+
+  '@types/leaflet@1.9.20':
+    resolution: {integrity: sha512-rooalPMlk61LCaLOvBF2VIf9M47HgMQqi5xQ9QRi7c8PkdIe0WrIi5IxXUXQjAdL0c+vcQ01mYWbthzmp9GHWw==}
+
   '@types/lodash-es@4.17.12':
     resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==}
 
@@ -1419,6 +1431,9 @@ packages:
   konva@9.3.20:
     resolution: {integrity: sha512-7XPD/YtgfzC8b1c7z0hhY5TF1IO/pBYNa29zMTA2PeBaqI0n5YplUeo4JRuRcljeAF8lWtW65jePZZF7064c8w==}
 
+  leaflet@1.9.4:
+    resolution: {integrity: sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==}
+
   lie@3.1.1:
     resolution: {integrity: sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==}
 
@@ -2521,6 +2536,12 @@ snapshots:
 
   '@types/estree@1.0.7': {}
 
+  '@types/geojson@7946.0.16': {}
+
+  '@types/leaflet@1.9.20':
+    dependencies:
+      '@types/geojson': 7946.0.16
+
   '@types/lodash-es@4.17.12':
     dependencies:
       '@types/lodash': 4.17.16
@@ -3597,6 +3618,8 @@ snapshots:
 
   konva@9.3.20: {}
 
+  leaflet@1.9.4: {}
+
   lie@3.1.1:
     dependencies:
       immediate: 3.0.6

+ 49 - 12
src/example/components/slide/actions.ts

@@ -1,7 +1,7 @@
 import { DrawItem, shapeNames, ShapeType } from "@/index";
-import { ImageData, defaultStyle } from "@/core/components/image/index";
+import { defaultStyle } from "@/core/components/image/index";
 import { v4 as uuid } from "uuid";
-import { getAMapInfo } from "../../dialog/basemap";
+import { getMapInfo } from "../../dialog/basemap/index";
 import { selectAI } from "../../dialog/ai";
 import { drawPlatformResource } from "../../platform/platform-draw";
 import { selectFile } from "@/utils/dom";
@@ -74,7 +74,7 @@ export const imp: MenuItem = {
       name: "场景",
       handler: async (draw: Draw) => {
         const aiData = await selectAI();
-        console.log(aiData)
+        console.log(aiData);
         await drawPlatformResource(aiData, draw);
       },
     },
@@ -192,6 +192,49 @@ export const getMapImageItem = (url: string, size: Size) => ({
   mat: [1, 0, 0, 1, 0, 0],
 });
 
+export const selectMap = async () => {
+  const info = await getMapInfo({
+    activeGroupIndex: 1,
+    tileGroups: [
+      {
+        name: '高德影像地图',
+        tiles: [
+          {
+            minimumLevel: 1,
+            maximumLevel: 18,
+            url: `//wprd04.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=6&x={x}&y={y}&z={z}&layer=6&token=YOUR_API_KEY`
+          },
+          {
+            minimumLevel: 1,
+            maximumLevel: 18,
+            url: `//wprd04.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}&layer=6&token=YOUR_API_KEY`
+          },
+        ]
+      },
+      {
+        name: '高德矢量地图',
+        tiles: [
+          {
+            minimumLevel: 1,
+            maximumLevel: 18,
+            url: `//wprd04.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}&layer=6&token=YOUR_API_KEY`
+          },
+          {
+            minimumLevel: 1,
+            maximumLevel: 18,
+            url: `//wprd04.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}&layer=6&token=YOUR_API_KEY`
+          },
+        ]
+      },
+    ]
+  });
+  const url = await window.platform.uploadResourse(
+    new File([info.blob], "map.png")
+  );
+  // draw.store.addItem('image', getMapImageItem(url, info.size))
+  return { url, info };
+};
+
 export const dbImage: MenuItem = {
   value: uuid(),
   icon: "",
@@ -201,18 +244,12 @@ export const dbImage: MenuItem = {
       value: uuid(),
       icon: "",
       name: "高德地图",
-      handler: async (draw: Draw) => {
-        const info = await getAMapInfo();
-        const url = await window.platform.uploadResourse(
-          new File([info.blob], "map.png")
-        );
-        draw.store.addItem('image', getMapImageItem(url, info.size))
-      },
+      handler: selectMap
     },
   ],
 };
 
-export const showThree = ref(false)
+export const showThree = ref(false);
 export const test: MenuItem = {
   value: uuid(),
   icon: "debugger",
@@ -224,7 +261,7 @@ export const test: MenuItem = {
       icon: "",
       name: "显示三维视角",
       handler: () => {
-        showThree.value = !showThree.value
+        showThree.value = !showThree.value;
       },
     },
     ...dbImage.children!,

+ 42 - 0
src/example/dialog/basemap/gd-index.ts

@@ -0,0 +1,42 @@
+import { markRaw, reactive } from "vue";
+import selectAMapImage from "./gd-map/selectAMapImage.vue";
+import { Size } from "@/utils/math";
+
+export type BasemapInfo = { blob: Blob; info?: MarkInfo, size: Size };
+
+type Props = {
+  title: string;
+  width: string;
+  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: "1252px",
+}) as Props;
+
+export type MarkInfo = { lat: number; lng: number; text: string; zoom?: number };
+
+export const getAMapInfo = (args?: Props['args']) =>
+  new Promise<BasemapInfo>((resolve, reject) => {
+    props.content = markRaw(selectAMapImage);
+    props.title = "选择高德地图底图";
+    props.args = args
+    props.visiable = true;
+    props.submit = (info: BasemapInfo) => {
+      resolve(info);
+      props.visiable = false
+    };
+    props.cancel = () => {
+      reject("cancel");
+      props.visiable = false
+    };
+  });

+ 1 - 1
src/example/dialog/basemap/gd-map/selectAMapImage.vue

@@ -40,7 +40,7 @@ import { ElInput } from "element-plus";
 import { ref, watch } from "vue";
 import { analysisGPS, debounce } from "@/utils/shared";
 import { Size } from "@/utils/math";
-import { BasemapInfo, MarkInfo } from "..";
+import { BasemapInfo, MarkInfo } from "../gd-index";
 
 const props = withDefaults(
   defineProps<{

+ 16 - 13
src/example/dialog/basemap/index.ts

@@ -1,8 +1,17 @@
 import { markRaw, reactive } from "vue";
-import selectAMapImage from "./gd-map/selectAMapImage.vue";
+import selectMapImage from "./leaflet/index.vue";
 import { Size } from "@/utils/math";
+import { Tile } from "./leaflet/useLeaflet";
 
-export type BasemapInfo = { blob: Blob; info?: MarkInfo, size: Size };
+export type BasemapInfo = { blob: Blob; size: Size };
+export type TileGroup = {
+  tiles: Tile[];
+  name: string;
+};
+export type SelectMapImageProps = {
+    tileGroups: TileGroup[]
+    activeGroupIndex?: number;
+}
 
 type Props = {
   title: string;
@@ -10,25 +19,19 @@ type Props = {
   visiable: boolean;
   height?: string;
   content: any;
-  args?: {
-    info?: MarkInfo;
-    size?: Size;
-    pixelRatio?: number;
-  },
+  args?: SelectMapImageProps,
   submit: (info: BasemapInfo) => void;
   cancel: () => void;
 };
 export const props = reactive({
-  title: "底图设置",
+  title: "选择地图位置",
   width: "1200px",
 }) as Props;
 
-export type MarkInfo = { lat: number; lng: number; text: string; zoom?: number };
-
-export const getAMapInfo = (args?: Props['args']) =>
+export const getMapInfo = (args: SelectMapImageProps) =>
   new Promise<BasemapInfo>((resolve, reject) => {
-    props.content = markRaw(selectAMapImage);
-    props.title = "选择高德地图底图";
+    props.content = markRaw(selectMapImage);
+    props.title = "选择地图底图";
     props.args = args
     props.visiable = true;
     props.submit = (info: BasemapInfo) => {

+ 176 - 0
src/example/dialog/basemap/leaflet/index.vue

@@ -0,0 +1,176 @@
+<template>
+  <div class="map-layout">
+    <div class="search">
+      <el-input
+        :model-value="keyword[searchType]"
+        @update:model-value="(val) => (keyword[searchType] = val)"
+        :placeholder="`请输入${searchName}`"
+        class="input-with-select"
+      >
+        <template #prepend>
+          <el-select v-model="searchType" style="width: 100px">
+            <el-option
+              :label="type.label"
+              :value="type.value"
+              v-for="type in searchTypes"
+            />
+          </el-select>
+        </template>
+      </el-input>
+    </div>
+    <div class="map" ref="mapEle">
+      <div class="tiles-select">
+        <el-dropdown placement="bottom-end">
+          <icon name="close" size="30px" color="#000" />
+          <template #dropdown>
+            <el-dropdown-menu>
+              <el-dropdown-item
+                v-for="(group, ndx) in tileGroups"
+                @click="groupIndex = ndx"
+              >
+                {{ group.name }}
+              </el-dropdown-item>
+            </el-dropdown-menu>
+          </template>
+        </el-dropdown>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { computed, ref, shallowRef, watch, watchEffect } from "vue";
+import { useLMap, useSetLTileLayers } from "./useLeaflet";
+import { BasemapInfo, SelectMapImageProps } from "../index";
+import html2canvas from "html2canvas";
+import {
+  ElInput,
+  ElSelect,
+  ElOption,
+  ElDropdown,
+  ElDropdownMenu,
+  ElDropdownItem,
+} from "element-plus";
+
+const props = defineProps<SelectMapImageProps>();
+
+const mapEle = shallowRef<HTMLDivElement>();
+const lMap = useLMap(mapEle);
+const setTileLayers = useSetLTileLayers(lMap);
+const groupIndex = ref(0);
+const tiles = computed(() => props.tileGroups[groupIndex.value].tiles);
+
+watchEffect(
+  () => {
+    if (props.activeGroupIndex) {
+      groupIndex.value = props.activeGroupIndex;
+    }
+  },
+  { flush: "sync" }
+);
+
+watchEffect(
+  () => {
+    if (groupIndex.value > props.tileGroups.length - 1) {
+      groupIndex.value = 0;
+    }
+  },
+  { flush: "sync" }
+);
+
+watchEffect(() => {
+  setTileLayers(tiles.value);
+});
+
+const searchTypes = [
+  { label: "经纬度", value: "latlng" },
+  { label: "名称", value: "name" },
+];
+const searchType = ref(searchTypes[0].value as "latlng" | "name");
+const searchName = computed(
+  () => searchTypes.find((item) => item.value === searchType.value)!.label
+);
+const keyword = ref({
+  latlng: "",
+  name: "",
+});
+
+const search = (type: "latlng" | "name", keyword: string) => {
+  // if (keyword)
+};
+watch([searchType, keyword], ([type, keyword]) => search(type, keyword[type]));
+
+const submit = async (): Promise<BasemapInfo> => {
+  if (!lMap.value) {
+    throw "地图未初始化";
+  }
+
+  const bound = lMap.value.getBounds();
+  const southWest = bound.getSouthWest(); // 西南角
+  const northEast = bound.getNorthEast(); // 东北角
+  const width = lMap.value.distance(
+    [southWest.lng, northEast.lat],
+    [northEast.lng, northEast.lat]
+  );
+  const height = lMap.value.distance(
+    [northEast.lng, southWest.lat],
+    [northEast.lng, northEast.lat]
+  );
+
+  let blob: Blob;
+  try {
+    const canvas = await html2canvas(mapEle.value!, {
+      useCORS: true, // 允许跨域图片(瓦片需支持 CORS)
+      allowTaint: true, // 允许污染 Canvas(慎用)
+      scale: 2, // 提高分辨率
+      logging: false, // 关闭日志
+    });
+
+    blob = await new Promise<Blob>((resolve, reject) => {
+      canvas.toBlob((blob) => {
+        blob ? resolve(blob) : reject("截图失败");
+      });
+    });
+  } catch (e) {
+    throw "截图失败";
+  }
+
+  return {
+    blob,
+    size: { width, height },
+  };
+};
+
+defineExpose({ submit });
+</script>
+
+<style lang="scss" scoped>
+.map-layout {
+  display: flex;
+}
+
+.search {
+  margin-right: 24px;
+  flex: 1;
+}
+
+.map {
+  flex: 0 0 auto;
+  height: 600px;
+  width: 800px;
+  position: relative;
+}
+
+.tiles-select {
+  position: absolute;
+  z-index: 9999;
+  right: 20px;
+  top: 20px;
+}
+</style>
+
+<style lang="scss">
+.leaflet-container {
+  background: #fff;
+}
+</style>

+ 83 - 0
src/example/dialog/basemap/leaflet/useLeaflet.ts

@@ -0,0 +1,83 @@
+import { mergeFuns } from "@/utils/shared";
+import { map, Map, tileLayer, } from "leaflet";
+import 'leaflet/dist/leaflet.css'
+import { onUnmounted, Ref, shallowRef, watch, watchEffect } from "vue";
+
+export const useLMap = (domRef: Ref<HTMLDivElement | undefined>) => {
+  const lMap = shallowRef<Map>();
+  const init = (dom: HTMLDivElement) => {
+    const mapi = map(dom, {
+      center: [22.364093, 113.600356],
+      zoom: 18,
+      preferCanvas: true,
+      attributionControl: false,
+      zoomControl: false,
+      trackResize: false,
+      // crs: CRS.EPSG4326,
+      inertia: true,
+    });
+
+    lMap.value = mapi;
+    return () => {
+      mapi.remove();
+      lMap.value = undefined;
+    };
+  };
+
+  watchEffect((onCleanup) => domRef.value && onCleanup(init(domRef.value)));
+
+  return lMap;
+};
+
+export type Tile = {
+  url: string;
+  maximumLevel: number;
+  minimumLevel: number;
+};
+
+export const useSetLTileLayers = (lMap: Ref<Map | undefined>) => {
+  const initTileLayers = (map: Map, tiles: Tile[]) => {
+    const lTileLayers = tiles.map((tile) => {
+      const lTileLayer = tileLayer(tile.url, {
+        crossOrigin: true,
+        minNativeZoom: tile.minimumLevel,
+        maxNativeZoom: tile.maximumLevel,
+        minZoom: 1,
+        maxZoom: 25,
+      });
+      map.addLayer(lTileLayer);
+      return lTileLayer;
+    });
+
+    const onDestory = mergeFuns(
+      lTileLayers.map((t) => () => {
+        map.removeLayer(t);
+        t.remove();
+      })
+    );
+
+    return {
+      lTileLayers,
+      onDestory,
+    };
+  };
+
+  let clear = () => {
+    _clear && _clear();
+    _clear = null;
+  };
+  let _clear: (() => void) | null = null;
+
+  onUnmounted(clear)
+
+  return (tiles: Tile[]) => {
+    clear();
+    _clear = watch(
+      lMap,
+      (lMap, _, onCleanup) => {
+        lMap && tiles && onCleanup(initTileLayers(lMap, tiles).onDestory)
+      },
+      { immediate: true }
+    );
+  };
+};

+ 4 - 4
src/example/fuse/views/tabulation/gen-tab.ts

@@ -316,10 +316,10 @@ export const repTabulationStore = async (
     }
     layer.icon = icons;
   }
-  if (import.meta.env.DEV) {
-    layer.table = repData.table
-    layer.text = repData.text
-  }
+  // if (import.meta.env.DEV) {
+  //   layer.table = repData.table
+  //   layer.text = repData.text
+  // }
 
   store.layers[defaultLayer] = layer;
 

+ 57 - 12
src/example/fuse/views/tabulation/index.vue

@@ -8,7 +8,7 @@
       <Header v-if="inited" :title="title" />
     </template>
     <template #slide>
-      <Slide v-if="inited" />
+      <Slide v-if="inited" @update-map-image="setMapHandler" />
     </template>
   </Container>
 
@@ -44,38 +44,82 @@ import { MathUtils } from "three";
 import { components } from "@/core/components";
 import { ShapeType } from "@/index";
 import { mergeFuns, round } from "@/utils/shared";
-import { PaperKey } from "@/example/components/slide/actions";
-import { StoreData } from "@/core/store/store";
 import { getImageSize } from "@/utils/shape";
 import { tabCustomStyle } from "../defStyle";
 import { defaultLayer } from "@/constant";
+import { Size } from "@/utils/math";
 
 const uploadResourse = window.platform.uploadResourse;
 const full = ref(false);
 const draw = ref<Draw>();
 
-const setMap = async (paperKey: PaperKey, compass: number, store: StoreData) => {
-  const data = tabulationData.value;
+const setMap = async (config: { url: string; size: Size }) => {
+  const store = tabulationData.value.store;
+  const paperKey = tabulationData.value.paperKey;
+  const compass = 0;
 
-  if (data.mapUrl && data.high && data.width) {
-    const size = await getImageSize(data.mapUrl);
+  if (config.url && config.size.height && config.size.width) {
+    const size = await getImageSize(config.url);
     const cover = {
-      url: data.mapUrl,
+      url: config.url,
       ...size,
       proportion: {
-        scale: (data.width / size.width) * 1000,
+        scale: (config.size.width / size.width) * 1000,
         unit: "mm",
       },
     };
-    if (!data.store.config) {
+    if (!tabulationData.value.store.config) {
       const layer = await genTabulationData(paperKey, compass, cover);
-      data.store.layers[defaultLayer] = layer;
+      tabulationData.value.store.layers[defaultLayer] = layer;
     } else {
       await repTabulationStore(paperKey, compass, cover, store);
     }
   }
 };
 
+const setMapHandler = async (config: { url: string; size: Size }) => {
+  const size = await getImageSize(config.url);
+  const cover = {
+    url: config.url,
+    ...size,
+    proportion: {
+      scale: (config.size.width / size.width) * 1000,
+      unit: "mm",
+    },
+  };
+  const data = await genTabulationData(tabulationData.value.paperKey, undefined, cover);
+  const store = draw.value!.store;
+  const img = data.image?.find((item) => item.key === tableCoverKey);
+  const text = data.text?.find((item) => item.key === tableCoverScaleKey);
+
+  if (img && text) {
+    const sImage = store.getTypeItems("image").find((item) => item.key === tableCoverKey);
+    const sText = store
+      .getTypeItems("text")
+      .find((item) => item.key === tableCoverScaleKey);
+
+    draw.value!.history.onceTrack(() => {
+      if (sImage) {
+        store.setItem("image", {
+          id: sImage.id,
+          value: { ...sImage, ...img, id: sImage.id },
+        });
+      } else {
+        store.addItem("image", img);
+      }
+
+      if (sText) {
+        store.setItem("text", {
+          id: text.id,
+          value: { ...sText, ...text, id: sText.id },
+        });
+      } else {
+        store.addItem("text", text);
+      }
+    });
+  }
+};
+
 const inited = ref(false);
 const init = async (draw: Draw) => {
   const quitMerges: Array<() => void> = [];
@@ -86,8 +130,9 @@ const init = async (draw: Draw) => {
   draw.config.showComponentSize = false;
 
   const config: any = tabulationData.value.store.config || {};
+  const data = tabulationData.value;
   const p = tabulationData.value.paperKey;
-  await setMap(p, 0, tabulationData.value.store);
+  await setMap({ url: data.mapUrl!, size: { width: data.width!, height: data.high! } });
   draw.store.setStore({
     ...tabulationData.value.store,
     config: {

+ 22 - 0
src/example/fuse/views/tabulation/slide.vue

@@ -12,6 +12,7 @@ import {
   test,
   PaperKey,
   draw as drawMenuRaw,
+  selectMap,
 } from "../../../components/slide/actions.ts";
 import { useDraw } from "../../../components/container/use-draw.ts";
 import { computed, h, nextTick, reactive, watch } from "vue";
@@ -22,6 +23,11 @@ import { copy } from "@/utils/shared.ts";
 import { v4 as uuid } from "uuid";
 import SlideIcons from "@/example/components/slide/slide-icons.vue";
 import { iconGroups } from "@/example/constant.ts";
+import { Size } from "@/utils/math.ts";
+
+const emit = defineEmits<{
+  (e: "updateMapImage", v: { url: string; size: Size }): void;
+}>();
 
 const draw = useDraw();
 const paper = reactive({
@@ -37,6 +43,22 @@ const drawMenu = copy(drawMenuRaw);
 drawMenu.children!.shift();
 
 const menus = reactive([
+  {
+    value: uuid(),
+    icon: "",
+    name: "底图",
+    children: [
+      {
+        value: uuid(),
+        icon: "",
+        name: "高德地图",
+        handler: async () => {
+          const result = await selectMap();
+          emit("updateMapImage", { url: result.url, size: result.info.size });
+        },
+      },
+    ],
+  },
   paper,
   drawMenu,
   {