提交 667e8636 作者: 方治民

Initial commit

上级
{
"env": {
"node": true,
"es6": true
},
"extends": ["prettier"],
"plugins": ["prettier"]
// "rules": { "prettier/prettier": "error" }
}
version: 2
updates:
- package-ecosystem: npm
directory: "/"
schedule:
interval: daily
time: "21:00"
open-pull-requests-limit: 10
ignore:
- dependency-name: eslint-config-prettier
versions:
- 8.0.0
- 8.1.0
- 8.2.0
- dependency-name: eslint
versions:
- 7.21.0
- dependency-name: docsify
versions:
- 4.12.1
- dependency-name: tape
versions:
- 5.2.1
- dependency-name: husky
versions:
- 5.1.2
- 5.1.3
name: Node CI
on:
push:
branches:
- master
jobs:
build:
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v2
- id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v2
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
- uses: actions/setup-node@v2
with:
node-version: '12'
- run: |
yarn
yarn test
yarn dist
mkdir -p foxgis-server-lite-win foxgis-server-lite-linux foxgis-server-lite-macos
wget -q -O - https://mapbox-node-binary.s3.amazonaws.com/sqlite3/v5.0.2/napi-v3-win32-x64.tar.gz | tar zxf - --strip-components=1 -C foxgis-server-lite-win
wget -q -O - https://mapbox-node-binary.s3.amazonaws.com/sqlite3/v5.0.2/napi-v3-linux-x64.tar.gz | tar zxf - --strip-components=1 -C foxgis-server-lite-linux
wget -q -O - https://mapbox-node-binary.s3.amazonaws.com/sqlite3/v5.0.2/napi-v3-darwin-x64.tar.gz | tar zxf - --strip-components=1 -C foxgis-server-lite-macos
cp -R data dist/foxgis-server-lite-win.exe foxgis-server-lite-win/
cp -R data dist/foxgis-server-lite-linux foxgis-server-lite-linux/
cp -R data dist/foxgis-server-lite-macos foxgis-server-lite-macos/
tar zcf foxgis-server-lite-win.tar.gz foxgis-server-lite-win
tar zcf foxgis-server-lite-linux.tar.gz foxgis-server-lite-linux
tar zcf foxgis-server-lite-macos.tar.gz foxgis-server-lite-macos
mv foxgis-server-lite-win.tar.gz foxgis-server-lite-linux.tar.gz foxgis-server-lite-macos.tar.gz docs/
- uses: JamesIves/github-pages-deploy-action@3.7.1
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BASE_BRANCH: master
BRANCH: gh-pages
FOLDER: docs
# Logs
logs
*.log
npm-debug.log*
# Runtime data
pids
*.pid
*.seed
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules
jspm_packages
# Optional npm cache directory
.npm
# Optional REPL history
.node_repl_history
dist/
data/cache/**
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx lint-staged
{
"tabWidth": 2,
"singleQuote": true,
"semi": false,
"printWidth": 100,
"arrowParens": "avoid",
"trailingComma": "none",
"endOfLine": "lf"
}
MIT License
Copyright (c) 2016 jingsam
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.
[![Actions Status](https://github.com/jingsam/foxgis-server-lite/workflows/Node%20CI/badge.svg)](https://github.com/jingsam/foxgis-server-lite/actions)
# FoxGIS Server Lite
> 一款简单易用的矢量瓦片地图服务软件。
## 使用文档
请查看[帮助文档](https://jingsam.github.io/foxgis-server-lite/#/)
## 快速开始
将系统代码克隆下来后,相关命令如下:
```
yarn // 安装依赖
yarn start // 启动服务,默认服务地址是localhost:1234/api
```
此外,还有以下可用命令:
```
yarn test // 测试
yarn dist // 打包为二进制文件
yarn docs // 打开文档
```
## 项目结构
本项目基于Express搭建,并对Express默认的目录结构做了更改。Express默认的目录结构是按照Model、View、Controller组织代码,本项目则是按照服务组织代码。每个服务分配一个目录,每个服务目录下再按照MVC划分文件,各服务间尽量进行代码隔离、数据隔离,便于以后改造为微服务。
本项目的主要代码结构如下,主要的代码逻辑在`app`目录:
```
|-- app/
| |-- services/ // API服务目录
| | |-- styles/ // 地图样式服务
| | | |-- index.js // 地图样式服务入口,同时也定义了服务API的子路由
| | | |-- controller.js // 地图样式服务的Controller层,负责具体的业务过程
| | |-- tilesets/ // 地图瓦片服务
| | |-- sprites/ // 符号库服务
| | |-- fonts/ // 字体服务
| | |-- assets/ // 静态文件服务
| | |-- index.js // 服务总路由
| |-- index.js // 系统入口
| |-- routes.js // 总路由
|-- bin/ // 执行文件目录
| |-- www // 系统启动脚本
|-- data/ // 系统数据目录
|-- docs/ // 文档目录
|-- test/ // 测试文件
```
const express = require('express')
const compression = require('compression')
const morgan = require('morgan')
const cors = require('cors')
const services = require('./services')
const app = express()
app.disable('x-powered-by')
app.set('json spaces', 2)
app.set('trust proxy', true)
app.use(morgan('dev'))
app.use(cors())
app.use(compression())
app.use(express.json({ limit: '5mb' }))
app.use(express.urlencoded({ extended: false }))
app.use(express.static('public'))
app.use('/api', services)
// catch 404 and forward to error handler
app.use((req, res, next) => {
next({ status: 404, message: 'URL错误,请检查URL是否正确。' })
})
// error handler
// eslint-disable-next-line no-unused-vars
app.use((err, req, res, next) => {
if (err.code === 'ENOENT') return res.sendStatus(404)
res.status(err.status || 500)
res.json({
message: err.message,
error: err.stack && err.stack.split('\n')
})
})
module.exports = app
const express = require('express')
const router = express.Router()
router.use('/assets', express.static('./data/assets'))
module.exports = router
const fs = require('fs')
const path = require('path')
const glyphPbfComposite = require('@mapbox/glyph-pbf-composite')
module.exports.list = (req, res, next) => {
const fontsDir = path.resolve(`./data/fonts`)
fs.readdir(fontsDir, (err, files) => {
if (err) return next(err)
const promises = files.map(file => {
return new Promise(resolve => {
fs.stat(path.join(fontsDir, file), (err, stats) => {
if (err || !stats.isDirectory()) return resolve()
resolve(file)
})
})
})
Promise.all(promises)
.then(fontIds => {
const fonts = fontIds.filter(Boolean)
res.json(fonts)
})
.catch(err => {
next(err)
})
})
}
module.exports.getGlyphs = (req, res, next) => {
const { start, end } = req.params
const fontIds = req.params.fontIds.split(',').map(fontId => fontId.trim())
const fontsDir = path.resolve(`./data/fonts`)
const glyphPaths = fontIds.map(fontId => `${fontsDir}/${fontId}/${start}-${end}.pbf`)
const promises = glyphPaths.map(glyphPath => {
return new Promise(resolve => {
fs.readFile(glyphPath, (err, buffer) => {
if (err) return resolve()
resolve(buffer)
})
})
})
Promise.all(promises)
.then(buffers => {
const glyphs = buffers.filter(buffer => buffer && buffer.length > 0)
if (glyphs.length === 0) return res.sendStatus(404)
res.set('Content-Type', 'application/x-protobuf')
res.send(glyphPbfComposite.combine(glyphs))
})
.catch(err => {
next(err)
})
}
const router = require('express').Router()
const fonts = require('./controller')
router.get('/fonts', fonts.list)
router.get('/fonts/:fontIds/:start(\\d+)-:end(\\d+).pbf', fonts.getGlyphs)
module.exports = router
const express = require('express')
const styles = require('./styles')
const tilesets = require('./tilesets')
const sprites = require('./sprites')
const fonts = require('./fonts')
const assets = require('./assets')
const tdt = require('./tdt')
const router = express.Router()
router.use(styles)
router.use(tilesets)
router.use(sprites)
router.use(fonts)
router.use(assets)
router.use(tdt)
module.exports = router
const fs = require('fs')
const path = require('path')
module.exports.list = (req, res, next) => {
const spritesDir = path.resolve(`./data/sprites`)
fs.readdir(spritesDir, (err, files) => {
if (err) return next(err)
const promises = files.map(file => {
return new Promise(resolve => {
fs.stat(path.join(spritesDir, file), (err, stats) => {
if (err || !stats.isDirectory()) return resolve()
resolve(file)
})
})
})
Promise.all(promises)
.then(spriteIds => {
const sprites = spriteIds.filter(Boolean)
res.json(sprites)
})
.catch(err => {
next(err)
})
})
}
module.exports.getSprite = (req, res, next) => {
const { spriteId, scale = '', format = 'json' } = req.params
const spritePath = path.resolve(`./data/sprites/${spriteId}/sprite${scale}.${format}`)
res.sendFile(spritePath, err => {
if (err) return next(err)
})
}
const router = require('express').Router()
const sprites = require('./controller')
router.get('/sprites', sprites.list)
router.get('/sprites/:spriteId/sprite:scale(@2x)?.:format(png|json)?', sprites.getSprite) // prettier-ignore
module.exports = router
const fs = require('fs')
const path = require('path')
const consolidate = require('consolidate')
module.exports.list = (req, res, next) => {
const stylesDir = path.resolve(`./data/styles`)
fs.readdir(stylesDir, (err, files) => {
if (err) return next(err)
const styles = files
.filter(file => path.extname(file) === '.json')
.map(file => path.parse(file).name)
res.json(styles)
})
}
module.exports.get = (req, res, next) => {
const { styleId } = req.params
const stylePath = path.resolve(`./data/styles/${styleId}.json`)
res.sendFile(stylePath, err => {
if (err) return next(err)
})
}
module.exports.getHtml = (req, res, next) => {
const { styleId } = req.params
consolidate.ejs(path.join(__dirname, './template.html'), { styleId }, (err, html) => {
if (err) return next(err)
res.send(html)
})
}
const router = require('express').Router()
const styles = require('./controller')
router.get('/styles', styles.list)
router.get('/styles/:styleId', styles.get)
router.get('/styles/:styleId/html', styles.getHtml)
module.exports = router
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title><%= styleId %></title>
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />
<script src="/api/assets/mapbox-gl.js"></script>
<link rel="stylesheet" href="/api/assets/mapbox-gl.css" />
<script src="/api/assets/plugin.js"></script>
<link rel="stylesheet" href="/api/assets/plugin.css" />
<style>
html,
body,
#map {
width: 100%;
height: 100%;
padding: 0;
margin: 0;
}
</style>
</head>
<body>
<div id="map"></div>
<script>
const map = new mapboxgl.Map({
container: 'map',
style: './',
attributionControl: false,
hash: true
})
map.addControl(new mapboxgl.NavigationControl())
map.addControl(new mapboxgl.FullscreenControl())
map.addControl(new GeolocateInfoControl(), 'bottom-left')
map.addControl(new mapboxgl.ScaleControl())
</script>
</body>
</html>
const fs = require('fs')
const path = require('path')
const https = require('https')
function downloadFile(url, filepath) {
if (!fs.existsSync(filepath)) {
fs.mkdirSync(path.dirname(filepath), { recursive: true })
}
return new Promise((resolve, reject) => {
//忽略证书认证
process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = 0
try {
https.get(
url,
{
headers: {
'User-Agent':
'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1 Edg/102.0.5005.124'
}
},
res => {
if (res.statusCode === 200) {
//使用数据流写入
res.pipe(fs.createWriteStream(filepath))
resolve()
} else {
res.resume()
console.log(`Request Failed With a Status Code: ${res.statusCode}`)
reject()
}
}
)
} catch (err) {
console.log(err)
reject()
}
})
}
module.exports.cache = async (req, res, next) => {
const { id, x, y, z, tk, v = 'DEFAULE' } = req.params
const host = 't0'
const uri = `https://${host}.tianditu.gov.cn/${id}_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=${id}&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX=${z}&TILEROW=${y}&TILECOL=${x}&tk=${tk}`
const src = path.resolve(`./data/cache/tdt/${v}/${id}/${z}/${x}/${y}.png`)
try {
if (!fs.existsSync(src)) {
await downloadFile(uri, src)
}
} catch (_) {}
res.sendFile(src, err => {
if (err) return next(err)
})
}
const router = require('express').Router()
const tdt = require('./controller')
router.get('/tdt/cache/:id/:z/:y/:x/:tk', tdt.cache)
module.exports = router
const fs = require('fs')
const path = require('path')
const MBTiles = require('@mapbox/mbtiles')
const consolidate = require('consolidate')
module.exports.list = (req, res, next) => {
const tilesetsDir = path.resolve(`./data/tilesets`)
fs.readdir(tilesetsDir, (err, files) => {
if (err) return next(err)
const tilesets = files
.filter(file => path.extname(file) === '.mbtiles')
.map(file => path.parse(file).name)
res.json(tilesets)
})
}
module.exports.getTilejson = (req, res, next) => {
const { tilesetId } = req.params
const tilesetsDir = path.resolve(`./data/tilesets`)
const source = `mbtiles://${tilesetsDir}/${tilesetId}.mbtiles?mode=ro`
new MBTiles(source, (err, source) => {
if (err) return next(err)
source.getInfo((err, info) => {
source.close()
if (err) return next(err)
const apiBaseUrl = `${req.protocol}://${req.headers.host}/api`
info.tiles = info.tiles || [`${apiBaseUrl}/tilesets/${tilesetId}/{z}/{x}/{y}.${info.format}`]
info.type = ['pbf', 'mvt'].includes(info.format) ? 'vector' : 'raster'
res.json(info)
})
})
}
module.exports.getHtml = (req, res, next) => {
const { tilesetId } = req.params
consolidate.ejs(path.join(__dirname, './template.html'), { tilesetId }, (err, html) => {
if (err) return next(err)
res.send(html)
})
}
module.exports.getTile = (req, res, next) => {
const { tilesetId, z, x, y } = req.params
const tilesetsDir = path.resolve(`./data/tilesets`)
const source = `mbtiles://${tilesetsDir}/${tilesetId}.mbtiles?mode=ro`
new MBTiles(source, (err, source) => {
if (err) return next(err)
source.getTile(+z, +x, +y, (err, data, headers) => {
source.close()
if (err && err.message.match(/(Tile|Grid) does not exist/)) return res.sendStatus(404)
if (err) return next(err)
if (!data) return res.sendStatus(204)
delete headers['ETag']
res.set(headers).send(data)
})
})
}
const router = require('express').Router()
const tilesets = require('./controller')
router.get('/tilesets', tilesets.list)
router.get('/tilesets/:tilesetId/tilejson', tilesets.getTilejson)
router.get('/tilesets/:tilesetId/html', tilesets.getHtml)
router.get('/tilesets/:tilesetId/:z(\\d+)/:x(\\d+)/:y(\\d+).:format([\\w\\.]+)',tilesets.getTile)
module.exports = router
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title><%= tilesetId %></title>
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />
<script src="/api/assets/mapbox-gl.js"></script>
<link rel="stylesheet" href="/api/assets/mapbox-gl.css" />
<script src="/api/assets/plugin.js"></script>
<link rel="stylesheet" href="/api/assets/plugin.css" />
<style>
html,
body,
#map {
width: 100%;
height: 100%;
padding: 0;
margin: 0;
}
#menu {
position: absolute;
top: 20px;
left: 20px;
width: 200px;
box-sizing: border-box;
padding: 20px;
background-color: #fff;
}
.popup {
color: #333;
display: table;
font-size: 14px;
}
.feature:not(:last-child) {
border-bottom: 1px solid #ccc;
}
.layer:before {
content: '#';
}
.layer {
display: block;
font-weight: bold;
}
.property {
display: table-row;
}
.property-key {
display: table-cell;
padding-right: 10px;
}
.property-value {
display: table-cell;
}
</style>
</head>
<body>
<div id="map"></div>
<div id="menu">
<input id="show-attributes" type="checkbox" />
<label for="show-attributes">显示属性</label>
</div>
<script>
fetch('./tilejson')
.then(response => response.json())
.then(tilejson => {
const style = {
version: 8,
sources: {
source: {
type: 'vector',
url: './tilejson'
}
},
layers: [
{
id: 'background',
type: 'background',
paint: {
'background-color': 'hsl(55, 1%, 20%)'
}
}
]
}
tilejson.vector_layers.forEach(layer => {
const color = getColor()
const layers = [
{
id: layer.id + '-polygon',
type: 'fill',
source: 'source',
'source-layer': layer.id,
filter: ['==', '$type', 'Polygon'],
paint: {
'fill-color': color,
'fill-opacity': 0.1
}
},
{
id: layer.id + '-polygon-outline',
type: 'line',
source: 'source',
'source-layer': layer.id,
filter: ['==', '$type', 'Polygon'],
layout: {
'line-join': 'round',
'line-cap': 'round'
},
paint: {
'line-color': color,
'line-width': 1,
'line-opacity': 0.75
}
},
{
id: layer.id + '-line',
type: 'line',
source: 'source',
'source-layer': layer.id,
filter: ['==', '$type', 'LineString'],
layout: {
'line-join': 'round',
'line-cap': 'round'
},
paint: {
'line-color': color,
'line-width': 1,
'line-opacity': 0.75
}
},
{
id: layer.id + '-point',
type: 'circle',
source: 'source',
'source-layer': layer.id,
filter: ['==', '$type', 'Point'],
paint: {
'circle-color': color,
'circle-radius': 2.5,
'circle-opacity': 0.75
}
}
]
style.layers = style.layers.concat(layers)
})
const center = tilejson.center || [0, 0, 1]
var map = new mapboxgl.Map({
container: 'map',
attributionControl: false,
style,
center: [center[0], center[1]],
zoom: center[2],
hash: true
})
map.addControl(new mapboxgl.NavigationControl())
map.addControl(new mapboxgl.FullscreenControl())
map.addControl(new GeolocateInfoControl(), 'bottom-left')
map.addControl(new mapboxgl.ScaleControl())
map.showTileBoundaries = true
const popup = new mapboxgl.Popup({
closeButton: false,
closeOnClick: false
})
const input = document.getElementById('show-attributes')
map.on('mousemove', e => {
if (!input.checked) return
const features = map.queryRenderedFeatures(e.point) || []
map.getCanvas().style.cursor = features.length ? 'pointer' : ''
if (!features.length) return popup.remove()
popup.setLngLat(e.lngLat).setHTML(renderPopup(features)).addTo(map)
})
input.addEventListener('change', e => {
if (!e.target.checked) return popup.remove()
})
})
let i = 0
function getColor() {
const colors = [
'#FC49A3', // pink
'#CC66FF', // purple-ish
'#66CCFF', // sky blue
'#66FFCC', // teal
'#00FF00', // lime green
'#FFCC66', // light orange
'#FF6666', // salmon
'#FF0000', // red
'#FF8000', // orange
'#FFFF66', // yellow
'#00FFFF' // turquoise
]
const color = colors[i]
i = (i + 1) % colors.length
return color
}
function renderPopup(features) {
return `
<div class="popup">
${features
.map(
feature => `
<div id="feature">${renderFeature(feature)}</div>
`
)
.join('')}
</div>
`
}
function renderFeature(feature) {
const sourceLayer = feature.layer['source-layer'] || feature.layer.source
const properties = []
if (feature.id) properties.push(['$id', feature.id])
properties.push(['$type', feature.geometry.type])
Object.keys(feature.properties).forEach(key => {
properties.push([key, feature.properties[key]])
})
return `
<div class="layer">${sourceLayer}</div>
${properties
.map(
property => `
<div class="property">
<div class="property-key">${property[0]}</div>
<div class="property-value">${property[1]}</div>
</div>
`
)
.join('')}
`
}
</script>
</body>
</html>
#!/usr/bin/env node
/**
* Set default environment variables.
*/
process.env.PORT = process.env.PORT || '1234';
/**
* Module dependencies.
*/
var app = require('../app');
var http = require('http');
/**
* Get port from environment and store in Express.
*/
var port = normalizePort(process.env.PORT);
app.set('port', port);
/**
* Create HTTP server.
*/
var server = http.createServer(app);
/**
* Listen on provided port, on all network interfaces.
*/
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);
/**
* Normalize a port into a number, string, or false.
*/
function normalizePort(val) {
var port = parseInt(val, 10);
if (isNaN(port)) {
// named pipe
return val;
}
if (port >= 0) {
// port number
return port;
}
return false;
}
/**
* Event listener for HTTP server "error" event.
*/
function onError(error) {
if (error.syscall !== 'listen') {
throw error;
}
var bind = typeof port === 'string'
? 'Pipe ' + port
: 'Port ' + port;
// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges');
process.exit(1);
break;
case 'EADDRINUSE':
console.error(bind + ' is already in use');
process.exit(1);
break;
default:
throw error;
}
}
/**
* Event listener for HTTP server "listening" event.
*/
function onListening() {
var addr = server.address();
var bind = typeof addr === 'string'
? 'pipe ' + addr
: 'port ' + addr.port;
console.log('Listening on ' + bind);
}
This source diff could not be displayed because it is too large. You can view the blob instead.
.mapboxgl-ctrl-lnglat {
background: rgb(0 0 0 / 40%);
color: white;
line-height: 2em;
padding: 0 10px;
}
.mapboxgl-ctrl-bottom-left {
display: flex;
bottom: 0;
left: 2px;
}
.mapboxgl-popup-content {
box-shadow: 0 0 20px #999;
}
#input {
position: absolute;
top: 0;
left: 0;
opacity: 0;
z-index: -10;
}
/**
* 经纬度坐标拾取控件
*/
class GeolocateInfoControl {
toText(lngLat) {
return '经度:' + lngLat.lng.toFixed(6) + ', 纬度:' + lngLat.lat.toFixed(6)
}
onAdd(map) {
this.map = map
this.container = document.createElement('div')
this.container.className = 'mapboxgl-ctrl mapboxgl-ctrl-lnglat'
this.container.textContent = this.toText(map.getCenter())
// 鼠标移动,显示经纬度信息
map.on('mousemove', e => {
const element = this.container
element.textContent = this.toText(e.lngLat)
})
// 创建一个 Marker 实例
const popup = new mapboxgl.Popup({ offset: 25, closeOnClick: false, closeButton: false })
.setLngLat(map.getCenter())
.setText(this.toText(map.getCenter()))
.addTo(map)
const marker = new mapboxgl.Marker().setLngLat(map.getCenter()).addTo(map)
// 创建一个存储经纬度的容器,用于复制功能
const textarea = document.createElement('textarea')
textarea.id = 'geolocate-info'
document.body.appendChild(textarea)
// 监听地图点击事件,复制经纬度
map.on('click', e => {
const lonlat = this.toText(e.lngLat)
// 更新 marker 的位置
popup.setLngLat(e.lngLat).setText(lonlat)
marker.setLngLat(e.lngLat)
// 更新 textarea 的内容
textarea.textContent = lonlat.replace('经度:', '').replace('纬度:', '')
textarea.select()
// 执行复制命令
document.execCommand('Copy')
})
return this.container
}
onRemove() {
this.container.parentNode.removeChild(this.container)
this.map = undefined
}
}
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论