【开源解析】基于PyQt5+Folium的谷歌地图应用开发:从入门到实战

06-01 1184阅读

🌐【开源解析】基于PyQt5+Folium的谷歌地图应用开发:从入门到实战【开源解析】基于PyQt5+Folium的谷歌地图应用开发:从入门到实战

🌈 个人主页:创客白泽 - CSDN博客

🔥 系列专栏:🐍《Python开源项目实战》

💡 热爱不止于代码,热情源自每一个灵感闪现的夜晚。愿以开源之火,点亮前行之路。

👍 如果觉得这篇文章有帮助,欢迎您一键三连,分享给更多人哦

【开源解析】基于PyQt5+Folium的谷歌地图应用开发:从入门到实战

【开源解析】基于PyQt5+Folium的谷歌地图应用开发:从入门到实战

📌 概述

在当今数据可视化与地理信息系统的交叉领域,交互式地图应用已成为不可或缺的工具。本文将深入剖析一个基于Python技术栈(PyQt5+Folium+Geopy)开发的**"谷歌地图"桌面应用**,它集成了地址解析、地图标注、距离测量等实用功能,并提供了三种不同的地图样式选择。

相较于传统Web地图应用,本项目的创新点在于:

  • 桌面端集成:通过PyQt5实现原生应用体验
  • 混合渲染技术:结合Folium的HTML生成与QWebEngineView的嵌入式渲染
  • 跨框架通信:实现Python与JavaScript的双向交互
  • 轻量级架构:无需复杂GIS系统即可实现核心功能

    🛠️ 功能特性

    核心功能矩阵

    功能模块实现技术特色说明
    地理编码Geopy/Nominatim支持全球地址解析
    地图渲染Folium+Leaflet三种专业地图样式
    距离测量Geodesic算法高精度大圆距离计算
    交互界面PyQt5响应式桌面UI
    地图导出HTML5可独立运行的网页地图

    特色功能详解

    1. 智能地址解析:基于OpenStreetMap的Nominatim服务,支持模糊地址匹配
    2. 实时距离测量:选择两个标记点即可显示精确的球面距离
    3. 动态标记高亮:可视化连线辅助空间关系分析
    4. 多地图样式:街道图、卫星图、地形图一键切换
    5. 跨平台运行:生成的HTML地图可在任何浏览器查看

    🎨 效果展示

    街道地图

    【开源解析】基于PyQt5+Folium的谷歌地图应用开发:从入门到实战

    卫星地图

    【开源解析】基于PyQt5+Folium的谷歌地图应用开发:从入门到实战

    【开源解析】基于PyQt5+Folium的谷歌地图应用开发:从入门到实战

    地形图

    【开源解析】基于PyQt5+Folium的谷歌地图应用开发:从入门到实战

    【开源解析】基于PyQt5+Folium的谷歌地图应用开发:从入门到实战

    距离测量演示

    【开源解析】基于PyQt5+Folium的谷歌地图应用开发:从入门到实战

    🧩 实现步骤详解

    1. 环境搭建

    pip install PyQt5 folium geopy PyQtWebEngine
    

    2. 核心架构设计

    【开源解析】基于PyQt5+Folium的谷歌地图应用开发:从入门到实战

    3. 关键技术实现

    3.1 混合地图渲染
    def initialize_map(self):
        # 加载Leaflet库
        html = """
        
        
        
            
        
        
            
            
                // JavaScript地图控制逻辑
                var map = L.map('map').setView([39.9042, 116.4074], 4);
            
        
        
        """
        self.map_view.setHtml(html)
    
    3.2 跨语言通信
    # Python调用JavaScript
    self.map_view.page().runJavaScript("addMarker(39.9, 116.4, '北京', '中国首都');")
    # JavaScript回调Python
    self.map_view.page().runJavaScript("""
        map.on('click', function(e) {
            console.log(e.latlng);
        });
    """)
    
    3.3 距离测量算法
    from geopy.distance import geodesic
    def calculate_distance(loc1, loc2):
        """使用Vincenty公式计算球面距离"""
        return geodesic(
            (loc1['latitude'], loc1['longitude']),
            (loc2['latitude'], loc2['longitude'])
        ).kilometers
    

    🔍 代码深度解析

    1. 地理编码服务封装

    def geocode_location(self, address):
        try:
            location = self.geolocator.geocode(address)
            if location:
                return (location.latitude, location.longitude)
            return None
        except (GeocoderTimedOut, GeocoderServiceError) as e:
            # 实现自动重试机制
            time.sleep(0.5)
            return self.geocode_location(address)
    

    优化点:增加了异常处理和自动重试机制,提高服务稳定性

    2. 动态标记管理

    def update_embedded_map(self):
        # 使用JS批量操作DOM元素
        js_clear = "clearMarkers();"
        js_add_markers = []
        
        for loc in self.locations:
            js_add_markers.append(
                f"addMarker({loc['latitude']}, {loc['longitude']}, "
                f"'{loc['name']}', '{loc['address']}');"
            )
        
        self.map_view.page().runJavaScript(js_clear + "".join(js_add_markers))
    

    性能优化:减少Python-JS通信次数,使用批量操作提升渲染效率

    3. 地图样式热切换

    self.map_styles = {
        "🌍 街道地图": {
            "url": "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
            "attr": "OpenStreetMap"
        },
        "🛰️ 卫星地图": {
            "url": "https://mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={z}",
            "attr": "Google"
        }
    }
    def update_map_style(self):
        style = next(s for s in self.map_styles 
                    if self.style_buttons[s].isChecked())
        
        js = f"""
        map.eachLayer(layer => {{
            if (layer instanceof L.TileLayer) {{
                map.removeLayer(layer);
            }}
        }});
        L.tileLayer('{self.map_styles[style]["url"]}', {{
            attribution: '{self.map_styles[style]["attr"]}'
        }}).addTo(map);
        """
        self.map_view.page().runJavaScript(js)
    

    📥 源码下载

    import folium
    from geopy.geocoders import Nominatim
    from geopy.distance import geodesic  # 添加距离计算功能
    from geopy.exc import GeocoderTimedOut, GeocoderServiceError
    from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, 
                                QLabel, QLineEdit, QPushButton, QTreeWidget, QTreeWidgetItem,
                                QRadioButton, QGroupBox, QFileDialog, QMessageBox, QScrollArea)
    from PyQt5.QtCore import Qt, QUrl, QTimer
    from PyQt5.QtWebEngineWidgets import QWebEngineView
    from PyQt5.QtGui import QIcon
    import sys
    import webbrowser
    import os
    import time
    class SimpleMapViewerApp(QMainWindow):
        def __init__(self):
            super().__init__()
            self.setWindowTitle("谷歌桌面地图")
            self.setGeometry(100, 100, 1200, 800)
            
            self.geolocator = Nominatim(user_agent="simple_map_viewer")
            self.locations = []
            self.current_map_file = os.path.join(os.path.expanduser("~"), "map.html")
            self.map_view = None
            self.map_initialized = False
            self.selected_markers = []  # 存储选中的标记用于距离计算
            
            # 地图样式选项
            self.map_styles = {
                "🌍 街道地图": "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
                "🛰️ 卫星地图": "https://mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={z}",
                "⛰️ 地形图": "https://mt1.google.com/vt/lyrs=p&x={x}&y={y}&z={z}"
            }
            
            # 创建UI
            self.create_widgets()
            
            # 延迟初始化地图,确保WebEngineView完全加载
            QTimer.singleShot(500, self.initialize_map)
            
        def create_widgets(self):
            # 主窗口布局
            main_widget = QWidget()
            self.setCentralWidget(main_widget)
            main_layout = QHBoxLayout(main_widget)
            
            # 左侧控制面板
            control_panel = QWidget()
            control_panel.setMinimumWidth(350)
            control_panel.setMaximumWidth(400)
            control_layout = QVBoxLayout(control_panel)
            
            # 地图样式选择
            style_group = QGroupBox("🗂️ 地图样式")
            style_layout = QVBoxLayout()
            self.style_buttons = []
            
            for style_name in self.map_styles:
                btn = QRadioButton(style_name)
                btn.toggled.connect(lambda checked, name=style_name: self.update_map_style() if checked else None)
                style_layout.addWidget(btn)
                self.style_buttons.append(btn)
            
            self.style_buttons[0].setChecked(True)
            style_group.setLayout(style_layout)
            control_layout.addWidget(style_group)
            
            # 搜索框
            search_group = QGroupBox("🔍 位置搜索")
            search_layout = QVBoxLayout()
            
            self.search_entry = QLineEdit()
            self.search_entry.setPlaceholderText("输入地址或地名...")
            search_layout.addWidget(self.search_entry)
            
            search_btn = QPushButton("搜索")
            search_btn.setIcon(QIcon.fromTheme("edit-find"))
            search_btn.clicked.connect(self.search_location)
            search_layout.addWidget(search_btn)
            
            search_group.setLayout(search_layout)
            control_layout.addWidget(search_group)
            
            # 位置列表
            list_group = QGroupBox("📍 位置列表")
            list_layout = QVBoxLayout()
            
            self.location_list = QTreeWidget()
            self.location_list.setHeaderLabels(["名称", "地址"])
            self.location_list.setColumnWidth(0, 150)
            self.location_list.setSelectionMode(QTreeWidget.ExtendedSelection)
            self.location_list.itemSelectionChanged.connect(self.on_location_selection_changed)  # 添加选择变化事件
            list_layout.addWidget(self.location_list)
            
            # 距离显示标签
            self.distance_label = QLabel("两地距离: 未选择")
            self.distance_label.setAlignment(Qt.AlignCenter)
            self.distance_label.setStyleSheet("font-weight: bold; color: #2c3e50;")
            list_layout.addWidget(self.distance_label)
            
            # 列表操作按钮
            list_btn_layout = QHBoxLayout()
            remove_btn = QPushButton("🗑️ 删除选中")
            remove_btn.clicked.connect(self.remove_location)
            list_btn_layout.addWidget(remove_btn)
            
            clear_btn = QPushButton("🧹 清空列表")
            clear_btn.clicked.connect(self.clear_locations)
            list_btn_layout.addWidget(clear_btn)
            
            list_layout.addLayout(list_btn_layout)
            list_group.setLayout(list_layout)
            control_layout.addWidget(list_group)
            
            # 添加位置表单
            form_group = QGroupBox("➕ 添加位置")
            form_layout = QVBoxLayout()
            
            name_layout = QHBoxLayout()
            name_layout.addWidget(QLabel("名称:"))
            self.name_entry = QLineEdit()
            name_layout.addWidget(self.name_entry)
            form_layout.addLayout(name_layout)
            
            addr_layout = QHBoxLayout()
            addr_layout.addWidget(QLabel("地址:"))
            self.address_entry = QLineEdit()
            addr_layout.addWidget(self.address_entry)
            form_layout.addLayout(addr_layout)
            
            add_btn = QPushButton("➕ 添加位置")
            add_btn.clicked.connect(self.add_location)
            form_layout.addWidget(add_btn)
            
            form_group.setLayout(form_layout)
            control_layout.addWidget(form_group)
            
            # 地图操作按钮
            map_btn_group = QGroupBox("🛠️ 地图操作")
            map_btn_layout = QHBoxLayout()
            
            create_btn = QPushButton("🖨️ 生成地图")
            create_btn.clicked.connect(self.create_map)
            map_btn_layout.addWidget(create_btn)
            
            show_btn = QPushButton("👀 查看地图")
            show_btn.clicked.connect(self.show_map)
            map_btn_layout.addWidget(show_btn)
            
            map_btn_group.setLayout(map_btn_layout)
            control_layout.addWidget(map_btn_group)
            
            control_layout.addStretch()
            
            # 右侧地图预览
            self.map_view = QWebEngineView()
            self.map_view.setHtml(self.get_empty_html())
            
            # 添加到主布局
            main_layout.addWidget(control_panel)
            main_layout.addWidget(self.map_view, stretch=1)
            
        def get_empty_html(self):
            """返回初始空白HTML"""
            return """
            
            
            
                地图预览
                
                
            
            
                
            
            
            """
            
        def initialize_map(self):
            """初始化地图,确保Leaflet库正确加载"""
            html = """
            
            
            
                地图预览
                
                
                
                
                    var map = L.map('map').setView([39.9042, 116.4074], 4);
                    var osmLayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
                        attribution: '© OpenStreetMap contributors'
                    });
                    osmLayer.addTo(map);
                    
                    // 存储标记的数组
                    var markers = [];
                    var selectedMarkers = [];
                    var line = null;
                    
                    function clearMarkers() {
                        for (var i = 0; i ' + address)
                            .bindTooltip(name);
                        markers.push(marker);
                        return marker;
                    }
                    
                    function setView(lat, lng, zoom) {
                        map.setView([lat, lng], zoom);
                    }
                    
                    function highlightMarkers(markerIndices) {
                        // 重置所有标记样式
                        for (var i = 0; i = 0 && idx 
免责声明:我们致力于保护作者版权,注重分享,被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理! 图片声明:本站部分配图来自人工智能系统AI生成,觅知网授权图片,PxHere摄影无版权图库和百度,360,搜狗等多加搜索引擎自动关键词搜索配图,如有侵权的图片,请第一时间联系我们。

目录[+]

取消
微信二维码
微信二维码
支付宝二维码