Quellcode durchsuchen

初始化仓库

bill vor 4 Jahren
Ursprung
Commit
b74d2bbb11
11 geänderte Dateien mit 912 neuen und 81 gelöschten Zeilen
  1. 136 4
      package-lock.json
  2. 2 3
      package.json
  3. BIN
      public/case_b.jpg
  4. 9 0
      public/index.html
  5. 15 16
      src/App.vue
  6. BIN
      src/assets/logo.png
  7. 0 58
      src/components/HelloWorld.vue
  8. 1 0
      src/main.js
  9. 291 0
      src/map/core/index.js
  10. 422 0
      src/map/index.vue
  11. 36 0
      src/map/util.js

+ 136 - 4
package-lock.json

@@ -1151,6 +1151,36 @@
         "postcss": "^7.0.0"
       }
     },
+    "@mapbox/jsonlint-lines-primitives": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz",
+      "integrity": "sha1-zlblOfg1UrWNENZy6k1vya3HsjQ="
+    },
+    "@mapbox/mapbox-gl-style-spec": {
+      "version": "13.21.0",
+      "resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-style-spec/-/mapbox-gl-style-spec-13.21.0.tgz",
+      "integrity": "sha512-qGRAZEHQfhjknjd9eCsNmKclXG5zK62DRdbgUiqAnrdkjjXrFNbs0KFaeyY8RzbdXzKRkwge6nqeKJfjYoD3vw==",
+      "requires": {
+        "@mapbox/jsonlint-lines-primitives": "~2.0.2",
+        "@mapbox/point-geometry": "^0.1.0",
+        "@mapbox/unitbezier": "^0.0.0",
+        "csscolorparser": "~1.0.2",
+        "json-stringify-pretty-compact": "^2.0.0",
+        "minimist": "^1.2.5",
+        "rw": "^1.3.3",
+        "sort-object": "^0.3.2"
+      }
+    },
+    "@mapbox/point-geometry": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz",
+      "integrity": "sha1-ioP5M1x4YO/6Lu7KJUMyqgru2PI="
+    },
+    "@mapbox/unitbezier": {
+      "version": "0.0.0",
+      "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.0.tgz",
+      "integrity": "sha1-FWUb1VOme4WB+zmIEMmK2Go0Uk4="
+    },
     "@mrmlnc/readdir-enhanced": {
       "version": "2.2.1",
       "resolved": "https://registry.npm.taobao.org/@mrmlnc/readdir-enhanced/download/@mrmlnc/readdir-enhanced-2.2.1.tgz",
@@ -1237,6 +1267,11 @@
       "integrity": "sha1-pTUV2yXYA4N0OBtzryC7Ty5QjYc=",
       "dev": true
     },
+    "@svgdotjs/svg.js": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/@svgdotjs/svg.js/-/svg.js-3.1.1.tgz",
+      "integrity": "sha512-73FggAUBS+zuHhJOMZiAsuE5qpwA4pmWUbLuvof2g3YnWEc3QhXA3tjqZlJJukBobSA23a/avf1Vb1U1QbER1Q=="
+    },
     "@types/body-parser": {
       "version": "1.19.1",
       "resolved": "https://registry.nlark.com/@types/body-parser/download/@types/body-parser-1.19.1.tgz?cache=0&sync_timestamp=1625595663636&other_urls=https%3A%2F%2Fregistry.nlark.com%2F%40types%2Fbody-parser%2Fdownload%2F%40types%2Fbody-parser-1.19.1.tgz",
@@ -4008,6 +4043,11 @@
       "integrity": "sha1-6nAm/LAXd+295SEk4h8yfnrpUOQ=",
       "dev": true
     },
+    "csscolorparser": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/csscolorparser/-/csscolorparser-1.0.3.tgz",
+      "integrity": "sha1-s085HupNqPPpgjHizNjfnAQfFxs="
+    },
     "cssesc": {
       "version": "3.0.0",
       "resolved": "https://registry.npm.taobao.org/cssesc/download/cssesc-3.0.0.tgz",
@@ -6244,8 +6284,7 @@
     "ieee754": {
       "version": "1.2.1",
       "resolved": "https://registry.npm.taobao.org/ieee754/download/ieee754-1.2.1.tgz",
-      "integrity": "sha1-jrehCmP/8l0VpXsAFYbRd9Gw01I=",
-      "dev": true
+      "integrity": "sha1-jrehCmP/8l0VpXsAFYbRd9Gw01I="
     },
     "iferr": {
       "version": "0.1.5",
@@ -6962,6 +7001,11 @@
       "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=",
       "dev": true
     },
+    "json-stringify-pretty-compact": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-2.0.0.tgz",
+      "integrity": "sha512-WRitRfs6BGq4q8gTgOy4ek7iPFXjbra0H3PmDLKm2xnZ+Gh1HUhiKGgCZkSPNULlP7mvfu6FV/mOLhCarspADQ=="
+    },
     "json-stringify-safe": {
       "version": "5.0.1",
       "resolved": "https://registry.npm.taobao.org/json-stringify-safe/download/json-stringify-safe-5.0.1.tgz",
@@ -7256,6 +7300,11 @@
         "object-visit": "^1.0.0"
       }
     },
+    "mapbox-to-css-font": {
+      "version": "2.4.0",
+      "resolved": "https://registry.npmjs.org/mapbox-to-css-font/-/mapbox-to-css-font-2.4.0.tgz",
+      "integrity": "sha512-v674D0WtpxCXlA6E+sBlG1QJWdUkz/s9qAD91bJSXBGuBL5lL4tJXpoJEftecphCh2SVQCjWMS2vhylc3AIQTg=="
+    },
     "md5.js": {
       "version": "1.3.5",
       "resolved": "https://registry.npm.taobao.org/md5.js/download/md5.js-1.3.5.tgz",
@@ -7457,8 +7506,7 @@
     "minimist": {
       "version": "1.2.5",
       "resolved": "https://registry.npm.taobao.org/minimist/download/minimist-1.2.5.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fminimist%2Fdownload%2Fminimist-1.2.5.tgz",
-      "integrity": "sha1-Z9ZgFLZqaoqqDAg8X9WN9OTpdgI=",
-      "dev": true
+      "integrity": "sha1-Z9ZgFLZqaoqqDAg8X9WN9OTpdgI="
     },
     "minipass": {
       "version": "3.1.3",
@@ -7896,6 +7944,26 @@
       "integrity": "sha1-Cb6jND1BhZ69RGKS0RydTbYZCE4=",
       "dev": true
     },
+    "ol": {
+      "version": "6.6.1",
+      "resolved": "https://registry.npmjs.org/ol/-/ol-6.6.1.tgz",
+      "integrity": "sha512-QHNth7ty7UAPi5oEL5N5rJB/cgdFGAAzigYEM8LTUKCq/StTOTWDf2fFrom3wQVmsr1etf6i6hTBowtY/LiGgg==",
+      "requires": {
+        "ol-mapbox-style": "^6.4.1",
+        "pbf": "3.2.1",
+        "rbush": "^3.0.1"
+      }
+    },
+    "ol-mapbox-style": {
+      "version": "6.4.1",
+      "resolved": "https://registry.npmjs.org/ol-mapbox-style/-/ol-mapbox-style-6.4.1.tgz",
+      "integrity": "sha512-qeHgB5lEaCjvpaR6oK8bPWqPTUAYzM2CTSfYJzujIU3egYLPCvJfVagIfTEMRDUG3CXTtIYHOI2Pg58ihhWJYA==",
+      "requires": {
+        "@mapbox/mapbox-gl-style-spec": "^13.20.1",
+        "mapbox-to-css-font": "^2.4.0",
+        "webfont-matcher": "^1.1.0"
+      }
+    },
     "on-finished": {
       "version": "2.3.0",
       "resolved": "https://registry.npm.taobao.org/on-finished/download/on-finished-2.3.0.tgz",
@@ -8226,6 +8294,15 @@
         }
       }
     },
+    "pbf": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/pbf/-/pbf-3.2.1.tgz",
+      "integrity": "sha512-ClrV7pNOn7rtmoQVF4TS1vyU0WhYRnP92fzbfF75jAIwpnzdJXf8iTd4CMEqO4yUenH6NDqLiwjqlh6QgZzgLQ==",
+      "requires": {
+        "ieee754": "^1.1.12",
+        "resolve-protobuf-schema": "^2.1.0"
+      }
+    },
     "pbkdf2": {
       "version": "3.1.2",
       "resolved": "https://registry.npm.taobao.org/pbkdf2/download/pbkdf2-3.1.2.tgz?cache=0&sync_timestamp=1617975984684&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fpbkdf2%2Fdownload%2Fpbkdf2-3.1.2.tgz",
@@ -9012,6 +9089,11 @@
       "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=",
       "dev": true
     },
+    "protocol-buffers-schema": {
+      "version": "3.5.1",
+      "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.5.1.tgz",
+      "integrity": "sha512-YVCvdhxWNDP8/nJDyXLuM+UFsuPk4+1PB7WGPVDzm3HTHbzFLxQYeW2iZpS4mmnXrQJGBzt230t/BbEb7PrQaw=="
+    },
     "proxy-addr": {
       "version": "2.0.7",
       "resolved": "https://registry.nlark.com/proxy-addr/download/proxy-addr-2.0.7.tgz",
@@ -9141,6 +9223,11 @@
       "integrity": "sha1-M0WUG0FTy50ILY7uTNogFqmu9/Y=",
       "dev": true
     },
+    "quickselect": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz",
+      "integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw=="
+    },
     "randombytes": {
       "version": "2.1.0",
       "resolved": "https://registry.npm.taobao.org/randombytes/download/randombytes-2.1.0.tgz",
@@ -9178,6 +9265,14 @@
         "unpipe": "1.0.0"
       }
     },
+    "rbush": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/rbush/-/rbush-3.0.1.tgz",
+      "integrity": "sha512-XRaVO0YecOpEuIvbhbpTrZgoiI6xBlz6hnlr6EHhd+0x9ase6EmeN+hdwwUaJvLcsFFQ8iWVF1GAK1yB0BWi0w==",
+      "requires": {
+        "quickselect": "^2.0.0"
+      }
+    },
     "read-pkg": {
       "version": "5.2.0",
       "resolved": "https://registry.npm.taobao.org/read-pkg/download/read-pkg-5.2.0.tgz?cache=0&sync_timestamp=1616914988083&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fread-pkg%2Fdownload%2Fread-pkg-5.2.0.tgz",
@@ -9489,6 +9584,14 @@
       "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=",
       "dev": true
     },
+    "resolve-protobuf-schema": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz",
+      "integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==",
+      "requires": {
+        "protocol-buffers-schema": "^3.3.1"
+      }
+    },
     "resolve-url": {
       "version": "0.2.1",
       "resolved": "https://registry.npm.taobao.org/resolve-url/download/resolve-url-0.2.1.tgz",
@@ -9563,6 +9666,11 @@
         "aproba": "^1.1.1"
       }
     },
+    "rw": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
+      "integrity": "sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q="
+    },
     "rxjs": {
       "version": "6.6.7",
       "resolved": "https://registry.nlark.com/rxjs/download/rxjs-6.6.7.tgz?cache=0&sync_timestamp=1627507111626&other_urls=https%3A%2F%2Fregistry.nlark.com%2Frxjs%2Fdownload%2Frxjs-6.6.7.tgz",
@@ -10049,6 +10157,16 @@
         }
       }
     },
+    "sort-asc": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/sort-asc/-/sort-asc-0.1.0.tgz",
+      "integrity": "sha1-q3md9h/HPqCVbHnEtTHtHp53J+k="
+    },
+    "sort-desc": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/sort-desc/-/sort-desc-0.1.1.tgz",
+      "integrity": "sha1-GYuMDN6wlcRjNBhh45JdTuNZqe4="
+    },
     "sort-keys": {
       "version": "1.1.2",
       "resolved": "https://registry.npm.taobao.org/sort-keys/download/sort-keys-1.1.2.tgz",
@@ -10066,6 +10184,15 @@
         }
       }
     },
+    "sort-object": {
+      "version": "0.3.2",
+      "resolved": "https://registry.npmjs.org/sort-object/-/sort-object-0.3.2.tgz",
+      "integrity": "sha1-mODRme3kDgfGGoRAPGHWw7KQ+eI=",
+      "requires": {
+        "sort-asc": "^0.1.0",
+        "sort-desc": "^0.1.1"
+      }
+    },
     "source-list-map": {
       "version": "2.0.1",
       "resolved": "https://registry.npm.taobao.org/source-list-map/download/source-list-map-2.0.1.tgz",
@@ -11526,6 +11653,11 @@
         "defaults": "^1.0.3"
       }
     },
+    "webfont-matcher": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/webfont-matcher/-/webfont-matcher-1.1.0.tgz",
+      "integrity": "sha1-mM6VCXsp4x++czBT4Q5XFkLRxsc="
+    },
     "webpack": {
       "version": "4.46.0",
       "resolved": "https://registry.nlark.com/webpack/download/webpack-4.46.0.tgz?cache=0&sync_timestamp=1627907845071&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fwebpack%2Fdownload%2Fwebpack-4.46.0.tgz",

+ 2 - 3
package.json

@@ -8,7 +8,9 @@
     "lint": "vue-cli-service lint"
   },
   "dependencies": {
+    "@svgdotjs/svg.js": "^3.1.1",
     "core-js": "^3.6.5",
+    "ol": "^6.6.1",
     "vue": "^3.0.0"
   },
   "devDependencies": {
@@ -29,9 +31,6 @@
       "plugin:vue/vue3-essential",
       "eslint:recommended"
     ],
-    "parserOptions": {
-      "parser": "babel-eslint"
-    },
     "rules": {}
   },
   "browserslist": [

BIN
public/case_b.jpg


+ 9 - 0
public/index.html

@@ -15,3 +15,12 @@
     <!-- built files will be auto injected -->
   </body>
 </html>
+
+<style>
+  html, body, #app {
+    width: 100%;
+    height: 100%;
+    margin: 0;
+    padding: 0;
+  }
+</style>

+ 15 - 16
src/App.vue

@@ -1,26 +1,25 @@
 <template>
-  <img alt="Vue logo" src="./assets/logo.png">
-  <HelloWorld msg="Welcome to Your Vue.js App"/>
+
+  <Map />
 </template>
 
 <script>
-import HelloWorld from './components/HelloWorld.vue'
+import Map from './map'
+
+
+const events = []
+window.appLoadedListener = (cb) => {
+  events.push(cb)
+}
 
 export default {
-  name: 'App',
   components: {
-    HelloWorld
+    Map
+  },
+  mounted() {
+    setTimeout(() => {
+      events.forEach(event => event())
+    })
   }
 }
 </script>
-
-<style>
-#app {
-  font-family: Avenir, Helvetica, Arial, sans-serif;
-  -webkit-font-smoothing: antialiased;
-  -moz-osx-font-smoothing: grayscale;
-  text-align: center;
-  color: #2c3e50;
-  margin-top: 60px;
-}
-</style>

BIN
src/assets/logo.png


+ 0 - 58
src/components/HelloWorld.vue

@@ -1,58 +0,0 @@
-<template>
-  <div class="hello">
-    <h1>{{ msg }}</h1>
-    <p>
-      For a guide and recipes on how to configure / customize this project,<br>
-      check out the
-      <a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
-    </p>
-    <h3>Installed CLI Plugins</h3>
-    <ul>
-      <li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
-      <li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank" rel="noopener">eslint</a></li>
-    </ul>
-    <h3>Essential Links</h3>
-    <ul>
-      <li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
-      <li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
-      <li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
-      <li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
-      <li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
-    </ul>
-    <h3>Ecosystem</h3>
-    <ul>
-      <li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
-      <li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
-      <li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
-      <li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
-      <li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
-    </ul>
-  </div>
-</template>
-
-<script>
-export default {
-  name: 'HelloWorld',
-  props: {
-    msg: String
-  }
-}
-</script>
-
-<!-- Add "scoped" attribute to limit CSS to this component only -->
-<style scoped>
-h3 {
-  margin: 40px 0 0;
-}
-ul {
-  list-style-type: none;
-  padding: 0;
-}
-li {
-  display: inline-block;
-  margin: 0 10px;
-}
-a {
-  color: #42b983;
-}
-</style>

+ 1 - 0
src/main.js

@@ -1,4 +1,5 @@
 import { createApp } from 'vue'
 import App from './App.vue'
 
+
 createApp(App).mount('#app')

+ 291 - 0
src/map/core/index.js

@@ -0,0 +1,291 @@
+import 'ol/ol.css'
+import Map from 'ol/Map'
+import Tile from 'ol/layer/Tile'
+import XYZ from 'ol/source/XYZ';
+import View from 'ol/View'
+import ImageCanvas from 'ol/source/ImageCanvas'
+import LayerImage from 'ol/layer/Image'
+import { fromLonLat, transform } from 'ol/proj'
+
+//高德地图切片访问路径
+const tileSorce = 'http://wprd03.is.autonavi.com/appmaptile?lang=zh_cn&size=1&style=7&x={x}&y={y}&z={z}' 
+
+// 初始地图
+export const initMap = ($map) => {
+  const tileLayer = new Tile({
+    source: new XYZ({ url: tileSorce })
+  })
+
+  const map = new Map({
+    layers: [ tileLayer ],
+    target: $map,
+    view: new View({
+        center: fromLonLat([
+          113.59569403794666,
+          22.36656052911783
+        ]), //最初定位的位置
+        zoom: 4 //地图层级
+    })
+  })
+
+  return {
+    map,
+    async loadImage(args) {
+      const { file, minWidth, minHeight } = args
+      args.img = args.img 
+        ? args.img 
+        : await blobImageLoad(file, minWidth, minHeight)
+
+      return loadImageLayer(map, args)
+    },
+    screenToLatlan({x, y}) {
+      const real = map.getCoordinateFromPixel([x, y])
+      const latlan = transform(real,'EPSG:3857','EPSG:4326')
+      return latlan
+    }
+  }
+}
+
+const loadImageLayer = (map, args) => {
+  const {
+    lon,
+    lat
+  } = args
+  const itude = fromLonLat([lon, lat])
+  const { image: imageLayer, canvas } = loadImage(map, args, itude)
+
+  map.addLayer(imageLayer);
+  map.getView().setCenter(
+    fromLonLat([lon, lat])
+  );
+  map.getView().setZoom(19)
+
+  return canvas
+}
+
+// 经纬度转canvas坐标
+const itudeToCanvasPos = (map, extent, itude) => {
+  //Canvas四至范围不同于当前地图四至范围,计算出南北方向与东西方向的偏移
+  const mapExtent = map.getView()
+    .calculateExtent(map.getSize())
+    
+  //当前底图视图范围的投影坐标
+  const canvasOrigin = map.getPixelFromCoordinate(
+    [ extent[0], extent[3] ]
+  ); 
+  
+  //添加到地图上的canvas图像的左上角
+  const mapOrigin = map.getPixelFromCoordinate(
+    [ mapExtent[0], mapExtent[3] ]
+  );
+  
+  const delta = [
+    mapOrigin[0] - canvasOrigin[0], 
+    mapOrigin[1] - canvasOrigin[1]
+  ];
+  
+  const leftTop = map.getPixelFromCoordinate(itude)
+
+  return {
+    x: leftTop[0] + delta[0], 
+    y: leftTop[1] + delta[1]
+  }
+}
+
+// 平移,旋转,放大当前canvas
+const transformCanvasCall = (
+  canvas, 
+  transform, 
+  oper,
+  center = {
+    x: 0,
+    y: 0
+  }
+) => {
+  const ctx = canvas.getContext('2d')
+  const {
+    translate,
+    scale,
+    rotate
+  } = transform
+
+  
+
+  ctx.translate(center.x, center.y)
+  translate && ctx.translate(translate.x, translate.y)
+  rotate && ctx.rotate(rotate * (Math.PI / 180))
+  scale && ctx.scale(scale[0], scale[1])
+  oper && oper()
+  // scale && ctx.scale(1 / scale, 1 / scale)
+  // rotate && ctx.rotate(-rotate * (Math.PI / 180))
+  // translate && ctx.translate(-translate.x, -translate.y)
+  ctx.translate(-center.x, -center.y)
+}
+
+const genImgCanvasItudeToReal = (map, canvas, extent) => 
+  (itude) => {
+    return genImgCanvasPosToReal(map, canvas)(
+      itudeToCanvasPos(map, extent, itude)
+    )
+  }
+
+const genImgCanvasPosToReal = (map, canvas) => 
+  (pos) => {
+    const $real = map.viewport_
+    const offsetWidth = (canvas.width - $real.offsetWidth) / 2
+    const offsetHeight = (canvas.height - $real.offsetHeight) / 2
+
+    return {
+      x: pos.x - offsetWidth,
+      y: pos.y - offsetHeight
+    }
+  }
+
+const genImgCanvasTransfrom = (canvas, arrayImgs, scale, initPos) =>
+  (transform) => {
+    const ctx = canvas.getContext('2d');
+    const dscale = transform.scale || [1, 1]
+    const resize = 1 / (scale * 10)
+    const doScale = [
+      resize * dscale[0],
+      resize * dscale[1]
+    ]
+    const imgData = { width: 0, height: 0 }
+
+    arrayImgs.forEach(imgs => {
+      let height = 0
+      imgs.forEach(([img]) => height += img.height)
+      imgData.width += imgs[0][0].width
+      if (imgData.height < height) {
+        imgData.height = height
+      }
+    })
+
+    initPos.x -= imgData.width / 2
+    initPos.y -= imgData.height / 2
+
+    // , translate: { x: -(imgData.width / 2) * doScale[0], y: -(imgData.height / 2) * doScale[1] } 
+    ctx.fillStyle = 'rgba(0,0,0,0.1)'
+    ctx.fillRect(0,0, canvas.width, canvas.height)
+    transformCanvasCall(
+      canvas, 
+      { ...transform, scale: doScale },
+      () => {
+        transform.draw && transform.draw(ctx)
+        let width = 0
+        arrayImgs.forEach(imgs => {
+          let height = 0
+          imgs.forEach(([img]) => {
+            ctx.drawImage(img, width, height)
+            height += img.height
+          })
+          width += imgs[0][0].width
+        })
+      },
+      transform.center
+    )
+    
+    const move = {
+      x: transform.translate.x - initPos.x,
+      y: transform.translate.y - initPos.y,
+    }
+    const start = {
+      x: initPos.x + move.x,
+      y: initPos.y + move.y,
+    }
+    const end = {
+      x: start.x + imgData.width * doScale[0],
+      y: start.y + imgData.height * doScale[1],
+    }
+    
+    canvas.position = [
+      start,
+      end,
+      Math.abs(start.x - end.x) / resize,
+      Math.abs(start.y - end.y) / resize
+    ]
+
+    canvas.resize = resize
+    canvas.imgData = imgData
+    canvas.imgBox = [
+      canvas.posToReal(start),
+      canvas.posToReal(end),
+      Math.abs(start.x - end.x),
+      Math.abs(start.y - end.y)
+    ]
+  }
+
+
+// 加载url
+const loadImage = (map, args, itude) => {
+
+  const canvas = document.createElement('canvas');
+  const imageCanvas = new ImageCanvas({
+    canvasFunction(extent, scale, _2, size) {
+      const pos = itudeToCanvasPos(map, extent, itude)
+      const imgData = { width: 0, height: 0 }
+      args.img.forEach(imgs => {
+        let height = 0
+        imgs.forEach(([img]) => height += img.height)
+        imgData.width += imgs[0][0].width
+        if (imgData.height < height) {
+          imgData.height = height
+        }
+      })
+      pos.x -= imgData.width / 2 * scale
+      pos.y -= imgData.height / 2 * scale
+      canvas.width = size[0];
+      canvas.height = size[1]
+      canvas.posToReal = genImgCanvasPosToReal(map, canvas);
+      canvas.transform = genImgCanvasTransfrom(canvas, args.img, scale, pos, imageCanvas);
+      canvas.itudeToReal = genImgCanvasItudeToReal(map, canvas, extent)
+      canvas.transform({
+        ...args,
+        translate: {
+          x: (args.translate ? args.translate.x : 0) + pos.x,
+          y: (args.translate ? args.translate.y : 0) + pos.y
+        }
+      })
+      
+      return canvas;
+    }
+  })
+  const image = new LayerImage({ source: imageCanvas })
+  canvas.imageLayer = imageCanvas
+  return {
+    image,
+    canvas
+  }
+}
+
+
+// 返回本地url
+const blobImageLoad = (arrayImages, minWidth, minHeight) => {
+  console.log(arrayImages)
+  const analysis = (blob) => new Promise((resolve, reject) => {
+    const url = window.URL.createObjectURL(blob);
+    const img = new Image()
+
+    img.onload = () => {
+      if (img.width < minWidth || img.height < minHeight) {
+        reject('图片宽高需要大于512')
+      } else {
+        resolve([img, url])
+      }
+    }
+    img.src = url
+  })
+
+  let arrasPromises = []
+  for (let images of arrayImages) {
+    let analys = []
+    for (let bolb of images) {
+      analys.push(analysis(bolb))
+    }
+    arrasPromises.push(
+      Promise.all(analys)
+    )
+  }
+
+  return Promise.all(arrasPromises)
+}

+ 422 - 0
src/map/index.vue

@@ -0,0 +1,422 @@
+<template>
+  <div class="map-layer"  @mousemove.stop.prevent="move"  @mouseup="upMove">
+    <input type="file" @change="imageChange" directory webkitdirectory multiple>
+    <div ref="map" class="map" v-once @mousemove="moveHandle" @mousedown="mapStartHandle"></div>
+    <div class="ctrls" :style="boxStyle" @mousedown.stop.prevent="startMove($event, 'move')"></div>
+    <div class="cctrls" v-if="box.tl">
+      <span class="tl" :style="{left: box.tl.x + 'px', top: box.tl.y + 'px'}" @mousedown.prevent.stop="startMove($event, 'scale', 'tl')"></span>
+      <span class="tc" :style="{left: box.tc.x + 'px', top: box.tc.y + 'px'}" @mousedown.prevent.stop="startMove($event, 'scale', 'tc')"></span>
+      <span class="tr" :style="{left: box.tr.x + 'px', top: box.tr.y + 'px'}" @mousedown.prevent.stop="startMove($event, 'scale', 'tr')"></span>
+      <span class="rc" :style="{left: box.rc.x + 'px', top: box.rc.y + 'px'}" @mousedown.prevent.stop="startMove($event, 'scale', 'rc')"></span>
+      <span class="lc" :style="{left: box.lc.x + 'px', top: box.lc.y + 'px'}" @mousedown.prevent.stop="startMove($event, 'scale', 'lc')"></span>
+      <span class="br" :style="{left: box.br.x + 'px', top: box.br.y + 'px'}" @mousedown.prevent.stop="startMove($event, 'scale', 'br')"></span>
+      <span class="bl" :style="{left: box.bl.x + 'px', top: box.bl.y + 'px'}" @mousedown.prevent.stop="startMove($event, 'scale', 'bl')"></span>
+      <span class="bc" :style="{left: box.bc.x + 'px', top: box.bc.y + 'px'}" @mousedown.prevent.stop="startMove($event, 'scale', 'bc')"></span>
+      <span class="cc" :style="{left: box.cc.x + 'px', top: box.cc.y + 'px'}" @mousedown.prevent.stop="startMove($event, 'rotate')"></span>
+    </div>
+    <div class="box-info" v-if="boxPos.tl">
+      <div v-for="(item, key) in boxPos" :key="key">
+        <span>{{key}}</span>
+        <span>{{item}}</span>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { initMap } from './core'
+import {
+  getCanvasToScreenPos,
+  getScreenToCanvasPos
+} from './util'
+
+
+export default {
+  data() {
+    return {
+      isHover: false,
+      box: {},
+      left: 0,
+      top: 0
+    }
+  },
+  methods: {
+    async imageChange(e) {
+      let imagesArray = []
+      if (e.target.files.length > 0) {
+        const formatError = () => {
+          e.target.value = null
+          console.log(imagesXYZ)
+          alert('目录不规范 请上传 z/x/y.png 格式目录,且在最底级目录放置图片文件')
+        }
+        let imagesXYZ = {}
+        for (let file of e.target.files) {
+          let locals = file.webkitRelativePath.split(/[\\|//]/)
+          if (locals.length < 3) return formatError()
+          let current = imagesXYZ
+          for (let i = 0; i < locals.length; i++) {
+            let dir = locals[i]
+
+            if (i !== locals.length - 1) {
+              if (!current[dir]) {
+                current[dir] = i === locals.length - 2 ? [] : {}
+              }
+              current = current[dir]
+
+              if (i === locals.length - 3) {
+                current.key = 'z'
+              }
+            }
+            
+            if (i === locals.length - 1) {
+              current.push(file)
+            }
+          }
+        }
+        
+        (function analysis (updateXYZ) {
+          if (updateXYZ.key === 'z') {
+            imagesXYZ = updateXYZ
+            return;
+          }
+          const names = Object.keys(updateXYZ).sort((a, b) => b - a)
+          names.forEach(key => {
+            if (key !== names[0]) {
+              delete updateXYZ[key]
+            }
+          })
+          analysis(updateXYZ[names[0]])
+        })(imagesXYZ);
+
+        if (!(imagesXYZ && imagesXYZ.key === 'z' && !Array.isArray(imagesXYZ))) {
+          return formatError()
+        }
+
+        for (let key in imagesXYZ) {
+          if (!Array.isArray(imagesXYZ[key]) && key !== 'key') {
+            return formatError()
+          }
+        }
+
+        delete imagesXYZ.key
+
+        Object.keys(imagesXYZ).sort((a, b) => a - b).forEach(key => {
+          imagesArray.push(
+            imagesXYZ[key].sort((a, b) => parseInt(a.name) - parseInt(b))
+          )
+        })
+      } else {
+        imagesArray = [[e.target.files[0]]]
+      }
+
+      try {
+        this.transfroms = []
+        this.args = {
+          draw: (ctx) => {
+            this.transfroms.forEach(transform => {
+              transform.forEach(({translate, scale, rotate, center}) => {
+              // 设置绘制颜色
+                center && ctx.translate(center.x, center.y)
+                translate && ctx.translate(translate.x, translate.y)
+                rotate && ctx.rotate(rotate * (Math.PI / 180))
+                scale && ctx.scale(scale[0], scale[1])
+                center && ctx.translate(-center.x, -center.y)
+                // if (center) {
+                //   ctx.fillStyle = "geend";
+                //     // 绘制成矩形
+                //   ctx.fillRect(center.x, center.y, 100, 100);
+                // }
+              })
+            })
+            
+            setTimeout(() => {
+              this.updateBox(this.imgCanvas.imgBox)
+            })
+          },
+          file: imagesArray,
+          lon: 113.59963069739054,
+          lat: 22.364821730960752,
+          translate: {x: 0, y: 0},
+          scale: [1, 1],
+          direction: 0
+        }
+        this.imgCanvas = await this.map.loadImage(this.args)
+      } catch(e) {
+        alert(e)
+      }
+    },
+    updateBox() {
+      const calcPos = pos => getCanvasToScreenPos(this.imgCanvas, pos)
+      this.box = {
+        tl: this.imgCanvas.posToReal(calcPos({x: 0, y: 0})),
+        tc: this.imgCanvas.posToReal(calcPos({x: this.imgCanvas.imgData.width / 2, y: 0})),
+        tr: this.imgCanvas.posToReal(calcPos({x: this.imgCanvas.imgData.width, y: 0})),
+        rc: this.imgCanvas.posToReal(calcPos({x: this.imgCanvas.imgData.width, y: this.imgCanvas.imgData.height / 2})),
+        lc: this.imgCanvas.posToReal(calcPos({x: 0, y: this.imgCanvas.imgData.height / 2})),
+        br: this.imgCanvas.posToReal(calcPos({x: this.imgCanvas.imgData.width, y: this.imgCanvas.imgData.height})),
+        bl: this.imgCanvas.posToReal(calcPos({x: 0, y: this.imgCanvas.imgData.height})),
+        bc: this.imgCanvas.posToReal(calcPos({x: this.imgCanvas.imgData.width / 2, y: this.imgCanvas.imgData.height})),
+        cc: this.imgCanvas.posToReal(calcPos({x: this.imgCanvas.imgData.width / 2, y: this.imgCanvas.imgData.height / 2})),
+      }
+
+      let maxX = this.box.tl.x
+      let minX = this.box.tl.x
+      let maxY = this.box.tl.y
+      let minY = this.box.tl.y
+      Object.values(this.box).forEach(({x, y}) => {
+        x > maxX && (maxX = x)
+        y > maxY && (maxY = y)
+        x < minX && (minX = x)
+        y < minY && (minY = y)
+      })
+      this.box.width = Math.abs(maxX - minX)
+      this.box.height = Math.abs(maxY - minY)
+    },
+    mapStartHandle() {
+      this.mapDown = true
+    },
+    moveHandle(e) {
+      if (!this.imgCanvas || !this.imgCanvas.imgBox) {
+        return;
+      }
+      if (this.moveing && this.oper) {
+        this.move(e)
+      } else {
+        this.mapDown && this.imgCanvas.imageLayer.refresh()
+        // const [start, end] = this.box
+        
+        // this.isHover = e.clientX > start.x && e.clientX < end.x &&
+        //   e.clientY > start.y && e.clientY < end.y
+      }
+    },
+    startMove(ev, oper, dir) {
+      this.startTransform = {
+        ...this.args
+      }
+      this.transfroms.push([])
+      this.moveing = true
+      this.oper = oper
+      this.dir = dir
+      this.startMovePos = {
+        x: ev.clientX,
+        y: ev.clientY
+      }
+    },
+    move(ev) {
+      if (!this.moveing) return;
+      const transfrom = this.transfroms[this.transfroms.length - 1]
+      const start = getScreenToCanvasPos(
+        this.imgCanvas, 
+        this.startMovePos
+      )
+      const end = getScreenToCanvasPos(
+        this.imgCanvas, 
+        { x: ev.clientX, y: ev.clientY }
+      )
+      const move = { 
+        x: end.x - start.x, 
+        y: end.y - start.y 
+      }
+      
+      
+      if (this.oper === 'move') {
+        transfrom.push({ translate: move })
+      } else if (this.oper === 'scale'){
+        const width = this.imgCanvas.position[2]
+        const height = this.imgCanvas.position[3]
+        let xScale, yScale
+
+        switch(this.dir) {
+          case 'tl':
+            xScale = (width - move.x) / width
+            yScale = (height - move.y) / height
+            if (xScale > 0.1 && yScale > 0.1) {
+              transfrom.push({ 
+                scale: [xScale, yScale],
+                center: {x: this.imgCanvas.position[2], y: this.imgCanvas.position[3]}
+              })
+            }
+            break;
+          case 'tc':
+            yScale = (height - move.y) / height
+            if (yScale > 0.1) {
+              transfrom.push({ 
+                scale: [1, yScale],
+                center: {x: 0, y: this.imgCanvas.position[3]}
+              })
+            }
+            break;
+          case 'tr':
+            xScale = (width + move.x) / width
+            yScale = (height - move.y) / height
+            if (xScale > 0.1 && yScale > 0.1) {
+              transfrom.push({ 
+                scale: [xScale, yScale],
+                center: {x: 0, y: this.imgCanvas.position[3]}
+              })
+            }
+            break;
+          case 'rc':
+            xScale = (width + move.x) / width
+            if (xScale > 0.1) {
+              transfrom.push({ 
+                scale: [xScale, 1],
+                center: {x: 0, y: this.imgCanvas.position[3]}
+              })
+            }
+            break;
+          case 'lc':
+            xScale = (width - move.x) / width
+            if (xScale > 0.1) {
+            transfrom.push({ 
+              scale: [xScale, 1],
+              center: {x: this.imgCanvas.position[2], y: this.imgCanvas.position[3]}
+            })
+            }
+            break;
+          case 'br':
+            xScale = (width + move.x) / width
+            yScale = (height + move.y) / height
+            if (xScale > 0.1 && yScale > 0.1) {
+            transfrom.push({ 
+              scale: [xScale, yScale],
+              center: {x: 0, y: 0}
+            })
+            }
+            break;
+          case 'bl':
+            xScale = (width - move.x) / width
+            yScale = (height + move.y) / height
+            if (xScale > 0.1 && yScale > 0.1) {
+            transfrom.push({ 
+              scale: [xScale, yScale],
+              center: {x: this.imgCanvas.position[2], y: 0}
+            })
+            }
+            break;
+          case 'bc':
+            yScale = (height + move.y) / height
+            if (yScale > 0.1) {
+            transfrom.push({ 
+              scale: [1, yScale],
+              center: {x: 0, y: 0}
+            })
+            }
+            break;
+        }
+      } else if (this.oper === 'rotate') {
+        let move = ev.clientX - this.startMovePos.x
+        let height = this.imgCanvas.position[3]
+        let width = this.imgCanvas.position[2]
+        let center = {x: width / 2, y: height / 2}
+
+        // let zrotate = transfrom.
+        transfrom.push({ 
+          rotate: move / 3,
+          center: center
+        })
+      }
+      this.startMovePos = {
+        x: ev.clientX,
+        y: ev.clientY
+      }
+      this.imgCanvas.imageLayer.refresh()
+    },
+    upMove() {
+      this.moveing = false
+      this.mapDown = false
+      this.oper = null
+      this.dir = null
+      this.startMovePos = null
+
+      
+    }
+  },
+  computed: {
+    boxStyle() {
+      if (this.box && Object.keys(this.box).length) {
+        const box = this.box
+        return {
+          width: box.width + 20 + 'px',
+          height: box.height + 20 + 'px',
+          left: box.cc.x + 'px',
+          top: box.cc.y + 'px'
+        }
+      } else {
+        return {}
+      }
+    },
+    boxPos() {
+      if (this.box && Object.keys(this.box).length) {
+        const ret = {}
+        for (let key in this.box) {
+          if (key !== 'width' && key !== 'height') {
+            ret[key] = this.map.screenToLatlan(this.box[key])
+          }
+        }
+        return ret
+      } else {
+        return {}
+      }
+    }
+  },
+  mounted() {
+    window.appLoadedListener(() => {
+      this.map = initMap(this.$refs.map)
+    })
+  }
+}
+</script>
+
+<style scoped>
+.map-layer,
+.map {
+  width: 100%;
+  height: 100%;
+  position: relative;
+  overflow: hidden;
+}
+
+input {
+  z-index: 99;
+  position: absolute;
+  left: 0;
+  top: 0;
+}
+
+.test {
+  position: absolute;
+  z-index: 90;
+  width: 10px;
+  height: 20px;
+  border-radius: 50%;
+  background: gold;
+}
+
+.ctrls {
+  position: absolute;
+  z-index: 9;
+  --margin: 10px;
+  transform: translate(-50%, -50%);
+}
+
+.cctrls span {
+  z-index: 10;
+  position: absolute;
+  width: 10px;
+  height: 10px;
+  border-radius: 50%;
+  background: red;
+  cursor: pointer;
+  transform: translate(-50%, -50%);
+}
+
+.box-info {
+  padding: 10px;
+  color: #fff;
+  position: absolute;
+  bottom: 0;
+  left: 0;
+  background: rgba(0, 0, 0, 0.5);
+}
+</style>

+ 36 - 0
src/map/util.js

@@ -0,0 +1,36 @@
+
+// 获取逆转矩阵
+const getCanvasInverImatrix = $canvas => {
+  const ctx = $canvas.getContext('2d')
+  const transform = ctx.getTransform()
+  return transform.invertSelf();
+}
+
+// canvas坐标转屏幕坐标
+export const getCanvasToScreenPos = ($canvas, {x, y}) => {
+  const { 
+    a, b, c, 
+    d, e, f 
+  } = getCanvasInverImatrix($canvas)
+
+  const screenX = (c * y - d * x + d * e - c * f) / (b * c - a * d)
+  const screenY = (y - screenX * b - f) / d
+
+  return {
+    x: Math.round(screenX),
+    y: Math.round(screenY),
+  }
+}
+
+// 屏幕坐标转canvas坐标
+export const getScreenToCanvasPos = ($canvas, {x, y}) => {
+  const { 
+    a, b, c, 
+    d, e, f 
+  } = getCanvasInverImatrix($canvas)
+
+  return {
+    x: Math.round(x * a + y * c + e),
+    y: Math.round(x * b + y * d + f)
+  };
+}