init project

This commit is contained in:
qiushijie 2025-07-18 11:39:25 +08:00
commit 85e53bcfb1
447 changed files with 21438 additions and 0 deletions

18
.editorconfig Normal file
View File

@ -0,0 +1,18 @@
# EditorConfig is awesome: http://EditorConfig.org
# https://github.com/jokeyrhyme/standard-editorconfig
# top-most EditorConfig file
root = true
# defaults
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_size = 2
indent_style = space
[*.md]
trim_trailing_whitespace = false

View File

@ -0,0 +1,27 @@
if (process.env.VITE_APP_VERSION === undefined) {
const now = new Date;
process.env.VITE_APP_VERSION = `${now.getUTCFullYear() - 2000}.${now.getUTCMonth() + 1}.${now.getUTCDate()}-${now.getUTCHours() * 60 + now.getUTCMinutes()}`;
}
/**
* @type {import('electron-builder').Configuration}
* @see https://www.electron.build/configuration/configuration
*/
const config = {
directories: {
output: 'dist',
buildResources: 'buildResources',
},
files: [
'packages/**/dist/**',
],
extraMetadata: {
version: process.env.VITE_APP_VERSION,
},
nsis: {
oneClick: false,
allowToChangeInstallationDirectory: true
}
};
module.exports = config;

View File

@ -0,0 +1,4 @@
{
"chrome": "96",
"node": "16"
}

0
.env.development Normal file
View File

43
.eslintrc.json Normal file
View File

@ -0,0 +1,43 @@
{
"root": true,
"env": {
"es2021": true,
"node": true,
"browser": false
},
"extends": [
"eslint:recommended"
],
"parserOptions": {
"sourceType": "module"
},
"ignorePatterns": [
"node_modules/**",
"**/dist/**"
],
"rules": {
/**
* Having a semicolon helps the optimizer interpret your code correctly.
* This avoids rare errors in optimized code.
* @see https://twitter.com/alex_kozack/status/1364210394328408066
*/
"semi": [
"error",
"always"
],
/**
* This will make the history of changes in the hit a little cleaner
*/
"comma-dangle": [
"warn",
"always-multiline"
],
/**
* Just for beauty
*/
"quotes": [
"warn", "single"
]
}
}

3
.gitattributes vendored Normal file
View File

@ -0,0 +1,3 @@
.github/actions/**/*.js linguist-detectable=false
scripts/*.js linguist-detectable=false
*.config.js linguist-detectable=false

56
.gitignore vendored Normal file
View File

@ -0,0 +1,56 @@
node_modules
.DS_Store
dist
*.local
thumbs.db
.eslintcache
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
.idea/artifacts
.idea/compiler.xml
.idea/jarRepositories.xml
.idea/modules.xml
.idea/*.iml
.idea/modules
*.iml
*.ipr
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# Editor-based Rest Client
.idea/httpRequests
/.idea/csv-plugin.xml

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Alex Kozack
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

110
README.md Normal file
View File

@ -0,0 +1,110 @@
[![Required Node.JS >= v16.13](https://img.shields.io/static/v1?label=node&message=%3E=16.13&logo=node.js&color)](https://nodejs.org/about/releases/)
[![Required npm >= v8.1](https://img.shields.io/static/v1?label=npm&message=%3E=8.1&logo=npm&color)](https://github.com/npm/cli/releases)
# map-download
> 基于electron和maptalks实现高德地图、百度地图包括百度自定义地图 百度个性化地图午夜蓝、清新蓝、黑夜等等链接已经失效、腾讯地图、OpenStreetMap、CartoDb、ArcGIS在线地图、天地图、MapBox的下载
> 支持卫星遥感影像和标注合并
> 支持行政区划瓦片下载,裁切边界
> 支持下载瓦片格式jpeg、png、webp
> 软件下载地址https://github.com/Hxy1992/MapDownload/releases
> V0.42版本win-unpacked压缩包 百度网盘链接https://pan.baidu.com/s/1M12KnC8bIvyHo3ik3hxy9A 提取码9986
![image](https://user-images.githubusercontent.com/14800641/154039927-e8994f36-523b-40cb-b184-46a7d8e1a9f2.png)
## Build Setup
``` bash
# 安装依赖(依赖较大,使用国内镜像)
npm install
# 热更新服务
npm run dev / npm run watch
# 构建web
npm run build
# 构建应用
npm run compile
```
---
## 下载瓦片加载方式
### Cesium
```javascript
// 非百度地图
viewer.imageryLayers.addImageryProvider(new Cesium.UrlTemplateImageryProvider({
url: 'http://localhost:7099/{z}/{x}/{y}.png'
}))
// 百度地图需自定义BaiduImageryProvider可参考cesium-helper目录下代码
import BaiduImageryProvider from './cesium-helper/BaiduImageryProvider/BaiduImageryProvider.js'
viewer.imageryLayers.addImageryProvider(new BaiduImageryProvider({
url: 'http://localhost:7099/{z}/{x}/{y}.png'
}))
```
### openlayers
```javascript
// 非百度地图
const baseMap = new ol.layer.Tile({
source: new ol.source.XYZ({
url: 'http://localhost:7099/{z}/{x}/{y}.png',
projection: 'EPSG:3857',
}),
});
const map = new ol.Map({
layers: [baseMap],
target: 'map',
view: new ol.View({
center: ol.proj.transform([105.08052356963802, 36.04231948670001], 'EPSG:4326', 'EPSG:3857'),
zoom: 5,
}),
});
```
### maptalks
```javascript
// 非百度地图
var map = new maptalks.Map('map', {
center: [105.08052356963802, 36.04231948670001],
zoom: 5,
minZoom:1,
maxZoom:19,
baseLayer: new maptalks.TileLayer('base', {
'urlTemplate' : 'http://localhost:7099/{z}/{x}/{y}.png'
})
});
//百度地图
var map = new maptalks.Map('map', {
center: [105.08052356963802, 36.04231948670001],
zoom: 5,
minZoom:1,
maxZoom:19,
spatialReference:{
projection : 'baidu',
},
baseLayer: new maptalks.TileLayer('base', {
'urlTemplate' : 'http://localhost:7099/{z}/{x}/{y}.png'
})
});
```
# TODO
+ 自定义图层加载、下载支持上传geojson作为下载范围
+ 瓦片拼接大图
如果该项目对你有帮助麻烦给个star
声明:本软件仅供个人学习与科研使用,所下载的数据版权归各个地图服务商所有,任何组织或个人因数据使用不当造成的问题,软件作者不负责任。

0
buildResources/.gitkeep Normal file
View File

BIN
buildResources/icon.icns Normal file

Binary file not shown.

BIN
buildResources/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

11286
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

59
package.json Normal file
View File

@ -0,0 +1,59 @@
{
"name": "map-download",
"description": "高德地图、百度地图包括百度自定义地图、腾讯地图、OpenStreetMap、CartoDb、ArcGIS在线地图、天地图、MapBox的下载",
"author": "Hxy1992<https://github.com/Hxy1992>",
"repository": "https://github.com/Hxy1992/MapDownload",
"version": "v1.0.0",
"private": true,
"engines": {
"node": ">=v16.13",
"npm": ">=8.1"
},
"main": "packages/main/dist/index.cjs",
"scripts": {
"build": "node --max_old_space_size=8192 scripts/build.js",
"precompile": "cross-env MODE=production npm run build",
"compile": "electron-builder build --config .electron-builder.config.js --dir --config.asar=false",
"pretest": "npm run build",
"test": "node tests/app.spec.js",
"watch": "node scripts/watch.js",
"lint": "eslint . --ext js,ts,vue",
"dev": "npm run watch"
},
"browserslist": [
"Chrome 96"
],
"lint-staged": {
"*.{js,ts,vue}": "eslint --cache --fix"
},
"devDependencies": {
"@vicons/ionicons5": "^0.12.0",
"@vitejs/plugin-vue": "1.10.0",
"cross-env": "7.0.3",
"electron": "16.0.1",
"electron-builder": "22.14.5",
"electron-devtools-installer": "3.2.0",
"eslint": "8.3.0",
"eslint-plugin-vue": "8.1.1",
"naive-ui": "^2.28.0",
"playwright": "1.16.3",
"sass": "^1.45.0",
"vfonts": "^0.0.3",
"vite": "2.6.14"
},
"dependencies": {
"@turf/boolean-contains": "^6.5.0",
"@turf/boolean-crosses": "^6.5.0",
"@turf/boolean-disjoint": "^6.5.0",
"@turf/helpers": "^6.5.0",
"@turf/intersect": "^6.5.0",
"electron-updater": "4.6.2",
"fs-extra": "^10.0.0",
"maptalks": "^1.0.0-rc.33",
"marked": "^4.0.8",
"sharp": "^0.29.3",
"superagent": "^6.1.0",
"vue": "3.2.22",
"vue-router": "4.0.12"
}
}

20
packages/main/index.js Normal file
View File

@ -0,0 +1,20 @@
const { app, BrowserWindow, ipcMain } = require('electron');
const path = require('path');
const fs = require('fs');
function createWindow() {
// Your window creation code
}
app.whenReady().then(() => {
const mainWindow = createWindow();
// Add IPC listeners here
ipcMain.on('get-path-join', (event, ...args) => {
event.returnValue = path.join(...args);
});
ipcMain.handle('fs-exists', (_, filePath) => fs.existsSync(filePath));
ipcMain.handle('fs-read', (_, filePath) => fs.readFileSync(filePath, 'utf-8'));
ipcMain.handle('fs-write', (_, filePath, content) => fs.writeFileSync(filePath, content));
});

150
packages/main/src/index.js Normal file
View File

@ -0,0 +1,150 @@
import {app, BrowserWindow, shell} from 'electron';
import {join} from 'path';
import {URL} from 'url';
import { ipcHandle } from './ipcMain';
const isSingleInstance = app.requestSingleInstanceLock();
const isDevelopment = import.meta.env.MODE === 'development';
if (!isSingleInstance) {
app.quit();
process.exit(0);
}
app.disableHardwareAcceleration();
// Install "Vue.js devtools"
if (isDevelopment) {
app.whenReady()
.then(() => import('electron-devtools-installer'))
.then(({default: installExtension, VUEJS3_DEVTOOLS}) => installExtension(VUEJS3_DEVTOOLS, {
loadExtensionOptions: {
allowFileAccess: true,
},
}))
.catch(e => console.error('Failed install extension:', e));
}
let mainWindow = null;
const createWindow = async () => {
mainWindow = new BrowserWindow({
show: false, // Use 'ready-to-show' event to show window
webPreferences: {
nativeWindowOpen: true,
// nodeIntegration: true,
width: 800,
height: 600,
preload: join(__dirname, '../../preload/dist/index.cjs'),
},
});
/**
* If you install `show: true` then it can cause issues when trying to close the window.
* Use `show: false` and listener events `ready-to-show` to fix these issues.
*
* @see https://github.com/electron/electron/issues/25012
*/
mainWindow.on('ready-to-show', () => {
mainWindow?.show();
if (isDevelopment) {
mainWindow?.webContents.openDevTools();
}
});
/**
* URL for main window.
* Vite dev server for development.
* `file://../renderer/index.html` for production and test
*/
const pageUrl = isDevelopment && import.meta.env.VITE_DEV_SERVER_URL !== undefined
? import.meta.env.VITE_DEV_SERVER_URL
: new URL('../renderer/dist/index.html', 'file://' + __dirname).toString();
await mainWindow.loadURL(pageUrl, {
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
});
ipcHandle(mainWindow);
};
app.on('web-contents-created', (_event, contents) => {
/**
* Block navigation to origins not on the allowlist.
*
* Navigation is a common attack vector. If an attacker can convince the app to navigate away
* from its current page, they can possibly force the app to open web sites on the Internet.
*
* @see https://www.electronjs.org/docs/latest/tutorial/security#13-disable-or-limit-navigation
*/
contents.on('will-navigate', (event, url) => {
const allowedOrigins =
new Set(); // Do not use insecure protocols like HTTP. https://www.electronjs.org/docs/latest/tutorial/security#1-only-load-secure-content
const { origin, hostname } = new URL(url);
const isDevLocalhost = isDevelopment && hostname === 'localhost'; // permit live reload of index.html
if (!allowedOrigins.has(origin) && !isDevLocalhost){
console.warn('Blocked navigating to an unallowed origin:', origin);
event.preventDefault();
}
});
/**
* Hyperlinks to allowed sites open in the default browser.
*
* The creation of new `webContents` is a common attack vector. Attackers attempt to convince the app to create new windows,
* frames, or other renderer processes with more privileges than they had before; or with pages opened that they couldn't open before.
* You should deny any unexpected window creation.
*
* @see https://www.electronjs.org/docs/latest/tutorial/security#14-disable-or-limit-creation-of-new-windows
* @see https://www.electronjs.org/docs/latest/tutorial/security#15-do-not-use-openexternal-with-untrusted-content
*/
contents.setWindowOpenHandler(({ url }) => {
const allowedOrigins =
new Set([ // Do not use insecure protocols like HTTP. https://www.electronjs.org/docs/latest/tutorial/security#1-only-load-secure-content
'https://vitejs.dev',
'https://github.com',
'https://v3.vuejs.org']);
const { origin } = new URL(url);
if (allowedOrigins.has(origin)){
shell.openExternal(url);
} else {
console.warn('Blocked the opening of an unallowed origin:', origin);
}
return { action: 'deny' };
});
});
app.on('second-instance', () => {
// Someone tried to run a second instance, we should focus our window.
if (mainWindow) {
if (mainWindow.isMinimized()) mainWindow.restore();
mainWindow.focus();
}
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
app.whenReady()
.then(createWindow)
.catch((e) => console.error('Failed create window:', e));
// Auto-updates
if (import.meta.env.PROD) {
app.whenReady()
.then(() => import('electron-updater'))
.then(({autoUpdater}) => autoUpdater.checkForUpdatesAndNotify())
.catch((e) => console.error('Failed check updates:', e));
}

View File

@ -0,0 +1,10 @@
/**
* 设置请求头
*/
export function getHeader() {
return {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
// 'X-Forwarded-For': returnIp(),
};
}

View File

@ -0,0 +1,127 @@
// 在主进程中.
const { ipcMain } = require('electron');
const { dialog } = require('electron');
const fse = require('fs-extra');
const fs = require('fs');
const sharp = require('sharp');
const request = require('superagent');
const path = require('path');
import { getHeader } from './ipHandle';
ipcMain.handle('show-dialog', async () => {
const result = await dialog.showOpenDialog({ properties: ['openFile', 'openDirectory'] });
return result;
});
// 确保目录存在,不存在则创建
ipcMain.on('ensure-dir', (event, args) => {
fse.ensureDirSync(args);
});
// 下载事件
export function ipcHandle(win) {
// superagent & sharp 下载图片
ipcMain.on('save-image', (event, args) => {
// sharp(base64Data).composite 反过来试试
const savePath = path.normalize(args.savePath);
const sharpStream = sharp({
failOnError: false,
});
const promises = [];
if (args.imageBuffer) {
const base64Data = args.imageBuffer.replace(/^data:image\/\w+;base64,/, '');
const dataBuffer = Buffer.from(base64Data, 'base64');
promises.push(
sharpStream
.composite([{ input: dataBuffer, gravity: 'centre', blend: 'dest-in' }])
.toFile(savePath),
);
} else {
promises.push(
sharpStream
// .ensureAlpha()
.toFile(savePath),
);
}
request.get(args.url).set(getHeader()).pipe(sharpStream);
Promise.all(promises)
.then(() => {
win.webContents.send('imageDownloadDone', {
state: 'completed',
});
})
.catch((err) => {
console.error('错误', err);
try {
fs.unlinkSync(savePath);
} catch {
// do nothing
}
win.webContents.send('imageDownloadDone', {
state: 'error',
});
});
});
// superagent & sharp 下载、合并图片
ipcMain.on('save-image-merge', (event, args) => {
try {
const savePath = path.normalize(args.savePath);
let imgBack;
const imgBuffer = [];
args.layers.forEach(async (item, index) => {
const sharpStream = sharp({
failOnError: false,
});
request.get(item.url).set(getHeader()).pipe(sharpStream);
const bff = await sharpStream.toBuffer();
if (item.isLabel) {
imgBack = bff;
} else {
imgBuffer.push(bff);
}
// 结束保存
if (index === args.layers.length - 1) {
let opration;
if (args.imageBuffer) {
const base64Data = args.imageBuffer.replace(/^data:image\/\w+;base64,/, '');
const dataBuffer = Buffer.from(base64Data, 'base64');
sharp(imgBack)
.composite(imgBuffer.map(input => {
return { input, gravity: 'centre', blend: 'saturate' };
}))
.composite([{ input: dataBuffer, gravity: 'centre', blend: 'dest-in' }]);
} else {
opration = sharp(imgBack)
.composite(imgBuffer.map(input => {
return { input, gravity: 'centre', blend: 'saturate' };
}));
}
opration
.toFile(savePath)
.then(() => {
win.webContents.send('imageDownloadDone', {
state: 'completed',
});
})
.catch((err) => {
console.error('错误', err);
try {
fs.unlinkSync(savePath);
} catch (e) {
// do nothing
}
});
}
});
} catch {
win.webContents.send('imageDownloadDone', {
state: 'error',
});
}
});
}

View File

@ -0,0 +1,42 @@
import {node} from '../../.electron-vendors.cache.json';
import {join} from 'path';
import {builtinModules} from 'module';
const PACKAGE_ROOT = __dirname;
const config = {
mode: process.env.MODE,
root: PACKAGE_ROOT,
envDir: process.cwd(),
resolve: {
alias: {
'/@/': join(PACKAGE_ROOT, 'src') + '/',
},
},
build: {
sourcemap: 'inline',
target: `node${node}`,
outDir: 'dist',
assetsDir: '.',
minify: process.env.MODE !== 'development',
lib: {
entry: 'src/index.js',
formats: ['cjs'],
},
rollupOptions: {
external: [
'electron',
'electron-devtools-installer',
...builtinModules,
],
output: {
entryFileNames: '[name].cjs',
},
},
emptyOutDir: true,
brotliSize: false,
},
};
export default config;

View File

@ -0,0 +1,26 @@
import {contextBridge} from 'electron';
const { ipcRenderer } = require('electron');
const apiKey = 'electron';
let imageDownloadhandle;
ipcRenderer.on('imageDownloadDone', (event, state) => {
imageDownloadhandle && imageDownloadhandle(state);
});
/**
* @see https://github.com/electron/electron/issues/21437#issuecomment-573522360
*/
const api = {
versions: process.versions,
ipcRenderer: { ...ipcRenderer },
imageDownloadDone: (callback) => {
imageDownloadhandle = callback;
},
};
/**
* The "Main World" is the JavaScript context that your main renderer code runs in.
* By default, the page you load in your renderer executes code in this world.
*
* @see https://www.electronjs.org/docs/api/context-bridge
*/
contextBridge.exposeInMainWorld(apiKey, api);

View File

@ -0,0 +1,40 @@
import {chrome} from '../../.electron-vendors.cache.json';
import {join} from 'path';
import {builtinModules} from 'module';
const PACKAGE_ROOT = __dirname;
const config = {
mode: process.env.MODE,
root: PACKAGE_ROOT,
envDir: process.cwd(),
resolve: {
alias: {
'/@/': join(PACKAGE_ROOT, 'src') + '/',
},
},
build: {
sourcemap: 'inline',
target: `chrome${chrome}`,
outDir: 'dist',
assetsDir: '.',
minify: process.env.MODE !== 'development',
lib: {
entry: 'src/index.js',
formats: ['cjs'],
},
rollupOptions: {
external: [
'electron',
...builtinModules,
],
output: {
entryFileNames: '[name].cjs',
},
},
emptyOutDir: true,
brotliSize: false,
},
};
export default config;

View File

@ -0,0 +1,10 @@
{
"env": {
"browser": true,
"node": false
},
"extends": [
/** @see https://eslint.vuejs.org/rules/ */
"plugin:vue/vue3-recommended"
]
}

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<!-- 内容安全策略https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy -->
<meta http-equiv="Content-Security-Policy" content="script-src 'self' blob: 'unsafe-eval' 'unsafe-inline' 'unsafe-hashes'">
<title>MapDownload</title>
</head>
<body>
<div id="app"></div>
<script src="./src/index.js" type="module"></script>
</body>
</html>

View File

@ -0,0 +1,19 @@
<template>
<n-notification-provider>
<n-message-provider>
<router-view />
</n-message-provider>
</n-notification-provider>
</template>
<script>
import {defineComponent} from 'vue';
export default defineComponent({
name: 'App',
setup() {
},
});
</script>
<style>
</style>

View File

@ -0,0 +1,2 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1650436523116" class="icon" viewBox="0 0 1027 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2577" xmlns:xlink="http://www.w3.org/1999/xlink" width="200.5859375" height="200"><defs><style type="text/css">@font-face { font-family: feedback-iconfont; src: url("//at.alicdn.com/t/font_1031158_u69w8yhxdu.woff2?t=1630033759944") format("woff2"), url("//at.alicdn.com/t/font_1031158_u69w8yhxdu.woff?t=1630033759944") format("woff"), url("//at.alicdn.com/t/font_1031158_u69w8yhxdu.ttf?t=1630033759944") format("truetype"); }
</style></defs><path d="M977.6 555.2c25.6 0 43.2-17.6 43.2-43.2s-17.6-43.2-43.2-43.2h-84.8v-256h84.8c25.6 0 43.2-17.6 43.2-43.2S1003.2 128 977.6 128h-84.8V43.2c0-25.6-17.6-43.2-43.2-43.2s-43.2 17.6-43.2 43.2V128h-256V43.2c1.6-25.6-16-43.2-41.6-43.2s-43.2 17.6-43.2 43.2V128h-256V43.2C209.6 17.6 192 0 166.4 0S128 17.6 128 43.2V128H43.2C17.6 128 0 145.6 0 171.2s17.6 43.2 43.2 43.2H128v256H43.2C17.6 468.8 0 486.4 0 512s17.6 43.2 43.2 43.2H128v256H43.2c-25.6 0-43.2 17.6-43.2 43.2C0 878.4 17.6 896 43.2 896H128v84.8c0 25.6 17.6 43.2 43.2 43.2s43.2-17.6 43.2-43.2V896h256v84.8c0 25.6 17.6 43.2 43.2 43.2s43.2-17.6 43.2-43.2V896h256v84.8c0 25.6 17.6 43.2 43.2 43.2s43.2-17.6 43.2-43.2V896h84.8c25.6 0 43.2-17.6 43.2-43.2s-17.6-43.2-43.2-43.2H896v-256h84.8v1.6zM808 212.8v256h-256v-256h256z m-595.2 0h256v256h-256v-256z m0 598.4v-256h256v256h-256z m595.2 0h-256v-256h256v256z" p-id="2578"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,15 @@
<svg width="410" height="404" viewBox="0 0 410 404" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M399.641 59.5246L215.643 388.545C211.844 395.338 202.084 395.378 198.228 388.618L10.5817 59.5563C6.38087 52.1896 12.6802 43.2665 21.0281 44.7586L205.223 77.6824C206.398 77.8924 207.601 77.8904 208.776 77.6763L389.119 44.8058C397.439 43.2894 403.768 52.1434 399.641 59.5246Z" fill="url(#paint0_linear)"/>
<path d="M292.965 1.5744L156.801 28.2552C154.563 28.6937 152.906 30.5903 152.771 32.8664L144.395 174.33C144.198 177.662 147.258 180.248 150.51 179.498L188.42 170.749C191.967 169.931 195.172 173.055 194.443 176.622L183.18 231.775C182.422 235.487 185.907 238.661 189.532 237.56L212.947 230.446C216.577 229.344 220.065 232.527 219.297 236.242L201.398 322.875C200.278 328.294 207.486 331.249 210.492 326.603L212.5 323.5L323.454 102.072C325.312 98.3645 322.108 94.137 318.036 94.9228L279.014 102.454C275.347 103.161 272.227 99.746 273.262 96.1583L298.731 7.86689C299.767 4.27314 296.636 0.855181 292.965 1.5744Z" fill="url(#paint1_linear)"/>
<defs>
<linearGradient id="paint0_linear" x1="6.00017" y1="32.9999" x2="235" y2="344" gradientUnits="userSpaceOnUse">
<stop stop-color="#41D1FF"/>
<stop offset="1" stop-color="#BD34FE"/>
</linearGradient>
<linearGradient id="paint1_linear" x1="194.651" y1="8.81818" x2="236.076" y2="292.989" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFEA83"/>
<stop offset="0.0833333" stop-color="#FFDD35"/>
<stop offset="1" stop-color="#FFA800"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,175 @@
import * as Cesium from 'cesium';
import ImageryType from './ImageryType';
import BaiduMercatorTilingScheme from './BaiduMercatorTilingScheme';
const IMG_URL =
'https://shangetu{s}.map.bdimg.com/it/u=x={x};y={y};z={z};v=009;type=sate&fm=46';
const VEC_URL =
'https://online{s}.map.bdimg.com/tile/?qt=tile&x={x}&y={y}&z={z}&styles=sl&v=020';
const CUSTOM_URL =
'https://api{s}.map.bdimg.com/customimage/tile?&x={x}&y={y}&z={z}&scale=1&customid=midnight';
const TRAFFIC_URL =
'https://its.map.baidu.com:8002/traffic/TrafficTileService?time={time}&label={labelStyle}&v=016&level={z}&x={x}&y={y}&scaler=2';
class BaiduImageryProvider {
constructor(options = {}) {
this._url =
options.style === 'img'
? IMG_URL
: options.style === 'vec'
? VEC_URL
: options.style === 'traffic'
? TRAFFIC_URL
: CUSTOM_URL;
const baiduLocal = options.url;
if (baiduLocal) {
this._url = `${baiduLocal}/{z}/{x}/{y}.png`;
}
this._labelStyle = options.labelStyle || 'web2D';
this._tileWidth = 256;
this._tileHeight = 256;
this._maximumLevel = 18;
this._crs = options.crs || 'BD09';
if (options.crs === 'WGS84') {
const resolutions = [];
for (let i = 0; i < 19; i++) {
resolutions[i] = 256 * Math.pow(2, 18 - i);
}
this._tilingScheme = new BaiduMercatorTilingScheme({
resolutions,
rectangleSouthwestInMeters: new Cesium.Cartesian2(
-20037726.37,
-12474104.17,
),
rectangleNortheastInMeters: new Cesium.Cartesian2(
20037726.37,
12474104.17,
),
});
} else {
this._tilingScheme = new Cesium.WebMercatorTilingScheme({
rectangleSouthwestInMeters: new Cesium.Cartesian2(-33554054, -33746824),
rectangleNortheastInMeters: new Cesium.Cartesian2(33554054, 33746824),
});
}
this._rectangle = this._tilingScheme.rectangle;
this._credit = undefined;
this._token = undefined;
this._style = options.style || 'normal';
}
get url() {
return this._url;
}
get token() {
return this._token;
}
get tileWidth() {
if (!this.ready) {
throw new Cesium.DeveloperError(
'tileWidth must not be called before the imagery provider is ready.',
);
}
return this._tileWidth;
}
get tileHeight() {
if (!this.ready) {
throw new Cesium.DeveloperError(
'tileHeight must not be called before the imagery provider is ready.',
);
}
return this._tileHeight;
}
get maximumLevel() {
if (!this.ready) {
throw new Cesium.DeveloperError(
'maximumLevel must not be called before the imagery provider is ready.',
);
}
return this._maximumLevel;
}
get minimumLevel() {
if (!this.ready) {
throw new Cesium.DeveloperError(
'minimumLevel must not be called before the imagery provider is ready.',
);
}
return 0;
}
get tilingScheme() {
if (!this.ready) {
throw new Cesium.DeveloperError(
'tilingScheme must not be called before the imagery provider is ready.',
);
}
return this._tilingScheme;
}
get rectangle() {
if (!this.ready) {
throw new Cesium.DeveloperError(
'rectangle must not be called before the imagery provider is ready.',
);
}
return this._rectangle;
}
get ready() {
return !!this._url;
}
get credit() {
return this._credit;
}
get hasAlphaChannel() {
return true;
}
// eslint-disable-next-line
getTileCredits(x, y, level) {}
/**
* Request Image
* @param x
* @param y
* @param level
* @returns {Promise<HTMLImageElement | HTMLCanvasElement>}
*/
requestImage(x, y, level) {
if (!this.ready) {
throw new Cesium.DeveloperError(
'requestImage must not be called before the imagery provider is ready.',
);
}
const xTiles = this._tilingScheme.getNumberOfXTilesAtLevel(level);
const yTiles = this._tilingScheme.getNumberOfYTilesAtLevel(level);
let url = this._url
.replace('{z}', level)
.replace('{s}', String(1))
.replace('{style}', this._style)
.replace('{labelStyle}', this._labelStyle)
.replace('{time}', String(new Date().getTime()));
if (this._crs === 'WGS84') {
url = url.replace('{x}', String(x)).replace('{y}', String(-y));
} else {
url = url
.replace('{x}', String(x - xTiles / 2))
.replace('{y}', String(yTiles / 2 - y - 1));
}
return Cesium.ImageryProvider.loadImage(this, url);
}
}
ImageryType.BAIDU = 'baidu';
export default BaiduImageryProvider;

View File

@ -0,0 +1,494 @@
const EARTH_RADIUS = 6370996.81;
const MC_BAND = [12890594.86, 8362377.87, 5591021, 3481989.83, 1678043.12, 0];
const LL_BAND = [75, 60, 45, 30, 15, 0];
const MC2LL = [
[
1.410526172116255e-8,
8.98305509648872e-6,
-1.9939833816331,
2.009824383106796e2,
-1.872403703815547e2,
91.6087516669843,
-23.38765649603339,
2.57121317296198,
-0.03801003308653,
1.73379812e7,
],
[
-7.435856389565537e-9,
8.983055097726239e-6,
-0.78625201886289,
96.32687599759846,
-1.85204757529826,
-59.36935905485877,
47.40033549296737,
-16.50741931063887,
2.28786674699375,
1.026014486e7,
],
[
-3.030883460898826e-8,
8.98305509983578e-6,
0.30071316287616,
59.74293618442277,
7.357984074871,
-25.38371002664745,
13.45380521110908,
-3.29883767235584,
0.32710905363475,
6.85681737e6,
],
[
-1.981981304930552e-8,
8.983055099779535e-6,
0.03278182852591,
40.31678527705744,
0.65659298677277,
-4.44255534477492,
0.85341911805263,
0.12923347998204,
-0.04625736007561,
4.48277706e6,
],
[
3.09191371068437e-9,
8.983055096812155e-6,
0.00006995724062,
23.10934304144901,
-0.00023663490511,
-0.6321817810242,
-0.00663494467273,
0.03430082397953,
-0.00466043876332,
2.5551644e6,
],
[
2.890871144776878e-9,
8.983055095805407e-6,
-0.00000003068298,
7.47137025468032,
-0.00000353937994,
-0.02145144861037,
-0.00001234426596,
0.00010322952773,
-0.00000323890364,
8.260885e5,
],
];
const LL2MC = [
[
-0.0015702102444,
1.113207020616939e5,
1.704480524535203e15,
-1.033898737604234e16,
2.611266785660388e16,
-3.51496691766537e16,
2.659570071840392e16,
-1.072501245418824e16,
1.800819912950474e15,
82.5,
],
[
8.277824516172526e-4,
1.113207020463578e5,
6.477955746671608e8,
-4.082003173641316e9,
1.077490566351142e10,
-1.517187553151559e10,
1.205306533862167e10,
-5.124939663577472e9,
9.133119359512032e8,
67.5,
],
[
0.00337398766765,
1.113207020202162e5,
4.481351045890365e6,
-2.339375119931662e7,
7.968221547186455e7,
-1.159649932797253e8,
9.723671115602145e7,
-4.366194633752821e7,
8.477230501135234e6,
52.5,
],
[
0.00220636496208,
1.113207020209128e5,
5.175186112841131e4,
3.796837749470245e6,
9.920137397791013e5,
-1.22195221711287e6,
1.340652697009075e6,
-6.209436990984312e5,
1.444169293806241e5,
37.5,
],
[
-3.441963504368392e-4,
1.113207020576856e5,
2.782353980772752e2,
2.485758690035394e6,
6.070750963243378e3,
5.482118345352118e4,
9.540606633304236e3,
-2.71055326746645e3,
1.405483844121726e3,
22.5,
],
[
-3.218135878613132e-4,
1.113207020701615e5,
0.00369383431289,
8.237256402795718e5,
0.46104986909093,
2.351343141331292e3,
1.58060784298199,
8.77738589078284,
0.37238884252424,
7.45,
],
];
class BaiduMercatorProjection {
constructor() {
this.isWgs84 = false;
}
/**
*
* @param point1
* @param point2
* @returns {number}
*/
getDistanceByMC(point1, point2) {
if (!point1 || !point2) {
return 0;
}
point1 = this.convertMC2LL(point1);
if (!point1) {
return 0;
}
const x1 = this.toRadians(point1['lng']);
const y1 = this.toRadians(point1['lat']);
point2 = this.convertMC2LL(point2);
if (!point2) {
return 0;
}
const x2 = this.toRadians(point2['lng']);
const y2 = this.toRadians(point2['lat']);
return this.getDistance(x1, x2, y1, y2);
}
/**
* Calculate the distance between two points according to the latitude and longitude coordinates
* @param point1
* @param point2
* @returns {number|*}
*/
getDistanceByLL(point1, point2) {
if (!point1 || !point2) {
return 0;
}
point1['lng'] = this.getLoop(point1['lng'], -180, 180);
point1['lat'] = this.getRange(point1['lat'], -74, 74);
point2['lng'] = this.getLoop(point2['lng'], -180, 180);
point2['lat'] = this.getRange(point2['lat'], -74, 74);
const x1 = this.toRadians(point1['lng']);
const y1 = this.toRadians(point1['lat']);
const x2 = this.toRadians(point2['lng']);
const y2 = this.toRadians(point2['lat']);
return this.getDistance(x1, x2, y1, y2);
}
/**
* The plane cartesian coordinates are converted to latitude and longitude coordinates
* @param point
* @returns {Point|{lng: number, lat: number}}
*/
convertMC2LL(point) {
if (!point) {
return { lng: 0, lat: 0 };
}
let lnglat = {};
if (this.isWgs84) {
lnglat.lng = (point.lng / 20037508.34) * 180;
const mmy = (point.lat / 20037508.34) * 180;
lnglat.lat =
(180 / Math.PI) *
(2 * Math.atan(Math.exp((mmy * Math.PI) / 180)) - Math.PI / 2);
return {
lng: lnglat['lng'].toFixed(6),
lat: lnglat['lat'].toFixed(6),
};
}
const temp = {
lng: Math.abs(point['lng']),
lat: Math.abs(point['lat']),
};
let factor;
for (let i = 0; i < MC_BAND.length; i++) {
if (temp['lat'] >= MC_BAND[i]) {
factor = MC2LL[i];
break;
}
}
lnglat = this.convertor(point, factor);
return {
lng: lnglat['lng'].toFixed(6),
lat: lnglat['lat'].toFixed(6),
};
}
/**
* The latitude and longitude coordinates are converted to plane cartesian coordinates
* @param point
* @returns {{lng: number, lat: number}|*}
*/
convertLL2MC(point) {
if (!point) {
return { lng: 0, lat: 0 };
}
if (
point['lng'] > 180 ||
point['lng'] < -180 ||
point['lat'] > 90 ||
point['lat'] < -90
) {
return point;
}
if (this.isWgs84) {
const mercator = {};
const earthRad = 6378137.0;
mercator.lng = ((point.lng * Math.PI) / 180) * earthRad;
const a = (point.lat * Math.PI) / 180;
mercator.lat =
(earthRad / 2) * Math.log((1.0 + Math.sin(a)) / (1.0 - Math.sin(a)));
return {
lng: parseFloat(mercator['lng'].toFixed(2)),
lat: parseFloat(mercator['lat'].toFixed(2)),
};
}
point['lng'] = this.getLoop(point['lng'], -180, 180);
point['lat'] = this.getRange(point['lat'], -74, 74);
const temp = { lng: point['lng'], lat: point['lat'] };
let factor;
for (let i = 0; i < LL_BAND.length; i++) {
if (temp['lat'] >= LL_BAND[i]) {
factor = LL2MC[i];
break;
}
}
if (!factor) {
for (let i = 0; i < LL_BAND.length; i++) {
if (temp['lat'] <= -LL_BAND[i]) {
factor = LL2MC[i];
break;
}
}
}
const mc = this.convertor(point, factor);
return {
lng: parseFloat(mc['lng'].toFixed(2)),
lat: parseFloat(mc['lat'].toFixed(2)),
};
}
/**
*
* @param fromPoint
* @param factor
* @returns {{lng: *, lat: *}}
*/
convertor(fromPoint, factor) {
if (!fromPoint || !factor) {
return { lng: 0, lat: 0 };
}
let x = factor[0] + factor[1] * Math.abs(fromPoint['lng']);
const temp = Math.abs(fromPoint['lat']) / factor[9];
let y =
factor[2] +
factor[3] * temp +
factor[4] * temp * temp +
factor[5] * temp * temp * temp +
factor[6] * temp * temp * temp * temp +
factor[7] * temp * temp * temp * temp * temp +
factor[8] * temp * temp * temp * temp * temp * temp;
x *= fromPoint['lng'] < 0 ? -1 : 1;
y *= fromPoint['lat'] < 0 ? -1 : 1;
return {
lng: x,
lat: y,
};
}
/**
*
* @param x1
* @param x2
* @param y1
* @param y2
* @returns {number}
*/
getDistance(x1, x2, y1, y2) {
return (
EARTH_RADIUS *
Math.acos(
Math.sin(y1) * Math.sin(y2) +
Math.cos(y1) * Math.cos(y2) * Math.cos(x2 - x1),
)
);
}
/**
*
* @param deg
* @returns {number}
*/
toRadians(deg) {
return (Math.PI * deg) / 180;
}
/**
*
* @param rad
* @returns {number}
*/
toDegrees(rad) {
return (180 * rad) / Math.PI;
}
/**
*
* @param v
* @param a
* @param b
* @returns {number}
*/
getRange(v, a, b) {
if (a != null) {
v = Math.max(v, a);
}
if (b != null) {
v = Math.min(v, b);
}
return v;
}
/**
*
* @param v
* @param a
* @param b
* @returns {*}
*/
getLoop(v, a, b) {
while (v > b) {
v -= b - a;
}
while (v < a) {
v += b - a;
}
return v;
}
/**
*
* @param point
* @returns {{lng: number, lat: number}|*}
*/
lngLatToMercator(point) {
return this.convertLL2MC(point);
}
/**
*
* @param point
* @returns {{x: (number|*), y: (number|*)}}
*/
lngLatToPoint(point) {
const mercator = this.convertLL2MC(point);
return {
x: mercator['lng'],
y: mercator['lat'],
};
}
/**
* WebMercator transforms to latitude and longitude
* @param point
* @returns {Point|{lng: number, lat: number}}
*/
mercatorToLngLat(point) {
return this.convertMC2LL(point);
}
/**
*
* @param point
* @returns {Point|{lng: number, lat: number}}
*/
pointToLngLat(point) {
const mercator = { lng: point.x, lat: point.y };
return this.convertMC2LL(mercator);
}
/**
* Latitude and longitude coordinates transforms to pixel coordinates
* @param point
* @param zoom
* @param mapCenter
* @param mapSize
* @returns {{x: number, y: number}}
*/
pointToPixel(point, zoom, mapCenter, mapSize) {
if (!point) {
return { x: 0, y: 0 };
}
point = this.lngLatToMercator(point);
const zoomUnits = this.getZoomUnits(zoom);
const x = Math.round(
(point['lng'] - mapCenter['lng']) / zoomUnits + mapSize.width / 2,
);
const y = Math.round(
(mapCenter['lat'] - point['lat']) / zoomUnits + mapSize.height / 2,
);
return { x, y };
}
/**
* Pixel coordinates transforms to latitude and longitude coordinates
* @param pixel
* @param zoom
* @param mapCenter
* @param mapSize
* @returns {Point|{lng: number, lat: number}}
*/
pixelToPoint(pixel, zoom, mapCenter, mapSize) {
if (!pixel) {
return { lng: 0, lat: 0 };
}
const zoomUnits = this.getZoomUnits(zoom);
const lng = mapCenter['lng'] + zoomUnits * (pixel.x - mapSize.width / 2);
const lat = mapCenter['lat'] - zoomUnits * (pixel.y - mapSize.height / 2);
const point = { lng, lat };
return this.mercatorToLngLat(point);
}
/**
*
* @param zoom
* @returns {number}
*/
getZoomUnits(zoom) {
return Math.pow(2, 18 - zoom);
}
}
export default BaiduMercatorProjection;

View File

@ -0,0 +1,98 @@
import * as Cesium from 'cesium';
import CoordTransform from './CoordTransform';
import BaiduMercatorProjection from './BaiduMercatorProjection';
class BaiduMercatorTilingScheme extends Cesium.WebMercatorTilingScheme {
constructor(options) {
super(options);
const projection = new BaiduMercatorProjection();
this._projection.project = function(cartographic, result) {
result = result || {};
result = CoordTransform.WGS84ToGCJ02(
Cesium.Math.toDegrees(cartographic.longitude),
Cesium.Math.toDegrees(cartographic.latitude),
);
result = CoordTransform.GCJ02ToBD09(result[0], result[1]);
result[0] = Math.min(result[0], 180);
result[0] = Math.max(result[0], -180);
result[1] = Math.min(result[1], 74.000022);
result[1] = Math.max(result[1], -71.988531);
result = projection.lngLatToPoint({
lng: result[0],
lat: result[1],
});
return new Cesium.Cartesian2(result.x, result.y);
};
this._projection.unproject = function(cartesian, result) {
result = result || {};
result = projection.mercatorToLngLat({
lng: cartesian.x,
lat: cartesian.y,
});
result = CoordTransform.BD09ToGCJ02(result.lng, result.lat);
result = CoordTransform.GCJ02ToWGS84(result[0], result[1]);
return new Cesium.Cartographic(
Cesium.Math.toRadians(result[0]),
Cesium.Math.toRadians(result[1]),
);
};
this.resolutions = options.resolutions || [];
}
/**
*
* @param x
* @param y
* @param level
* @param result
* @returns {module:cesium.Rectangle|*}
*/
tileXYToNativeRectangle(x, y, level, result) {
const tileWidth = this.resolutions[level];
const west = x * tileWidth;
const east = (x + 1) * tileWidth;
const north = ((y = -y) + 1) * tileWidth;
const south = y * tileWidth;
if (!Cesium.defined(result)) {
return new Cesium.Rectangle(west, south, east, north);
}
result.west = west;
result.south = south;
result.east = east;
result.north = north;
return result;
}
/**
*
* @param position
* @param level
* @param result
* @returns {undefined|*}
*/
positionToTileXY(position, level, result) {
const rectangle = this._rectangle;
if (!Cesium.Rectangle.contains(rectangle, position)) {
return undefined;
}
const projection = this._projection;
const webMercatorPosition = projection.project(position);
if (!Cesium.defined(webMercatorPosition)) {
return undefined;
}
const tileWidth = this.resolutions[level];
const xTileCoordinate = Math.floor(webMercatorPosition.x / tileWidth);
const yTileCoordinate = -Math.floor(webMercatorPosition.y / tileWidth);
if (!Cesium.defined(result)) {
return new Cesium.Cartesian2(xTileCoordinate, yTileCoordinate);
}
result.x = xTileCoordinate;
result.y = yTileCoordinate;
return result;
}
}
export default BaiduMercatorTilingScheme;

View File

@ -0,0 +1,172 @@
const BD_FACTOR = (3.14159265358979324 * 3000.0) / 180.0
const PI = 3.1415926535897932384626
const RADIUS = 6378245.0
const EE = 0.00669342162296594323
class CoordTransform {
/**
* BD-09 To GCJ-02
* @param lng
* @param lat
* @returns {number[]}
*/
static BD09ToGCJ02(lng, lat) {
const x = +lng - 0.0065
const y = +lat - 0.006
const z = Math.sqrt(x * x + y * y) - 0.00002 * Math.sin(y * BD_FACTOR)
const theta = Math.atan2(y, x) - 0.000003 * Math.cos(x * BD_FACTOR)
const gg_lng = z * Math.cos(theta)
const gg_lat = z * Math.sin(theta)
return [gg_lng, gg_lat]
}
/**
* GCJ-02 To BD-09
* @param lng
* @param lat
* @returns {number[]}
* @constructor
*/
static GCJ02ToBD09(lng, lat) {
lat = +lat
lng = +lng
const z =
Math.sqrt(lng * lng + lat * lat) + 0.00002 * Math.sin(lat * BD_FACTOR)
const theta = Math.atan2(lat, lng) + 0.000003 * Math.cos(lng * BD_FACTOR)
const bd_lng = z * Math.cos(theta) + 0.0065
const bd_lat = z * Math.sin(theta) + 0.006
return [bd_lng, bd_lat]
}
/**
* WGS-84 To GCJ-02
* @param lng
* @param lat
* @returns {number[]}
*/
static WGS84ToGCJ02(lng, lat) {
lat = +lat
lng = +lng
if (this.out_of_china(lng, lat)) {
return [lng, lat]
} else {
const d = this.delta(lng, lat)
return [lng + d[0], lat + d[1]]
}
}
/**
* GCJ-02 To WGS-84
* @param lng
* @param lat
* @returns {number[]}
* @constructor
*/
static GCJ02ToWGS84(lng, lat) {
lat = +lat
lng = +lng
if (this.out_of_china(lng, lat)) {
return [lng, lat]
} else {
const d = this.delta(lng, lat)
const mgLng = lng + d[0]
const mgLat = lat + d[1]
return [lng * 2 - mgLng, lat * 2 - mgLat]
}
}
/**
*
* @param lng
* @param lat
* @returns {number[]}
*/
static delta(lng, lat) {
let dLng = this.transformLng(lng - 105, lat - 35)
let dLat = this.transformLat(lng - 105, lat - 35)
const radLat = (lat / 180) * PI
let magic = Math.sin(radLat)
magic = 1 - EE * magic * magic
const sqrtMagic = Math.sqrt(magic)
dLng = (dLng * 180) / ((RADIUS / sqrtMagic) * Math.cos(radLat) * PI)
dLat = (dLat * 180) / (((RADIUS * (1 - EE)) / (magic * sqrtMagic)) * PI)
return [dLng, dLat]
}
/**
*
* @param lng
* @param lat
* @returns {number}
*/
static transformLng(lng, lat) {
lat = +lat
lng = +lng
let ret =
300.0 +
lng +
2.0 * lat +
0.1 * lng * lng +
0.1 * lng * lat +
0.1 * Math.sqrt(Math.abs(lng))
ret +=
((20.0 * Math.sin(6.0 * lng * PI) + 20.0 * Math.sin(2.0 * lng * PI)) *
2.0) /
3.0
ret +=
((20.0 * Math.sin(lng * PI) + 40.0 * Math.sin((lng / 3.0) * PI)) * 2.0) /
3.0
ret +=
((150.0 * Math.sin((lng / 12.0) * PI) +
300.0 * Math.sin((lng / 30.0) * PI)) *
2.0) /
3.0
return ret
}
/**
*
* @param lng
* @param lat
* @returns {number}
*/
static transformLat(lng, lat) {
lat = +lat
lng = +lng
let ret =
-100.0 +
2.0 * lng +
3.0 * lat +
0.2 * lat * lat +
0.1 * lng * lat +
0.2 * Math.sqrt(Math.abs(lng))
ret +=
((20.0 * Math.sin(6.0 * lng * PI) + 20.0 * Math.sin(2.0 * lng * PI)) *
2.0) /
3.0
ret +=
((20.0 * Math.sin(lat * PI) + 40.0 * Math.sin((lat / 3.0) * PI)) * 2.0) /
3.0
ret +=
((160.0 * Math.sin((lat / 12.0) * PI) +
320 * Math.sin((lat * PI) / 30.0)) *
2.0) /
3.0
return ret
}
/**
*
* @param lng
* @param lat
* @returns {boolean}
*/
static out_of_china(lng, lat) {
lat = +lat
lng = +lng
return !(lng > 73.66 && lng < 135.05 && lat > 3.86 && lat < 53.55)
}
}
export default CoordTransform

View File

@ -0,0 +1,11 @@
const ImageryType = {
ARCGIS: 'arcgis',
SINGLE_TILE: 'single_tile',
WMS: 'wms',
WMTS: 'wmts',
XYZ: 'xyz',
COORD: 'coord'
}
export default ImageryType

View File

@ -0,0 +1,36 @@
<template>
<h2 id="versions">
Lib versions
</h2>
<div>
<ul aria-labelledby="versions">
<li
v-for="(version, lib) in versions"
:key="lib"
>
<strong>{{ lib }}</strong>: v{{ version }}
</li>
</ul>
</div>
</template>
<script >
import {defineComponent} from 'vue';
import {useElectron} from '/@/use/electron';
export default defineComponent({
name: 'App',
setup() {
const {versions} = useElectron();
// It makes no sense to make "versions" reactive
return {versions};
},
});
</script>
<style scoped>
div {
text-align: left;
display: grid;
justify-content: center;
}
</style>

View File

@ -0,0 +1,74 @@
<template>
<n-popover
ref="popover"
trigger="click"
width="200"
>
<template #trigger>
<span
style="cursor: pointer;color: #2080f0;"
title="选择下载区域"
@click="handleIconClick"
>
{{ chooseArea || '选择下载区域' }}
</span>
</template>
<n-tree
block-line
:data="layers"
:key-field="'areaCode'"
:label-field="'areaName'"
:default-expanded-keys="[-1]"
:on-update:selected-keys="handleSelect"
selectable
virtual-scroll
style="height: 320px"
/>
</n-popover>
</template>
<script>
import {defineComponent, ref} from 'vue';
import {getAreaList} from '/@/utils/areaList.js';
export default defineComponent({
name: 'AreaChoose',
components: {
},
props: {
},
emits: ['choose'],
setup(prop, {emit}) {
const layerList = getAreaList();
const chooseArea = ref('');
const popover = ref();
return {
chooseArea: chooseArea,
popover: popover,
layers: layerList,
icon: {
normal: '#333333',
active: '#2080f0',
},
handleSelect (keys, options) {
const option = options[0];
chooseArea.value = option.areaName;
popover.value.setShow(false);
option.fetchLoad().then(geojson => {
emit('choose', {option, geojson});
});
},
};
},
data() {
return {
};
},
computed: {
},
mounted() {
},
methods: {
},
});
</script>

View File

@ -0,0 +1,44 @@
<template>
<n-icon
size="20"
title="显示瓦片网格"
style="cursor: pointer;"
:color="gridColor"
@click="handleGridClick"
>
<svg
t="1650436523116"
class="icon"
viewBox="0 0 1027 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="2577"
width="200"
height="200"
><path
d="M977.6 555.2c25.6 0 43.2-17.6 43.2-43.2s-17.6-43.2-43.2-43.2h-84.8v-256h84.8c25.6 0 43.2-17.6 43.2-43.2S1003.2 128 977.6 128h-84.8V43.2c0-25.6-17.6-43.2-43.2-43.2s-43.2 17.6-43.2 43.2V128h-256V43.2c1.6-25.6-16-43.2-41.6-43.2s-43.2 17.6-43.2 43.2V128h-256V43.2C209.6 17.6 192 0 166.4 0S128 17.6 128 43.2V128H43.2C17.6 128 0 145.6 0 171.2s17.6 43.2 43.2 43.2H128v256H43.2C17.6 468.8 0 486.4 0 512s17.6 43.2 43.2 43.2H128v256H43.2c-25.6 0-43.2 17.6-43.2 43.2C0 878.4 17.6 896 43.2 896H128v84.8c0 25.6 17.6 43.2 43.2 43.2s43.2-17.6 43.2-43.2V896h256v84.8c0 25.6 17.6 43.2 43.2 43.2s43.2-17.6 43.2-43.2V896h256v84.8c0 25.6 17.6 43.2 43.2 43.2s43.2-17.6 43.2-43.2V896h84.8c25.6 0 43.2-17.6 43.2-43.2s-17.6-43.2-43.2-43.2H896v-256h84.8v1.6zM808 212.8v256h-256v-256h256z m-595.2 0h256v256h-256v-256z m0 598.4v-256h256v256h-256z m595.2 0h-256v-256h256v256z"
p-id="2578"
/></svg>
</n-icon>
</template>
<script >
import {defineComponent} from 'vue';
export default defineComponent({
name: 'GridIcon',
emits: ['showGrid'],
data() {
return {
showGrid: false,
gridColor: '#333333',
};
},
methods: {
handleGridClick() {
this.showGrid = !this.showGrid;
this.gridColor = this.showGrid ? '#2080f0' : '#333333';
this.$emit('showGrid', this.showGrid);
},
},
});
</script>

View File

@ -0,0 +1,205 @@
<template>
<div
v-if="visible"
class="box-modal"
>
<div class="dialog">
<div class="header">
<span class="title">帮助</span>
<n-icon
class="close"
size="20"
@click="ok"
>
<Close />
</n-icon>
</div>
<div class="content">
<div>
<b>MapDownload</b> - 基于electron和maptalks实现高德地图百度地图包括百度自定义地图腾讯地图OpenStreetMapCartoDbArcGIS在线地图的下载
</div>
<div>
<b>地址</b> - https://github.com/Hxy1992/MapDownload
</div>
<div
ref="code"
class="code"
/>
</div>
<!-- <div class="footer">
<button
@click="ok"
>
确定
</button>
</div> -->
</div>
</div>
</template>
<script>
import {defineComponent} from 'vue';
import { marked } from 'marked';
import { Close } from '@vicons/ionicons5';
export default defineComponent({
name: 'HelpDialog',
components: {
Close,
},
props: {
visible: {
required: true,
type: Boolean,
},
},
setup() {
},
data() {
return {
};
},
watch: {
visible(val) {
if (val) this.$nextTick(() => {
this.updateCode();
});
},
},
mounted() {
},
methods: {
updateCode() {
const code = `
## 下载瓦片加载方式
### Cesium
\`\`\`javascript
//
viewer.imageryLayers.addImageryProvider(new Cesium.UrlTemplateImageryProvider({
url: 'http://localhost:7099/{z}/{x}/{y}.png'
}))
// BaiduImageryProvidercesium-helper
import BaiduImageryProvider from './cesium-helper/BaiduImageryProvider/BaiduImageryProvider.js'
viewer.imageryLayers.addImageryProvider(new BaiduImageryProvider({
url: 'http://localhost:7099/{z}/{x}/{y}.png'
}))
\`\`\`
### openlayers
\`\`\`javascript
//
const baseMap = new ol.layer.Tile({
source: new ol.source.XYZ({
url: 'http://localhost:7099/{z}/{x}/{y}.png',
projection: 'EPSG:3857',
}),
});
const map = new ol.Map({
layers: [baseMap],
target: 'map',
view: new ol.View({
center: ol.proj.transform([105.08052356963802, 36.04231948670001], 'EPSG:4326', 'EPSG:3857'),
zoom: 5,
}),
});
\`\`\`
### maptalks
\`\`\`javascript
//
var map = new maptalks.Map('map', {
center: [105.08052356963802, 36.04231948670001],
zoom: 5,
minZoom:1,
maxZoom:19,
baseLayer: new maptalks.TileLayer('base', {
'urlTemplate' : 'http://localhost:7099/{z}/{x}/{y}.png'
})
});
//
var map = new maptalks.Map('map', {
center: [105.08052356963802, 36.04231948670001],
zoom: 5,
minZoom:1,
maxZoom:19,
spatialReference:{
projection : 'baidu',
},
baseLayer: new maptalks.TileLayer('base', {
'urlTemplate' : 'http://localhost:7099/{z}/{x}/{y}.png'
})
});
\`\`\`
`;
this.$refs.code.innerHTML = marked.parse(code);
},
ok() {
// eslint-disable-next-line
this.$emit('ok');
},
},
});
</script>
<style lang="scss" scoped>
.box-modal{
position: fixed;
left: 0px;
top: 0;
width: 100%;
height: 100%;
z-index: 100;
background-color: rgba(0, 0, 0, 0.3);
display: flex;
justify-content: center;
align-items: center;
.dialog{
width: 850px;
padding: 8px;
background-color: white;
box-shadow: 0px 2px 4px 0px rgb(54 58 80 / 30%);
border-radius: 3px;
.header{
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
padding: 5px 8px;
.title{
color: #303133;
font-weight: bold;
}
.close{
cursor: pointer;
}
}
.content{
width: 100%;
padding: 8px 16px;
.code{
height: 400px;
overflow: auto;
background: #aaa9af;
}
}
.footer{
width: 100%;
display: flex;
justify-content: center;
align-items: center;
padding: 5px 8px;
button{
cursor: pointer;
}
}
}
}
</style>

View File

@ -0,0 +1,307 @@
<template>
<n-spin
class="loadingSpin"
:show="mapLoading"
>
<div id="map" />
</n-spin>
<div class="box-controls">
<layer-control
@choose="chooseLayers"
/>
<div class="splitline" />
<n-icon
size="20"
title="绘制矩形"
style="cursor: pointer;"
:color="isDrawing ? '#2080f0' : '#333333'"
@click="drawRect"
>
<SquareOutline />
</n-icon>
<div class="splitline" />
<n-icon
size="20"
title="下载地图"
style="cursor: pointer;"
@click="showSave"
>
<CloudDownloadOutline />
</n-icon>
<div class="splitline" />
<GridIcon @show-grid="showGrid" />
<div class="splitline" />
<n-icon
size="20"
title="设置"
style="cursor: pointer;"
@click="showSet(true)"
>
<SettingsOutline />
</n-icon>
<div class="splitline" />
<n-icon
size="20"
title="帮助"
style="cursor: pointer;"
@click="showHelp(true)"
>
<HelpCircleOutline />
</n-icon>
<div class="splitline" />
<n-icon
size="20"
title="上传GeoJSON"
style="cursor: pointer;"
@click="uploadGeoJSON()"
>
<CloudUploadOutline />
</n-icon>
<div class="splitline" />
<area-choose
@choose="chooseArea"
/>
<save-diablog
:visible="saveVisible"
:download-extent="downloadExtent"
:base-layer="saveLayers"
:limit-max-zoom="limitMaxZoom"
:limit-min-zoom="limitMinZoom"
:is-baidu="isBaidu"
@ok="save"
@cancel="cancelSave"
/>
<help-diablog
:visible="helpVisible"
@ok="showHelp(false)"
/>
<map-key
:visible="setVisible"
@hide="showSet(false)"
/>
</div>
<ProgressControl />
<tips />
</template>
<script>
import {defineComponent} from 'vue';
import baseMap from '../utils/baseMap.js';
import {setMapLoading,getMapLoading} from '../utils/baseMap.js';
import LayerControl from './LayerControl.vue';
import AreaChoose from './AreaChoose.vue';
import SaveDiablog from './Save.vue';
import FileSave from '../utils/fileSave.js';
import HelpDiablog from './Help.vue';
import MapKey from './MapKey.vue';
import Tips from './Tips.vue';
import {useMessage, useNotification} from 'naive-ui';
import GridIcon from './GridIcon.vue';
import ProgressControl from './ProgressControl.vue';
import { CloudDownloadOutline, HelpCircleOutline, SettingsOutline, SquareOutline,CloudUploadOutline } from '@vicons/ionicons5';
// eslint-disable-next-line
let map
export default defineComponent({
name: 'HomeMain',
components: {
LayerControl,
SaveDiablog,
HelpDiablog,
MapKey,
Tips,
AreaChoose,
GridIcon,
ProgressControl,
CloudDownloadOutline,
HelpCircleOutline,
SettingsOutline,
SquareOutline,
CloudUploadOutline,
},
setup() {
window.$message = useMessage();
window.$notification = useNotification();
const mapLoading = getMapLoading();
return {
mapLoading: mapLoading,
};
},
data() {
return {
isDrawing: false,
saveVisible: false,
downloadExtent: {},
helpVisible: false,
setVisible: false,
saveLayers: [],
limitMinZoom: 1,
limitMaxZoom: 18,
isBaidu: false,
};
},
computed: {
},
mounted() {
map = new baseMap('map');
this.addMapRightClickHandle();
},
methods: {
chooseLayers(data) {
this._currentLayer = data;
map.switchBaseLayer(data);
},
drawRect() {
this.isDrawing = !this.isDrawing;
if (this.isDrawing) {
this.hideDrawTips();
this._drawStartInfo = window.$notification.create({
content: '已开启矩形绘制,右键下载瓦片',
duration: 10000,
});
map.startDraw();
} else {
map.endDraw();
}
},
hideDrawTips() {
if (this._drawStartInfo) {
this._drawStartInfo.destroy();
this._drawStartInfo = null;
}
},
//
addMapRightClickHandle() {
map.getMap().addEventListener('contextmenu', () => {
if (!this.isDrawing) return;
if (!this.showSave(false)) {
setTimeout(() => {
this.isDrawing = false;
map.endDraw();
this.hideDrawTips();
}, 50);
return;
}
});
},
showSave(showMsg = true) {
this.downloadExtent = map.getDownloadExtent();
if (!this.downloadExtent) {
if (showMsg) window.$message.warning('获取下载范围错误,请重新绘制下载范围');
return false;
}
const {tileLayer,maxZoom,minZoom,projection} = map.getBaseMapConfig();
this.saveLayers = tileLayer;
this.limitMaxZoom = maxZoom;
this.limitMinZoom = minZoom;
this.isBaidu = projection.code === 'BAIDU';
this.saveVisible = true;
map.fitExtent();
setMapLoading(true);
return true;
},
save(val) {
this.saveVisible = false;
this.$nextTick(() => {
const mapConfig = map.getBaseMapConfig();
val.mapConfig = mapConfig;
if (val.clipImage) {
val.downloadGeometry = map.getDownloadGeometry();
}
new FileSave(val);
});
},
cancelSave() {
setMapLoading(false);
this.saveVisible = false;
},
showHelp(val) {
this.helpVisible = val;
},
showSet(val) {
this.setVisible = val;
if (!val && this._currentLayer && (this._currentLayer.parent === 'Tdt' || this._currentLayer.parent === 'Mapbox')) {
map.switchBaseLayer(this._currentLayer);
}
},
chooseArea(data) {
//
this.isDrawing = false;
map.endDraw();
this.hideDrawTips();
//
const {geojson} = data;
// console.log(option);
map.addGeometry(geojson, true, () => {
this.showSave();
});
map.fitExtent();
},
uploadGeoJSON() {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = '.geojson';
fileInput.click();
fileInput.onchange = () => {
const file = fileInput.files[0];
if (!file) return;
const reader = new FileReader();
reader.readAsText(file);
reader.onload = () => {
const geojson = JSON.parse(reader.result);
console.log(geojson)
this.chooseArea({geojson});
};
}
},
showGrid(val) {
map.showTileGrid(val);
},
},
});
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style lang="scss" scoped>
.loadingSpin{
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
}
#map{
position: absolute;
margin: 0;
padding: 0;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.box-controls{
position: absolute;
left: 70px;
top: 10px;
background-color: white;
box-shadow: 0px 2px 4px 0px rgb(54 58 80 / 30%);
// width: 200px;
padding: 8px;
display: flex;
// .items{
// width: 20px;
// height: 20px;
// background-size: contain;
// background-repeat: no-repeat;
// background-position: center center;
// cursor: pointer;
// }
.splitline{
width: 1px;
height: 20px;
margin: 0 8px;
background-color: #999999;
}
}
</style>

View File

@ -0,0 +1,81 @@
<template>
<n-dropdown
:options="layers"
placement="bottom-start"
trigger="click"
:key-field="'uuid'"
:on-clickoutside="onClickoutside"
@select="handleSelect"
>
<n-icon
class="sourceLayer"
size="20"
title="切换地图源"
style="cursor: pointer;"
:color="layerColor"
@click="handleIconClick"
>
<Layers />
</n-icon>
</n-dropdown>
</template>
<script>
import {defineComponent, ref} from 'vue';
import {getMapList} from '/@/utils/layerList.js';
import {getKeys} from '/@/utils/mapKey.js';
import { Layers } from '@vicons/ionicons5';
export default defineComponent({
name: 'LayerControl',
components: {
Layers,
},
props: {
},
emits: ['choose'],
setup(prop, {emit}) {
const showDropdownRef = ref(false);
const layerList = getMapList();
return {
layers: layerList,
layersVisible: showDropdownRef,
icon: {
normal: '#333333',
active: '#2080f0',
},
handleSelect (key, layer) {
const parent = layerList.find(item => { return item.uuid === layer.pid; });
const {mapboxKey, tdtKey} = getKeys();
if ((parent.value === 'Mapbox' && !mapboxKey) || (parent.value === 'Tdt' && !tdtKey)) {
window.$message.warning(`请设置${parent.label}地图Key`);
}
emit('choose', { parent: parent.value, layer: layer });
showDropdownRef.value = false;
},
handleIconClick (e) {
e.preventDefault();
showDropdownRef.value = true;
},
onClickoutside () {
showDropdownRef.value = false;
},
};
},
data() {
return {
};
},
computed: {
layerColor() {
return this.layersVisible ? this.icon.active : this.icon.normal;
},
},
mounted() {
},
methods: {
},
});
</script>

View File

@ -0,0 +1,116 @@
<template>
<n-modal
:show="showModal"
:show-icon="false"
:on-mask-click="cancel"
:on-esc="cancel"
:on-close="cancel"
preset="dialog"
>
<template #header>
地图Key配置
</template>
<div class="dialog-content">
<div class="item">
<span class="label">天地图</span>
<input
v-model="tdtKey"
class="value"
type="text"
>
</div>
<div class="item">
<span class="label">MapBox</span>
<input
v-model="mapboxKey"
class="value"
type="text"
>
</div>
</div>
<template #action>
<n-button @click="cancel">
取消
</n-button>
<n-button
type="info"
@click="ok"
>
确定
</n-button>
</template>
</n-modal>
</template>
<script>
import {defineComponent} from 'vue';
import {getKeys, setKeys} from '/@/utils/mapKey.js';
export default defineComponent({
name: 'MapKey',
props: {
visible: {
required: true,
type: Boolean,
},
},
setup() {
},
data() {
return {
showModal: false,
tdtKey: '',
mapboxKey: '',
};
},
watch: {
visible() {
this.showModal = this.visible;
},
},
created() {
this.showModal = this.visible;
},
mounted() {
this.getKeys();
},
methods: {
getKeys() {
const data = getKeys();
this.tdtKey = data?.tdtKey;
this.mapboxKey = data?.mapboxKey;
},
cancel() {
// eslint-disable-next-line
this.$emit('hide');
},
ok() {
setKeys({
tdtKey: this.tdtKey || '',
mapboxKey: this.mapboxKey || '',
});
this.cancel();
},
},
});
</script>
<style lang="scss" scoped>
.dialog-content{
width: 100%;
padding: 8px 16px;
.item{
margin: 3px 0;
}
.label{
display: inline-block;
width: 80px;
text-align: right;
}
.value{
display: inline-block;
width: 260px;
}
}
</style>

View File

@ -0,0 +1,74 @@
<template>
<div
ref="container"
class="box-progress"
>
<progress
ref="progress"
class="progress"
value="0"
max="100"
/>
<div class="item">
已下载:<span
ref="progressSuccess"
class="success"
/>
</div>
<div class="item">
失败:<span
ref="progressError"
class="error"
/>
</div>
<button @click="closeProgress">
关闭
</button>
</div>
</template>
<script >
import {defineComponent} from 'vue';
import { setProgressDom, showProgress } from '../utils/progress';
export default defineComponent({
name: 'ProgressControl',
data() {
return {
};
},
mounted() {
setProgressDom({
success: this.$refs['progressSuccess'],
error: this.$refs['progressError'],
progress: this.$refs['progress'],
container: this.$refs['container'],
});
},
methods: {
closeProgress() {
showProgress(false);
},
},
});
</script>
<style lang="scss" scoped>
.box-progress{
position: absolute;
right: 10px;
bottom: 10px;
background-color: white;
box-shadow: 0px 2px 4px 0px rgb(54 58 80 / 30%);
width: 200px;
padding: 8px;
z-index: 100;
display: none;
.progress{
width: 100%;
}
.item{
text-align: left;
}
}
</style>

View File

@ -0,0 +1,274 @@
<template>
<n-modal
:show="showModal"
:show-icon="false"
:on-mask-click="cancel"
:on-esc="cancel"
:on-close="cancel"
preset="dialog"
>
<template #header>
下载参数配置
</template>
<div class="dialog-content">
<n-descriptions
label-placement="left"
title="下载范围"
size="small"
:column="1"
class="descriptions"
>
<n-descriptions-item label="xmin">
{{ downloadExtent.xmin }}
</n-descriptions-item>
<n-descriptions-item label="xmax">
{{ downloadExtent.xmax }}
</n-descriptions-item>
<n-descriptions-item label="ymin">
{{ downloadExtent.ymin }}
</n-descriptions-item>
<n-descriptions-item label="ymax">
{{ downloadExtent.ymax }}
</n-descriptions-item>
</n-descriptions>
<div class="item">
<span class="label">最小层级</span>
<input
v-model="minZoom"
class="value"
type="text"
>
</div>
<div class="item">
<span class="label">最大层级</span>
<input
v-model="maxZoom"
class="value"
type="text"
>
</div>
<div
v-if="showMerge"
class="item"
>
<span class="label">合并标注</span>
<div class="value">
<input
v-model="mergeLayers"
type="checkbox"
>是否合并
</div>
<div
v-if="mergeLayers"
class="warning-merge"
>
下载大量瓦片时合并可能会造成卡死
</div>
</div>
<div class="item">
<span class="label">边界裁切</span>
<div class="value">
<input
v-model="clipImage"
type="checkbox"
>
</div>
</div>
<div class="item">
<span class="label">下载路径</span>
<div class="value">
<input
v-model="savePath"
type="text"
disabled
style="width:215px;"
>
<button @click="setFolder">
选择
</button>
</div>
</div>
<div class="item">
<span class="label">瓦片格式</span>
<div class="value">
<n-select
v-model:value="imageType"
:options="imageTypeList"
/>
</div>
</div>
</div>
<template #action>
<n-button @click="cancel">
取消
</n-button>
<n-button
type="info"
@click="ok"
>
确定
</n-button>
</template>
</n-modal>
</template>
<script>
import {defineComponent} from 'vue';
export default defineComponent({
name: 'SaveDialog',
props: {
visible: {
required: true,
type: Boolean,
},
downloadExtent: {
required: true,
type: Object,
},
baseLayer: {
required: true,
type: [Object, Array],
},
limitMinZoom: {
required: true,
type: Number,
},
limitMaxZoom: {
required: true,
type: Number,
},
isBaidu: {
required: true,
type: Boolean,
},
},
setup() {
},
data() {
return {
showModal: false,
savePath: '',
maxZoom: '8',
minZoom: '5',
mergeLayers: false,
clipImage: false,
imageType: 'png',
imageTypeList: [
{
label: 'png',
value: 'png',
},
{
label: 'jpeg',
value: 'jpeg',
},
{
label: 'webp',
value: 'webp',
},
],
};
},
computed: {
showMerge() {
return Array.isArray(this.baseLayer) && this.baseLayer.length > 1;
},
},
watch: {
visible() {
this.showModal = this.visible;
},
},
created() {
this.showModal = this.visible;
},
mounted() {
},
methods: {
reset() {
this.savePath = '';
this.maxZoom = '';
this.minZoom = '';
},
async setFolder() {
const result = await window.electron.ipcRenderer.invoke('show-dialog');
if (result.canceled) return;
this.savePath = result.filePaths[0];
},
cancel() {
this.reset();
// eslint-disable-next-line
this.$emit('cancel', {});
},
ok() {
if (!this.savePath) {
return window.$message.warning('请选择保存目录');
}
if (!this.maxZoom) {
return window.$message.warning('请输入最大层级');
}
if (!this.minZoom) {
return window.$message.warning('请输入最小层级');
}
const minZoom = parseInt(Number(this.minZoom));
const maxZoom = parseInt(Number(this.maxZoom));
if (isNaN(minZoom) || isNaN(maxZoom)) {
return window.$message.warning('层级格式错误,请输入非负整数');
}
if (minZoom > maxZoom || minZoom < this.limitMinZoom || maxZoom > this.limitMaxZoom) {
return window.$message.warning('层级格式错误');
}
if (this.isBaidu && this.clipImage) {
return window.$message.warning('百度瓦片暂不支持裁切,请取消裁切后重试');
}
const param = {
savePath: this.savePath,
minZoom: minZoom,
maxZoom: maxZoom,
mergeLayers: this.mergeLayers,
extent: this.downloadExtent,
clipImage: this.clipImage,
imageType: this.imageType,
};
// eslint-disable-next-line
this.$emit('ok', param);
},
},
});
</script>
<style lang="scss" scoped>
.dialog-content{
width: 100%;
padding: 8px 16px;
.item{
margin: 3px 0;
}
.label{
display: inline-block;
width: 80px;
text-align: right;
}
.value{
display: inline-block;
width: 260px;
}
}
.descriptions{
::v-deep .n-descriptions-header{
font-size: 14px;
margin-bottom: 3px;
}
::v-deep .n-descriptions-table-content__label{
display: inline-block;
width: 80px;
text-align: right;
}
}
.warning-merge{
width: 100%;
color: red;
padding-left: 83px;
}
</style>

View File

@ -0,0 +1,80 @@
<template>
<div
v-if="visible"
class="modal"
@click="visible = false"
>
<div class="box-tips">
<div class="title">
地图下载教程
</div>
<div class="item">
1.选择需要下载的图层天地图和Mapbox需输入Key可在设置中自行配置
</div>
<div class="item">
2.绘制需要下载的地图范围 or 选择下载区域
</div>
<div class="item">
3.点击下载按钮配置相关信息点击确定
</div>
<div class="item">
4.等待下载完成即可
</div>
<div class="opt">
<button>确定</button>
</div>
</div>
</div>
</template>
<script >
import {defineComponent} from 'vue';
export default defineComponent({
name: 'TipsInfo',
setup() {
},
data() {
return {
visible: false,
};
},
created() {
const isFirstLogin = localStorage.getItem('is-first-login');
if (!isFirstLogin) {
this.visible = true;
}
localStorage.setItem('is-first-login', 1);
},
});
</script>
<style lang="scss" scoped>
.modal{
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 999;
background-color: #0000003b;
}
.box-tips {
position: absolute;
left: 70px;
top: 55px;
width: 360px;
background-color: white;
box-shadow: 0px 2px 4px 0px rgb(54 58 80 / 30%);
padding: 8px;
.title{
font-weight: bold;
}
.item{
padding-left: 5px;
margin-top: 5px;
}
.opt{
text-align: center;
}
}
</style>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More