无网络地图展示轨迹,地图瓦片下载,绘制管线

执行下载瓦片地图

下载代码

复制代码
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>
相关推荐
土豆12502 小时前
Tauri 入门与实践:用 Rust 构建你的下一个桌面应用
前端·rust
小陈工3 小时前
2026年4月2日技术资讯洞察:数据库融合革命、端侧AI突破与脑机接口产业化
开发语言·前端·数据库·人工智能·python·安全
IT_陈寒4 小时前
Vue的这个响应式问题,坑了我整整两小时
前端·人工智能·后端
HIT_Weston4 小时前
41、【Agent】【OpenCode】本地代理分析(五)
javascript·人工智能·opencode
C澒4 小时前
AI 生码:A 类生码方案架构升级
前端·ai编程
前端Hardy4 小时前
前端必看!LocalStorage这么用,再也不踩坑(多框架通用,直接复制)
前端·javascript·面试
前端Hardy4 小时前
前端必看!前端路由守卫这么写,再也不担心权限混乱(Vue/React通用)
前端·javascript·面试
Lee川5 小时前
从零构建现代化登录界面:React + Tailwind CSS 前端工程实践
前端·react.js
Awu12275 小时前
⚡精通 Claude 第 1 课:掌握 Slash Commands
前端·人工智能·ai编程