执行下载瓦片地图
下载代码
import argparse
import math
import os
import sys
import time
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen
def deg2num(lat_deg: float, lon_deg: float, zoom: int) -> tuple[int, int]:
lat_rad = math.radians(lat_deg)
n = 2.0 ** zoom
xtile = int((lon_deg + 180.0) / 360.0 * n)
ytile = int((1.0 - math.asinh(math.tan(lat_rad)) / math.pi) / 2.0 * n)
return xtile, ytile
def clamp(v: int, lo: int, hi: int) -> int:
return max(lo, min(hi, v))
def download(url: str, out_path: str, user_agent: str, timeout_s: int) -> bool:
os.makedirs(os.path.dirname(out_path), exist_ok=True)
if os.path.exists(out_path) and os.path.getsize(out_path) > 0:
return True
req = Request(url, headers={"User-Agent": user_agent})
try:
with urlopen(req, timeout=timeout_s) as resp:
data = resp.read()
with open(out_path, "wb") as f:
f.write(data)
return True
except HTTPError as e:
print(f"[HTTP {e.code}] {url}", file=sys.stderr)
except URLError as e:
print(f"[URL ERR] {url} {e}", file=sys.stderr)
except Exception as e:
print(f"[ERR] {url} {e}", file=sys.stderr)
return False
def main() -> int:
p = argparse.ArgumentParser(description="Download standard XYZ tiles into static/ditu/{z}/{x}/{y}.png")
p.add_argument("--template", default="https://tile.openstreetmap.org/{z}/{x}/{y}.png",
help="Tile URL template. Must contain {z} {x} {y}. Prefer your own/local tile server if possible.")
p.add_argument("--out", default=os.path.join("static", "ditu"),
help="Output directory root. Default: static/ditu")
p.add_argument("--min-zoom", type=int, default=0)
p.add_argument("--max-zoom", type=int, default=2)
p.add_argument("--min-lat", type=float, default=-85.0)
p.add_argument("--max-lat", type=float, default=85.0)
p.add_argument("--min-lng", type=float, default=-180.0)
p.add_argument("--max-lng", type=float, default=180.0)
p.add_argument("--sleep-ms", type=int, default=200,
help="Delay between requests (ms). Be polite to tile servers.")
p.add_argument("--timeout-s", type=int, default=20)
p.add_argument("--retries", type=int, default=2, help="Retry count for failed downloads.")
p.add_argument("--user-agent", default="jspt-lc-windows-ai/1.0 (offline tile downloader)",
help="HTTP User-Agent header.")
args = p.parse_args()
if args.min_zoom > args.max_zoom:
raise SystemExit("--min-zoom must be <= --max-zoom")
total = 0
ok = 0
for z in range(args.min_zoom, args.max_zoom + 1):
max_tile = (2 ** z) - 1
x0, y0 = deg2num(args.max_lat, args.min_lng, z) # top-left
x1, y1 = deg2num(args.min_lat, args.max_lng, z) # bottom-right
x0 = clamp(min(x0, x1), 0, max_tile)
x1 = clamp(max(x0, x1), 0, max_tile)
y0 = clamp(min(y0, y1), 0, max_tile)
y1 = clamp(max(y0, y1), 0, max_tile)
z_total = (x1 - x0 + 1) * (y1 - y0 + 1)
z_done = 0
z_ok = 0
print(f"[z={z}] x:{x0}-{x1} y:{y0}-{y1} tiles:{z_total}")
for x in range(x0, x1 + 1):
for y in range(y0, y1 + 1):
total += 1
z_done += 1
url = args.template.replace("{z}", str(z)).replace("{x}", str(x)).replace("{y}", str(y))
out_path = os.path.join(args.out, str(z), str(x), f"{y}.png")
success = False
for attempt in range(0, max(0, args.retries) + 1):
if download(url, out_path, args.user_agent, args.timeout_s):
success = True
break
time.sleep(0.4 * (attempt + 1))
if success:
ok += 1
z_ok += 1
time.sleep(max(0, args.sleep_ms) / 1000.0)
if z_done % 200 == 0 or z_done == z_total:
print(f"[z={z}] {z_done}/{z_total} ok:{z_ok} (all:{ok}/{total})")
print(f"[z={z}] done ok:{z_ok}/{z_total}")
print(f"Downloaded {ok}/{total} tiles into {args.out}")
return 0 if ok == total else 2
if __name__ == "__main__":
raise SystemExit(main())
执行命令
python -u tools/download_xyz_tiles.py --template "http://webrd01.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}" --out static/ditu --min-zoom 14 --max-zoom 20 --min-lat 30.346387 --max-lat 30.349999 --min-lng 119.977740 --max-lng 119.984449 --sleep-ms 0 --timeout-s 20 --retries 2
全国地图
python -u tools/download_xyz_tiles.py --template "http://webrd01.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}" --out static/ditu --min-zoom 11 --max-zoom 18 --min-lat 18 --max-lat 54 --min-lng 73 --max-lng 135 --sleep-ms 0 --timeout-s 20 --retries 2
代码地图显示,绘制管线
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="initial-scale=1.0, user-scalable=no, width=device-width">
<link rel="stylesheet" href="/static/css/ol.css">
<style>
html,
body,
#container {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
#container {
position: relative;
background: #f3f3f3;
}
#map {
width: 100%;
height: 100%;
}
.ipt-wrap {
padding: 0 20px;
position: relative;
}
.ipt-wrap .text {
opacity: 0;
}
.text-input {
position: absolute;
top: 0px;
left: 0px;
width: 100% !important;
border: none !important;
outline: none !important;
font-weight: 700;
display: inline-block !important;
background-color: transparent !important;
opacity: 1 !important;
padding: 0 !important;
}
.text-input:focus {
background-color: transparent !important;
}
.input-card {
position: absolute;
right: 10px;
bottom: 10px;
background: rgba(255, 255, 255, 0.95);
border-radius: 6px;
padding: 10px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.15);
z-index: 1200;
}
.btn {
cursor: pointer;
border: none;
border-radius: 4px;
padding: 6px 10px;
background: #2d8cf0;
color: #fff;
}
.set-fontsize {
flex-direction: row !important;
bottom: 6rem !important;
display: flex;
gap: 10px;
}
.set-fontsize .btn {
width: 40px !important;
}
</style>
<title>管线绘制(本地地图 · OpenLayers)</title>
<script src="/static/js/ol.js"></script>
<script src="/static/js/vue.min.js"></script>
<script src="/static/js/axios.min.js"></script>
<script src="/static/js/coordinateTransformer.js"></script>
</head>
<body>
<div id="container">
<div id="map"></div>
</div>
<div id="newApp">
<div v-if="isShow" class="input-card" style="width: 120px">
<button class="btn" @click="closePolyEditorArr">结束编辑</button>
</div>
<div class="input-card set-fontsize" style="width: 120px">
<button class="btn" @click="fontsizeBtn(1)">加</button>
<button class="btn" @click="fontsizeBtn(-1)">减</button>
</div>
</div>
<script type="text/javascript">
new Vue({
el: '#newApp',
data: {
tileUrlTpl: '/static/ditu/{z}/{x}/{y}.png',
placeholderUrl: '/static/imgs/tile_no_permission.svg',
map: null,
vectorSource: null,
vectorLayer: null,
select: null,
modify: null,
labelOverlays: [],
color1: '#E91E4E',
color2: '#1E88E5',
isShow: false,
ids: "{{ ids }}" || '',
fontSize: 13,
},
mounted() {
this.initOlMap();
this.loadServerData();
},
methods: {
initOlMap() {
const vm = this;
const tileSource = new ol.source.XYZ({
url: this.tileUrlTpl,
maxZoom: 20,
tileLoadFunction: function (tile, src) {
const img = tile.getImage();
if (!(img instanceof HTMLImageElement)) return;
img.onerror = function () {
img.onerror = null;
img.src = vm.placeholderUrl;
};
img.src = src;
},
});
const rasterLayer = new ol.layer.Tile({
source: tileSource,
});
this.vectorSource = new ol.source.Vector();
this.vectorLayer = new ol.layer.Vector({
source: this.vectorSource,
style: function (feature) {
return vm.featureStyleFunction(feature);
},
});
this.map = new ol.Map({
target: 'map',
layers: [rasterLayer, this.vectorLayer],
view: new ol.View({
center: ol.proj.fromLonLat(this.lonLatGCJ(116.395577, 39.892257)),
zoom: 14,
maxZoom: 22,
}),
});
this.select = new ol.interaction.Select({
layers: [this.vectorLayer],
hitTolerance: 12,
filter: function (feature) {
return feature.getGeometry().getType() === 'LineString';
},
});
this.modify = new ol.interaction.Modify({
features: this.select.getFeatures(),
insertVertexCondition: function () {
return false;
},
});
this.map.addInteraction(this.select);
this.map.addInteraction(this.modify);
this.select.on('select', function (e) {
vm.isShow = e.selected.length > 0;
});
},
featureStyleFunction(feature) {
const geom = feature.getGeometry();
const type = geom.getType();
if (type === 'LineString') {
const color = feature.get('strokeColor') || this.color2;
const width = feature.get('strokeWidth') || 4;
return new ol.style.Style({
stroke: new ol.style.Stroke({
color: color,
width: width,
lineCap: 'round',
lineJoin: 'round',
}),
});
}
if (type === 'Point' && feature.get('isMarker')) {
const color = feature.get('markerColor') || '#0080FF';
return new ol.style.Style({
image: new ol.style.Circle({
radius: 6,
fill: new ol.style.Fill({ color: color }),
stroke: new ol.style.Stroke({ color: '#ffffff', width: 2 }),
}),
});
}
return null;
},
fontsizeBtn(type) {
const fontSize = this.fontSize;
if (type < 0 && fontSize <= 12) return;
if (type > 0 && fontSize >= 35) return;
this.fontSize = fontSize + type;
const textArr = document.getElementsByClassName('ipt-wrap');
for (let i = 0; i < textArr.length; i++) {
textArr[i].style.fontSize = this.fontSize + 'px';
}
},
loadServerData() {
const formData = new FormData();
formData.append("ids", this.ids);
axios({
method: 'post',
url: '/mapFinish',
data: formData
}).then((res) => {
if (res.data && res.data.success === true) {
this.disposeData(res.data.data || []);
} else {
window.alert((res.data && res.data.msg) || '加载失败');
}
});
},
// 经纬度处理 把经纬度 处理为 高德 GCJ-02 坐标
lonLatGCJ(lng, lat) {
let info = transform(lng, lat)
return [info.lon, info.lat]
},
disposeData(res) {
if (!res || !res.length || !this.map) return;
for (let i = 0; i < res.length; i++) {
const item = res[i];
const points = [];
for (let j = 0; j < (item.line || []).length; j++) {
const parts = String(item.line[j]).split(" ");
const lat = parseFloat(parts[0]);
const lng = parseFloat(parts[1]);
if (!isNaN(lat) && !isNaN(lng)) {
points.push(ol.proj.fromLonLat(this.lonLatGCJ(lng, lat)));
// let info = transform(lng, lat)
// points.push(ol.proj.fromLonLat([info.lon, info.lat]));
}
}
let pageColor = this.color2;
let num = 2;
if ((i + 1) % 2) {
pageColor = this.color1;
num = 1;
}
if (points.length) {
this.addLineFeature(points, pageColor, 4);
const midIdx = Math.floor(points.length / 2);
const c = points[midIdx];
if (i === 0) {
this.map.getView().setCenter(c);
this.map.getView().setZoom(17);
}
const ll = ol.proj.toLonLat(c);
this.drawTextOverlay(ll[0], ll[1], `${item.name || '暂无'}`, pageColor, true);
}
const rtkArr = [];
for (let k = 0; k < (item.rtk || []).length; k++) {
rtkArr.push(String(item.rtk[k]).split(" "));
}
if (rtkArr.length > 1) {
const rtkBegin = {
north: rtkArr[0][0],
east: rtkArr[0][1],
height: rtkArr[0][2],
lat: parseFloat(rtkArr[0][4]),
lng: parseFloat(rtkArr[0][5])
};
const endIdx = rtkArr.length - 1;
const rtkEnd = {
north: rtkArr[endIdx][0],
east: rtkArr[endIdx][1],
height: rtkArr[endIdx][2],
lat: parseFloat(rtkArr[endIdx][4]),
lng: parseFloat(rtkArr[endIdx][5])
};
const numE = Math.abs(rtkBegin.lng - rtkEnd.lng);
const numN = Math.abs(rtkBegin.lat - rtkEnd.lat);
this.addMarkerFeature(rtkBegin.lng, rtkBegin.lat, pageColor);
this.addMarkerFeature(rtkEnd.lng, rtkEnd.lat, pageColor);
if (numE > numN) {
this.markMyLine(rtkBegin.lng, rtkBegin.lat, rtkBegin, pageColor, 'begin');
this.markMyLine(rtkEnd.lng, rtkEnd.lat, rtkEnd, pageColor, 'end');
} else {
this.markMyLine(rtkBegin.lng, rtkBegin.lat, rtkBegin, pageColor, 'begin');
this.markMyLine(rtkEnd.lng, rtkEnd.lat, rtkEnd, pageColor, 'end');
}
}
}
},
addLineFeature(coords3857, color, width) {
const f = new ol.Feature({
geometry: new ol.geom.LineString(coords3857),
});
f.set('strokeColor', color);
f.set('strokeWidth', width);
this.vectorSource.addFeature(f);
},
addMarkerFeature(lng, lat, color) {
const f = new ol.Feature({
geometry: new ol.geom.Point(ol.proj.fromLonLat(this.lonLatGCJ(lng, lat))),
});
f.set('isMarker', true);
f.set('markerColor', color);
this.vectorSource.addFeature(f);
},
drawTextOverlay(lng, lat, title, color, status) {
console.log(lng, lat, title, color)
const el = document.createElement('div');
el.innerHTML =
`<div class='ipt-wrap' style="font-size:${this.fontSize}px;">
<div class='text'>${title}</div>
<input type='text' value="${title}" class='text-input' style="width:100%;color:${color}">
</div>`;
const overlay = new ol.Overlay({
element: el,
positioning: 'center-center',
stopEvent: false,
});
if (status) {
overlay.setPosition(ol.proj.fromLonLat([lng, lat]));
} else {
overlay.setPosition(ol.proj.fromLonLat(this.lonLatGCJ(lng, lat)));
}
this.map.addOverlay(overlay);
this.labelOverlays.push(overlay);
},
markMyLine(lng, lat, info, pageColor, type) {
const lngN = parseFloat(lng);
const latN = parseFloat(lat);
let lng1 = 0.00015;
let lng2 = 0.00045;
let lat1 = 0.00015;
let lat2 = 0.00015;
if (type === 'end') {
lng1 = -0.00015;
lng2 = -0.00045;
}
const path = [
ol.proj.fromLonLat(this.lonLatGCJ(lngN, latN)),
ol.proj.fromLonLat(this.lonLatGCJ(lngN + lng1, latN + lat1)),
ol.proj.fromLonLat(this.lonLatGCJ(lngN + lng2, latN + lat2)),
];
this.addLineFeature(path, pageColor, 1);
let pathXL = lngN + 0.0003;
let pathYL = latN + 0.0002;
let pathXL2 = lngN + 0.0003;
let pathYL2 = latN + 0.0001;
if (type === 'end') {
pathXL = lngN - 0.0003;
pathXL2 = lngN - 0.0003;
}
this.drawTextOverlay(pathXL, pathYL, `X:${info.north}`, pageColor, false);
this.drawTextOverlay(pathXL2, pathYL2, `Y:${info.east}`, pageColor, false);
},
closePolyEditorArr() {
if (this.select) {
this.select.getFeatures().clear();
}
this.isShow = false;
},
}
})
</script>
</body>
</html>